From 3739f8e04f93f170660139a22c59ce3f064e00f5 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Tue, 30 Jun 2026 18:42:14 +0000 Subject: [PATCH 01/22] asst liab pagination +userids --- proto/autogenerated/client.md | 19 +++- proto/autogenerated/go/types.go | 25 +++-- proto/autogenerated/ts/types.ts | 125 +++++++++++++++++++++---- proto/service/structs.proto | 21 ++++- src/services/lnd/lnd.ts | 8 +- src/services/main/adminManager.ts | 44 +++++++-- src/services/main/liquidityProvider.ts | 9 +- 7 files changed, 205 insertions(+), 46 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index a69ff3075..bc72fdf4f 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1221,9 +1221,8 @@ The nostr server will send back a message response, and inside the body there wi - __users_balance__: _number_ ### AssetsAndLiabilitiesReq - - __limit_invoices__: _number_ *this field is optional - - __limit_payments__: _number_ *this field is optional - - __limit_providers__: _number_ *this field is optional + - __liquidity_providers__: ARRAY of: _[LiquidityProviderFilter](#LiquidityProviderFilter)_ + - __lnd_providers__: ARRAY of: _[LndProviderFilter](#LndProviderFilter)_ ### AuthApp - __app__: _[Application](#Application)_ @@ -1472,6 +1471,12 @@ The nostr server will send back a message response, and inside the body there wi - __pubkey__: _string_ - __tracked__: _[TrackedLiquidityProvider](#TrackedLiquidityProvider)_ *this field is optional +### LiquidityProviderFilter + - __latestIncomingInvoice__: _[OperationsCursor](#OperationsCursor)_ *this field is optional + - __latestOutgoingInvoice__: _[OperationsCursor](#OperationsCursor)_ *this field is optional + - __limit__: _number_ *this field is optional + - __pubkey__: _string_ + ### LiveDebitRequest - __debit__: _[LiveDebitRequest_debit](#LiveDebitRequest_debit)_ - __k1__: _string_ *this field is optional @@ -1535,6 +1540,13 @@ The nostr server will send back a message response, and inside the body there wi - __pending_channels__: _number_ - __root_ops__: ARRAY of: _[RootOperation](#RootOperation)_ +### LndProviderFilter + - __limit_invoices__: _number_ *this field is optional + - __limit_payments__: _number_ *this field is optional + - __pubkey__: _string_ + - __skip_invoices__: _number_ *this field is optional + - __skip_payments__: _number_ *this field is optional + ### LndSeed - __seed__: ARRAY of: _string_ @@ -1831,6 +1843,7 @@ The nostr server will send back a message response, and inside the body there wi - __amount__: _number_ - __ts__: _number_ - __type__: _[TrackedOperationType](#TrackedOperationType)_ + - __user_id__: _string_ *this field is optional ### TransactionSwapQuote - __chain_fee_sats__: _number_ diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 76a6898ea..391d70ccd 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -181,9 +181,8 @@ type AssetsAndLiabilities struct { Users_balance int64 `json:"users_balance"` } type AssetsAndLiabilitiesReq struct { - Limit_invoices int64 `json:"limit_invoices"` - Limit_payments int64 `json:"limit_payments"` - Limit_providers int64 `json:"limit_providers"` + Liquidity_providers []LiquidityProviderFilter `json:"liquidity_providers"` + Lnd_providers []LndProviderFilter `json:"lnd_providers"` } type AuthApp struct { App *Application `json:"app"` @@ -432,6 +431,12 @@ type LiquidityAssetProvider struct { Pubkey string `json:"pubkey"` Tracked *TrackedLiquidityProvider `json:"tracked"` } +type LiquidityProviderFilter struct { + Latestincominginvoice *OperationsCursor `json:"latestIncomingInvoice"` + Latestoutgoinginvoice *OperationsCursor `json:"latestOutgoingInvoice"` + Limit int64 `json:"limit"` + Pubkey string `json:"pubkey"` +} type LiveDebitRequest struct { Debit *LiveDebitRequest_debit `json:"debit"` K1 string `json:"k1"` @@ -495,6 +500,13 @@ type LndNodeMetrics struct { Pending_channels int64 `json:"pending_channels"` Root_ops []RootOperation `json:"root_ops"` } +type LndProviderFilter struct { + Limit_invoices int64 `json:"limit_invoices"` + Limit_payments int64 `json:"limit_payments"` + Pubkey string `json:"pubkey"` + Skip_invoices int64 `json:"skip_invoices"` + Skip_payments int64 `json:"skip_payments"` +} type LndSeed struct { Seed []string `json:"seed"` } @@ -788,9 +800,10 @@ type TrackedLndProvider struct { Unconfirmed_balance int64 `json:"unconfirmed_balance"` } type TrackedOperation struct { - Amount int64 `json:"amount"` - Ts int64 `json:"ts"` - Type TrackedOperationType `json:"type"` + Amount int64 `json:"amount"` + Ts int64 `json:"ts"` + Type TrackedOperationType `json:"type"` + User_id string `json:"user_id"` } type TransactionSwapQuote struct { Chain_fee_sats int64 `json:"chain_fee_sats"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index bf813a088..839bdec28 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -1016,30 +1016,34 @@ export const AssetsAndLiabilitiesValidate = (o?: AssetsAndLiabilities, opts: Ass } export type AssetsAndLiabilitiesReq = { - limit_invoices?: number - limit_payments?: number - limit_providers?: number + liquidity_providers: LiquidityProviderFilter[] + lnd_providers: LndProviderFilter[] } -export type AssetsAndLiabilitiesReqOptionalField = 'limit_invoices' | 'limit_payments' | 'limit_providers' -export const AssetsAndLiabilitiesReqOptionalFields: AssetsAndLiabilitiesReqOptionalField[] = ['limit_invoices', 'limit_payments', 'limit_providers'] +export const AssetsAndLiabilitiesReqOptionalFields: [] = [] export type AssetsAndLiabilitiesReqOptions = OptionsBaseMessage & { - checkOptionalsAreSet?: AssetsAndLiabilitiesReqOptionalField[] - limit_invoices_CustomCheck?: (v?: number) => boolean - limit_payments_CustomCheck?: (v?: number) => boolean - limit_providers_CustomCheck?: (v?: number) => boolean + checkOptionalsAreSet?: [] + liquidity_providers_ItemOptions?: LiquidityProviderFilterOptions + liquidity_providers_CustomCheck?: (v: LiquidityProviderFilter[]) => boolean + lnd_providers_ItemOptions?: LndProviderFilterOptions + lnd_providers_CustomCheck?: (v: LndProviderFilter[]) => boolean } export const AssetsAndLiabilitiesReqValidate = (o?: AssetsAndLiabilitiesReq, opts: AssetsAndLiabilitiesReqOptions = {}, path: string = 'AssetsAndLiabilitiesReq::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') - if ((o.limit_invoices || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_invoices')) && typeof o.limit_invoices !== 'number') return new Error(`${path}.limit_invoices: is not a number`) - if (opts.limit_invoices_CustomCheck && !opts.limit_invoices_CustomCheck(o.limit_invoices)) return new Error(`${path}.limit_invoices: custom check failed`) - - if ((o.limit_payments || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_payments')) && typeof o.limit_payments !== 'number') return new Error(`${path}.limit_payments: is not a number`) - if (opts.limit_payments_CustomCheck && !opts.limit_payments_CustomCheck(o.limit_payments)) return new Error(`${path}.limit_payments: custom check failed`) + if (!Array.isArray(o.liquidity_providers)) return new Error(`${path}.liquidity_providers: is not an array`) + for (let index = 0; index < o.liquidity_providers.length; index++) { + const liquidity_providersErr = LiquidityProviderFilterValidate(o.liquidity_providers[index], opts.liquidity_providers_ItemOptions, `${path}.liquidity_providers[${index}]`) + if (liquidity_providersErr !== null) return liquidity_providersErr + } + if (opts.liquidity_providers_CustomCheck && !opts.liquidity_providers_CustomCheck(o.liquidity_providers)) return new Error(`${path}.liquidity_providers: custom check failed`) - if ((o.limit_providers || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_providers')) && typeof o.limit_providers !== 'number') return new Error(`${path}.limit_providers: is not a number`) - if (opts.limit_providers_CustomCheck && !opts.limit_providers_CustomCheck(o.limit_providers)) return new Error(`${path}.limit_providers: custom check failed`) + if (!Array.isArray(o.lnd_providers)) return new Error(`${path}.lnd_providers: is not an array`) + for (let index = 0; index < o.lnd_providers.length; index++) { + const lnd_providersErr = LndProviderFilterValidate(o.lnd_providers[index], opts.lnd_providers_ItemOptions, `${path}.lnd_providers[${index}]`) + if (lnd_providersErr !== null) return lnd_providersErr + } + if (opts.lnd_providers_CustomCheck && !opts.lnd_providers_CustomCheck(o.lnd_providers)) return new Error(`${path}.lnd_providers: custom check failed`) return null } @@ -2543,6 +2547,46 @@ export const LiquidityAssetProviderValidate = (o?: LiquidityAssetProvider, opts: return null } +export type LiquidityProviderFilter = { + latestIncomingInvoice?: OperationsCursor + latestOutgoingInvoice?: OperationsCursor + limit?: number + pubkey: string +} +export type LiquidityProviderFilterOptionalField = 'latestIncomingInvoice' | 'latestOutgoingInvoice' | 'limit' +export const LiquidityProviderFilterOptionalFields: LiquidityProviderFilterOptionalField[] = ['latestIncomingInvoice', 'latestOutgoingInvoice', 'limit'] +export type LiquidityProviderFilterOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: LiquidityProviderFilterOptionalField[] + latestIncomingInvoice_Options?: OperationsCursorOptions + latestOutgoingInvoice_Options?: OperationsCursorOptions + limit_CustomCheck?: (v?: number) => boolean + pubkey_CustomCheck?: (v: string) => boolean +} +export const LiquidityProviderFilterValidate = (o?: LiquidityProviderFilter, opts: LiquidityProviderFilterOptions = {}, path: string = 'LiquidityProviderFilter::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.latestIncomingInvoice === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('latestIncomingInvoice')) { + const latestIncomingInvoiceErr = OperationsCursorValidate(o.latestIncomingInvoice, opts.latestIncomingInvoice_Options, `${path}.latestIncomingInvoice`) + if (latestIncomingInvoiceErr !== null) return latestIncomingInvoiceErr + } + + + if (typeof o.latestOutgoingInvoice === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('latestOutgoingInvoice')) { + const latestOutgoingInvoiceErr = OperationsCursorValidate(o.latestOutgoingInvoice, opts.latestOutgoingInvoice_Options, `${path}.latestOutgoingInvoice`) + if (latestOutgoingInvoiceErr !== null) return latestOutgoingInvoiceErr + } + + + if ((o.limit || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit')) && typeof o.limit !== 'number') return new Error(`${path}.limit: is not a number`) + if (opts.limit_CustomCheck && !opts.limit_CustomCheck(o.limit)) return new Error(`${path}.limit: custom check failed`) + + if (typeof o.pubkey !== 'string') return new Error(`${path}.pubkey: is not a string`) + if (opts.pubkey_CustomCheck && !opts.pubkey_CustomCheck(o.pubkey)) return new Error(`${path}.pubkey: custom check failed`) + + return null +} + export type LiveDebitRequest = { debit: LiveDebitRequest_debit k1?: string @@ -2947,6 +2991,45 @@ export const LndNodeMetricsValidate = (o?: LndNodeMetrics, opts: LndNodeMetricsO return null } +export type LndProviderFilter = { + limit_invoices?: number + limit_payments?: number + pubkey: string + skip_invoices?: number + skip_payments?: number +} +export type LndProviderFilterOptionalField = 'limit_invoices' | 'limit_payments' | 'skip_invoices' | 'skip_payments' +export const LndProviderFilterOptionalFields: LndProviderFilterOptionalField[] = ['limit_invoices', 'limit_payments', 'skip_invoices', 'skip_payments'] +export type LndProviderFilterOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: LndProviderFilterOptionalField[] + limit_invoices_CustomCheck?: (v?: number) => boolean + limit_payments_CustomCheck?: (v?: number) => boolean + pubkey_CustomCheck?: (v: string) => boolean + skip_invoices_CustomCheck?: (v?: number) => boolean + skip_payments_CustomCheck?: (v?: number) => boolean +} +export const LndProviderFilterValidate = (o?: LndProviderFilter, opts: LndProviderFilterOptions = {}, path: string = 'LndProviderFilter::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.limit_invoices || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_invoices')) && typeof o.limit_invoices !== 'number') return new Error(`${path}.limit_invoices: is not a number`) + if (opts.limit_invoices_CustomCheck && !opts.limit_invoices_CustomCheck(o.limit_invoices)) return new Error(`${path}.limit_invoices: custom check failed`) + + if ((o.limit_payments || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_payments')) && typeof o.limit_payments !== 'number') return new Error(`${path}.limit_payments: is not a number`) + if (opts.limit_payments_CustomCheck && !opts.limit_payments_CustomCheck(o.limit_payments)) return new Error(`${path}.limit_payments: custom check failed`) + + if (typeof o.pubkey !== 'string') return new Error(`${path}.pubkey: is not a string`) + if (opts.pubkey_CustomCheck && !opts.pubkey_CustomCheck(o.pubkey)) return new Error(`${path}.pubkey: custom check failed`) + + if ((o.skip_invoices || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('skip_invoices')) && typeof o.skip_invoices !== 'number') return new Error(`${path}.skip_invoices: is not a number`) + if (opts.skip_invoices_CustomCheck && !opts.skip_invoices_CustomCheck(o.skip_invoices)) return new Error(`${path}.skip_invoices: custom check failed`) + + if ((o.skip_payments || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('skip_payments')) && typeof o.skip_payments !== 'number') return new Error(`${path}.skip_payments: is not a number`) + if (opts.skip_payments_CustomCheck && !opts.skip_payments_CustomCheck(o.skip_payments)) return new Error(`${path}.skip_payments: custom check failed`) + + return null +} + export type LndSeed = { seed: string[] } @@ -4667,13 +4750,16 @@ export type TrackedOperation = { amount: number ts: number type: TrackedOperationType + user_id?: string } -export const TrackedOperationOptionalFields: [] = [] +export type TrackedOperationOptionalField = 'user_id' +export const TrackedOperationOptionalFields: TrackedOperationOptionalField[] = ['user_id'] export type TrackedOperationOptions = OptionsBaseMessage & { - checkOptionalsAreSet?: [] + checkOptionalsAreSet?: TrackedOperationOptionalField[] amount_CustomCheck?: (v: number) => boolean ts_CustomCheck?: (v: number) => boolean type_CustomCheck?: (v: TrackedOperationType) => boolean + user_id_CustomCheck?: (v?: string) => boolean } export const TrackedOperationValidate = (o?: TrackedOperation, opts: TrackedOperationOptions = {}, path: string = 'TrackedOperation::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') @@ -4688,6 +4774,9 @@ export const TrackedOperationValidate = (o?: TrackedOperation, opts: TrackedOper if (!enumCheckTrackedOperationType(o.type)) return new Error(`${path}.type: is not a valid TrackedOperationType`) if (opts.type_CustomCheck && !opts.type_CustomCheck(o.type)) return new Error(`${path}.type: custom check failed`) + if ((o.user_id || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('user_id')) && typeof o.user_id !== 'string') return new Error(`${path}.user_id: is not a string`) + if (opts.user_id_CustomCheck && !opts.user_id_CustomCheck(o.user_id)) return new Error(`${path}.user_id: custom check failed`) + return null } diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 33a4ebf1f..d306bad63 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -19,10 +19,24 @@ message EncryptionExchangeRequest { string deviceId = 2; } +message LndProviderFilter { + string pubkey = 1; + optional int64 limit_invoices = 2; + optional int64 skip_invoices = 3; + optional int64 limit_payments = 4; + optional int64 skip_payments = 5; +} + +message LiquidityProviderFilter { + string pubkey = 1; + optional OperationsCursor latestIncomingInvoice = 2; + optional OperationsCursor latestOutgoingInvoice = 3; + optional int64 limit = 4; +} + message AssetsAndLiabilitiesReq { - optional int64 limit_invoices = 1; - optional int64 limit_payments = 2; - optional int64 limit_providers = 4; + repeated LndProviderFilter lnd_providers = 1; + repeated LiquidityProviderFilter liquidity_providers = 2; } message BumpTx { @@ -41,6 +55,7 @@ message TrackedOperation { int64 ts = 1; int64 amount = 2; TrackedOperationType type = 3; + optional string user_id = 4; } message AssetOperation { diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 6e00b8b9e..18adb4736 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -630,20 +630,20 @@ export default class { return response } - async GetAllInvoices(max: number) { + async GetAllInvoices(max: number, startOffset = 0) { this.log(DEBUG, "Getting all paid invoices") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return { invoices: [] } } - const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) + const res = await this.lightning.listInvoices({ indexOffset: BigInt(startOffset), numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response } - async GetAllPayments(max: number) { + async GetAllPayments(max: number, startOffset = 0) { this.log(DEBUG, "Getting all payments") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return { payments: [] } } - const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }) + const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: BigInt(startOffset), maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }) return res.response } diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index dbde290dd..6c2ce08f4 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -406,7 +406,11 @@ export class AdminManager { if (this.liquidityProvider.GetProviderPubkey() !== provider.provider_pubkey) { return { pubkey: provider.provider_pubkey, tracked: undefined } } - const providerOps = await this.liquidityProvider.GetOperations(req.limit_providers || 100) + const filter = req.liquidity_providers.find(p => p.pubkey === provider.provider_pubkey) + const incoming = filter?.latestIncomingInvoice + const outgoing = filter?.latestOutgoingInvoice + const limit = filter?.limit + const providerOps = await this.liquidityProvider.GetOperations(incoming, outgoing, limit) // we only care about invoices cuz they are the only ops we can generate with a provider const invoices: Types.AssetOperation[] = [] const payments: Types.AssetOperation[] = [] @@ -435,7 +439,10 @@ export class AdminManager { const invoice = op.identifier const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(invoice) if (userInvoice) { - const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP } + const tracked: Types.TrackedOperation = { + ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, + type: USER_OP, user_id: userInvoice.user.user_id + } return { ts, amount, tracked } } const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice", invoice) @@ -452,7 +459,10 @@ export class AdminManager { const invoice = op.identifier const userInvoice = await this.storage.paymentStorage.GetPaymentOwner(invoice) if (userInvoice) { - const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP } + const tracked: Types.TrackedOperation = { + ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, + type: USER_OP, user_id: userInvoice.user.user_id + } return { ts, amount, tracked } } const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice_payment", invoice) @@ -469,7 +479,11 @@ export class AdminManager { return { pubkey: provider.provider_pubkey, tracked: undefined } } - const latestLndPayments = await this.lnd.GetAllPayments(req.limit_payments || 50) + const filter = req.lnd_providers.find(p => p.pubkey === provider.provider_pubkey) + const startOffset = filter?.skip_payments || 0 + const limit = filter?.limit_payments || 50 + + const latestLndPayments = await this.lnd.GetAllPayments(limit, startOffset) const payments: Types.AssetOperation[] = [] for (const payment of latestLndPayments.payments) { if (payment.status !== Payment_PaymentStatus.SUCCEEDED) { @@ -479,7 +493,7 @@ export class AdminManager { payments.push(assetOp) } const invoices: Types.AssetOperation[] = [] - const paidInvoices = await this.lnd.GetAllInvoices(req.limit_invoices || 100) + const paidInvoices = await this.lnd.GetAllInvoices(limit, startOffset) for (const invoiceEntry of paidInvoices.invoices) { if (invoiceEntry.state !== Invoice_InvoiceState.SETTLED) { continue @@ -526,7 +540,10 @@ export class AdminManager { const ts = Number(payment.creationTimeNs / (BigInt(1000_000_000))) const amount = Number(payment.valueSat) if (userInvoice) { - const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP } + const tracked: Types.TrackedOperation = { + ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, + type: USER_OP, user_id: userInvoice.user.user_id + } return { ts, amount, tracked } } const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice_payment", invoice) @@ -543,7 +560,10 @@ export class AdminManager { const amount = Number(invoiceEntry.amtPaidSat) const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(invoice) if (userInvoice) { - const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP } + const tracked: Types.TrackedOperation = { + ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, + type: USER_OP, user_id: userInvoice.user.user_id + } return { ts, amount, tracked } } const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice", invoice) @@ -560,7 +580,10 @@ export class AdminManager { const userOp = await this.storage.paymentStorage.GetTxHashPaymentOwner(tx.txHash) if (userOp) { // user transaction payments are actually deprecated from lnd, but we keep this for consstency - const tracked: Types.TrackedOperation = { ts: userOp.paid_at_unix, amount: userOp.paid_amount, type: USER_OP } + const tracked: Types.TrackedOperation = { + ts: userOp.paid_at_unix, amount: userOp.paid_amount, + type: USER_OP, user_id: userOp.user.user_id + } return { ts, amount, tracked } } const rootOp = await this.storage.metricsStorage.GetRootOperation("chain_payment", tx.txHash) @@ -577,7 +600,10 @@ export class AdminManager { const outputIndex = Number(output.outputIndex) const userOp = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner(output.address, tx.txHash) if (userOp) { - const tracked: Types.TrackedOperation = { ts: userOp.paid_at_unix, amount: userOp.paid_amount, type: USER_OP } + const tracked: Types.TrackedOperation = { + ts: userOp.paid_at_unix, amount: userOp.paid_amount, + type: USER_OP, user_id: userOp.user_address.user.user_id + } return { ts, amount, tracked } } const rootOp = await this.storage.metricsStorage.GetRootAddressTransaction(output.address, tx.txHash, outputIndex) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index f10d63bf2..02697fa53 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -317,14 +317,17 @@ export class LiquidityProvider { return res } - GetOperations = async (max = 200) => { + GetOperations = async (incoming: Types.OperationsCursor | undefined, outgoing: Types.OperationsCursor | undefined, limit: number | undefined) => { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") } + const latestIncomingInvoice = incoming || { ts: 0, id: 0 } + const latestOutgoingInvoice = outgoing || { ts: 0, id: 0 } + const maxSize = limit || 200 const res = await this.client.GetUserOperations({ - latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, + latestIncomingInvoice, latestOutgoingInvoice, latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { ts: 0, id: 0 }, - latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: max + latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: maxSize }) if (res.status === 'ERROR') { this.log("error getting operations", res.reason) From 22889d1bc4439b62fcc187a0a8b6e904bf32dd92 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 1 Jul 2026 16:31:59 +0000 Subject: [PATCH 02/22] users info --- proto/autogenerated/client.md | 47 ++++++ proto/autogenerated/go/http_client.go | 60 ++++++++ proto/autogenerated/go/types.go | 23 +++ proto/autogenerated/ts/express_server.ts | 44 ++++++ proto/autogenerated/ts/http_client.ts | 28 ++++ proto/autogenerated/ts/nostr_client.ts | 30 ++++ proto/autogenerated/ts/nostr_transport.ts | 32 ++++ proto/autogenerated/ts/types.ts | 161 +++++++++++++++++++- proto/service/methods.proto | 46 ++++-- proto/service/structs.proto | 27 ++++ src/services/main/adminManager.ts | 10 +- src/services/main/appUserManager.ts | 28 ++++ src/services/main/paymentManager.ts | 7 +- src/services/serverMethods/index.ts | 8 + src/services/storage/db/storageInterface.ts | 8 +- src/services/storage/db/storageProcessor.ts | 26 +++- src/services/storage/userStorage.ts | 5 + 17 files changed, 562 insertions(+), 28 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index bc72fdf4f..d4a7ef794 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -253,6 +253,16 @@ The nostr server will send back a message response, and inside the body there wi - input: [GetUserOperationsRequest](#GetUserOperationsRequest) - output: [GetUserOperationsResponse](#GetUserOperationsResponse) +- GetUserOperationsFromAdmin + - auth type: __Admin__ + - input: [GetUserOperationsRequest](#GetUserOperationsRequest) + - output: [GetUserOperationsResponse](#GetUserOperationsResponse) + +- GetUsersAdminInfo + - auth type: __Admin__ + - input: [UsersAdminInfoRequest](#UsersAdminInfoRequest) + - output: [UsersAdminInfo](#UsersAdminInfo) + - LinkNPubThroughToken - auth type: __GuestWithPub__ - input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest) @@ -840,6 +850,20 @@ The nostr server will send back a message response, and inside the body there wi - input: [GetUserOperationsRequest](#GetUserOperationsRequest) - output: [GetUserOperationsResponse](#GetUserOperationsResponse) +- GetUserOperationsFromAdmin + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/user/operations__ + - input: [GetUserOperationsRequest](#GetUserOperationsRequest) + - output: [GetUserOperationsResponse](#GetUserOperationsResponse) + +- GetUsersAdminInfo + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/users/info__ + - input: [UsersAdminInfoRequest](#UsersAdminInfoRequest) + - output: [UsersAdminInfo](#UsersAdminInfo) + - HandleLnurlAddress - auth type: __Guest__ - http method: __get__ @@ -1196,6 +1220,12 @@ The nostr server will send back a message response, and inside the body there wi - __info__: _[UserInfo](#UserInfo)_ - __max_withdrawable__: _number_ +### AppUserAdminInfo + - __app_user_id__: _string_ + - __has_callback_url__: _boolean_ + - __has_topic_id__: _boolean_ + - __npub__: _string_ + ### Application - __balance__: _number_ - __id__: _string_ @@ -1404,6 +1434,7 @@ The nostr server will send back a message response, and inside the body there wi - __latestOutgoingTx__: _[OperationsCursor](#OperationsCursor)_ - __latestOutgoingUserToUserPayment__: _[OperationsCursor](#OperationsCursor)_ - __max_size__: _number_ + - __user_id__: _string_ *this field is optional ### GetUserOperationsResponse - __latestIncomingInvoiceOperations__: _[UserOperations](#UserOperations)_ @@ -1412,6 +1443,7 @@ The nostr server will send back a message response, and inside the body there wi - __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_ - __latestOutgoingTxOperations__: _[UserOperations](#UserOperations)_ - __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_ + - __user_id__: _string_ *this field is optional ### GraphPoint - __x__: _number_ @@ -1901,6 +1933,13 @@ The nostr server will send back a message response, and inside the body there wi ### UseInviteLinkRequest - __invite_token__: _string_ +### UserAdminInfo + - __app_users__: ARRAY of: _[AppUserAdminInfo](#AppUserAdminInfo)_ + - __balance__: _number_ + - __locked__: _boolean_ + - __owner_of_app_id__: _string_ *this field is optional + - __user_id__: _string_ + ### UserHealthState - __downtime_reason__: _string_ @@ -1940,6 +1979,14 @@ The nostr server will send back a message response, and inside the body there wi - __operations__: ARRAY of: _[UserOperation](#UserOperation)_ - __toIndex__: _[OperationsCursor](#OperationsCursor)_ +### UsersAdminInfo + - __total__: _number_ + - __users__: ARRAY of: _[UserAdminInfo](#UserAdminInfo)_ + +### UsersAdminInfoRequest + - __skip__: _number_ *this field is optional + - __take__: _number_ *this field is optional + ### UsersInfo - __always_been_inactive__: _number_ - __balance_avg__: _number_ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index ca3ccfe92..e0794f738 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -112,6 +112,8 @@ type Client struct { GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error) GetUserOffers func() (*UserOffers, error) GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) + GetUserOperationsFromAdmin func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) + GetUsersAdminInfo func(req UsersAdminInfoRequest) (*UsersAdminInfo, error) HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error @@ -1600,6 +1602,64 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + GetUserOperationsFromAdmin: func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/user/operations" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := GetUserOperationsResponse{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, + GetUsersAdminInfo: func(req UsersAdminInfoRequest) (*UsersAdminInfo, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/users/info" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := UsersAdminInfo{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, HandleLnurlAddress: func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) { auth, err := params.RetrieveGuestAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 391d70ccd..c111a3d0f 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -156,6 +156,12 @@ type AppUser struct { Info *UserInfo `json:"info"` Max_withdrawable int64 `json:"max_withdrawable"` } +type AppUserAdminInfo struct { + App_user_id string `json:"app_user_id"` + Has_callback_url bool `json:"has_callback_url"` + Has_topic_id bool `json:"has_topic_id"` + Npub string `json:"npub"` +} type Application struct { Balance int64 `json:"balance"` Id string `json:"id"` @@ -364,6 +370,7 @@ type GetUserOperationsRequest struct { Latestoutgoingtx *OperationsCursor `json:"latestOutgoingTx"` Latestoutgoingusertouserpayment *OperationsCursor `json:"latestOutgoingUserToUserPayment"` Max_size int64 `json:"max_size"` + User_id string `json:"user_id"` } type GetUserOperationsResponse struct { Latestincominginvoiceoperations *UserOperations `json:"latestIncomingInvoiceOperations"` @@ -372,6 +379,7 @@ type GetUserOperationsResponse struct { Latestoutgoinginvoiceoperations *UserOperations `json:"latestOutgoingInvoiceOperations"` Latestoutgoingtxoperations *UserOperations `json:"latestOutgoingTxOperations"` Latestoutgoingusertouserpayemnts *UserOperations `json:"latestOutgoingUserToUserPayemnts"` + User_id string `json:"user_id"` } type GraphPoint struct { X int64 `json:"x"` @@ -861,6 +869,13 @@ type UsageMetrics struct { type UseInviteLinkRequest struct { Invite_token string `json:"invite_token"` } +type UserAdminInfo struct { + App_users []AppUserAdminInfo `json:"app_users"` + Balance int64 `json:"balance"` + Locked bool `json:"locked"` + Owner_of_app_id string `json:"owner_of_app_id"` + User_id string `json:"user_id"` +} type UserHealthState struct { Downtime_reason string `json:"downtime_reason"` } @@ -900,6 +915,14 @@ type UserOperations struct { Operations []UserOperation `json:"operations"` Toindex *OperationsCursor `json:"toIndex"` } +type UsersAdminInfo struct { + Total int64 `json:"total"` + Users []UserAdminInfo `json:"users"` +} +type UsersAdminInfoRequest struct { + Skip int64 `json:"skip"` + Take int64 `json:"take"` +} type UsersInfo struct { Always_been_inactive int64 `json:"always_been_inactive"` Balance_avg int64 `json:"balance_avg"` diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index 6f04506ad..c14292c6a 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -1575,6 +1575,50 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.GetUserOperationsFromAdmin) throw new Error('method: GetUserOperationsFromAdmin is not implemented') + app.post('/api/admin/user/operations', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetUserOperationsFromAdmin', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetUserOperationsFromAdmin) throw new Error('method: GetUserOperationsFromAdmin is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.GetUserOperationsRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.GetUserOperationsFromAdmin({rpcName:'GetUserOperationsFromAdmin', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) + if (!opts.allowNotImplementedMethods && !methods.GetUsersAdminInfo) throw new Error('method: GetUsersAdminInfo is not implemented') + app.post('/api/admin/users/info', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetUsersAdminInfo', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetUsersAdminInfo) throw new Error('method: GetUsersAdminInfo is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.UsersAdminInfoRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.GetUsersAdminInfo({rpcName:'GetUsersAdminInfo', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.HandleLnurlAddress) throw new Error('method: HandleLnurlAddress is not implemented') app.get('/.well-known/lnurlp/:address_name', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'HandleLnurlAddress', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 1e96081d9..7d4696da6 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -754,6 +754,34 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetUserOperationsFromAdmin: async (request: Types.GetUserOperationsRequest): Promise => { + const auth = await params.retrieveAdminAuth() + if (auth === null) throw new Error('retrieveAdminAuth() returned null') + let finalRoute = '/api/admin/user/operations' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.GetUserOperationsResponseValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, + GetUsersAdminInfo: async (request: Types.UsersAdminInfoRequest): Promise => { + const auth = await params.retrieveAdminAuth() + if (auth === null) throw new Error('retrieveAdminAuth() returned null') + let finalRoute = '/api/admin/users/info' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.UsersAdminInfoValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, HandleLnurlAddress: async (routeParams: Types.HandleLnurlAddress_RouteParams): Promise => { const auth = await params.retrieveGuestAuth() if (auth === null) throw new Error('retrieveGuestAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 8a07f5bf3..d08c58f57 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -696,6 +696,36 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetUserOperationsFromAdmin: async (request: Types.GetUserOperationsRequest): Promise => { + const auth = await params.retrieveNostrAdminAuth() + if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + const data = await send(params.pubDestination, {rpcName:'GetUserOperationsFromAdmin',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.GetUserOperationsResponseValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, + GetUsersAdminInfo: async (request: Types.UsersAdminInfoRequest): Promise => { + const auth = await params.retrieveNostrAdminAuth() + if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + const data = await send(params.pubDestination, {rpcName:'GetUsersAdminInfo',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.UsersAdminInfoValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, LinkNPubThroughToken: async (request: Types.LinkNPubThroughTokenRequest): Promise => { const auth = await params.retrieveNostrGuestWithPubAuth() if (auth === null) throw new Error('retrieveNostrGuestWithPubAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index c92fc4239..905c51c59 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -1154,6 +1154,38 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'GetUserOperationsFromAdmin': + try { + if (!methods.GetUserOperationsFromAdmin) throw new Error('method: GetUserOperationsFromAdmin is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.GetUserOperationsRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.GetUserOperationsFromAdmin({rpcName:'GetUserOperationsFromAdmin', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break + case 'GetUsersAdminInfo': + try { + if (!methods.GetUsersAdminInfo) throw new Error('method: GetUsersAdminInfo is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.UsersAdminInfoRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.GetUsersAdminInfo({rpcName:'GetUsersAdminInfo', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'LinkNPubThroughToken': try { if (!methods.LinkNPubThroughToken) throw new Error('method: LinkNPubThroughToken is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 839bdec28..91920829d 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: export type AdminContext = { admin_id: string } -export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | BumpTx_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminInvoiceSwapQuotes_Input | GetAdminTransactionSwapQuotes_Input | GetAssetsAndLiabilities_Input | GetInviteLinkState_Input | GetSeed_Input | ListAdminInvoiceSwaps_Input | ListAdminTxSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminInvoiceSwap_Input | PayAdminTransactionSwap_Input | RefundAdminInvoiceSwap_Input | UpdateChannelPolicy_Input -export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | BumpTx_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminInvoiceSwapQuotes_Output | GetAdminTransactionSwapQuotes_Output | GetAssetsAndLiabilities_Output | GetInviteLinkState_Output | GetSeed_Output | ListAdminInvoiceSwaps_Output | ListAdminTxSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminInvoiceSwap_Output | PayAdminTransactionSwap_Output | RefundAdminInvoiceSwap_Output | UpdateChannelPolicy_Output +export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | BumpTx_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminInvoiceSwapQuotes_Input | GetAdminTransactionSwapQuotes_Input | GetAssetsAndLiabilities_Input | GetInviteLinkState_Input | GetSeed_Input | GetUserOperationsFromAdmin_Input | GetUsersAdminInfo_Input | ListAdminInvoiceSwaps_Input | ListAdminTxSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminInvoiceSwap_Input | PayAdminTransactionSwap_Input | RefundAdminInvoiceSwap_Input | UpdateChannelPolicy_Input +export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | BumpTx_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminInvoiceSwapQuotes_Output | GetAdminTransactionSwapQuotes_Output | GetAssetsAndLiabilities_Output | GetInviteLinkState_Output | GetSeed_Output | GetUserOperationsFromAdmin_Output | GetUsersAdminInfo_Output | ListAdminInvoiceSwaps_Output | ListAdminTxSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminInvoiceSwap_Output | PayAdminTransactionSwap_Output | RefundAdminInvoiceSwap_Output | UpdateChannelPolicy_Output export type AppContext = { app_id: string } @@ -219,6 +219,12 @@ export type GetUserOffers_Output = ResultError | ({ status: 'OK' } & UserOffers) export type GetUserOperations_Input = {rpcName:'GetUserOperations', req: GetUserOperationsRequest} export type GetUserOperations_Output = ResultError | ({ status: 'OK' } & GetUserOperationsResponse) +export type GetUserOperationsFromAdmin_Input = {rpcName:'GetUserOperationsFromAdmin', req: GetUserOperationsRequest} +export type GetUserOperationsFromAdmin_Output = ResultError | ({ status: 'OK' } & GetUserOperationsResponse) + +export type GetUsersAdminInfo_Input = {rpcName:'GetUsersAdminInfo', req: UsersAdminInfoRequest} +export type GetUsersAdminInfo_Output = ResultError | ({ status: 'OK' } & UsersAdminInfo) + export type HandleLnurlAddress_RouteParams = { address_name: string } @@ -413,6 +419,8 @@ export type ServerMethods = { GetUserOfferInvoices?: (req: GetUserOfferInvoices_Input & {ctx: UserContext }) => Promise GetUserOffers?: (req: GetUserOffers_Input & {ctx: UserContext }) => Promise GetUserOperations?: (req: GetUserOperations_Input & {ctx: UserContext }) => Promise + GetUserOperationsFromAdmin?: (req: GetUserOperationsFromAdmin_Input & {ctx: AdminContext }) => Promise + GetUsersAdminInfo?: (req: GetUsersAdminInfo_Input & {ctx: AdminContext }) => Promise HandleLnurlAddress?: (req: HandleLnurlAddress_Input & {ctx: GuestContext }) => Promise HandleLnurlPay?: (req: HandleLnurlPay_Input & {ctx: GuestContext }) => Promise HandleLnurlWithdraw?: (req: HandleLnurlWithdraw_Input & {ctx: GuestContext }) => Promise @@ -860,6 +868,39 @@ export const AppUserValidate = (o?: AppUser, opts: AppUserOptions = {}, path: st return null } +export type AppUserAdminInfo = { + app_user_id: string + has_callback_url: boolean + has_topic_id: boolean + npub: string +} +export const AppUserAdminInfoOptionalFields: [] = [] +export type AppUserAdminInfoOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + app_user_id_CustomCheck?: (v: string) => boolean + has_callback_url_CustomCheck?: (v: boolean) => boolean + has_topic_id_CustomCheck?: (v: boolean) => boolean + npub_CustomCheck?: (v: string) => boolean +} +export const AppUserAdminInfoValidate = (o?: AppUserAdminInfo, opts: AppUserAdminInfoOptions = {}, path: string = 'AppUserAdminInfo::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.app_user_id !== 'string') return new Error(`${path}.app_user_id: is not a string`) + if (opts.app_user_id_CustomCheck && !opts.app_user_id_CustomCheck(o.app_user_id)) return new Error(`${path}.app_user_id: custom check failed`) + + if (typeof o.has_callback_url !== 'boolean') return new Error(`${path}.has_callback_url: is not a boolean`) + if (opts.has_callback_url_CustomCheck && !opts.has_callback_url_CustomCheck(o.has_callback_url)) return new Error(`${path}.has_callback_url: custom check failed`) + + if (typeof o.has_topic_id !== 'boolean') return new Error(`${path}.has_topic_id: is not a boolean`) + if (opts.has_topic_id_CustomCheck && !opts.has_topic_id_CustomCheck(o.has_topic_id)) return new Error(`${path}.has_topic_id: custom check failed`) + + if (typeof o.npub !== 'string') return new Error(`${path}.npub: is not a string`) + if (opts.npub_CustomCheck && !opts.npub_CustomCheck(o.npub)) return new Error(`${path}.npub: custom check failed`) + + return null +} + export type Application = { balance: number id: string @@ -2100,10 +2141,12 @@ export type GetUserOperationsRequest = { latestOutgoingTx: OperationsCursor latestOutgoingUserToUserPayment: OperationsCursor max_size: number + user_id?: string } -export const GetUserOperationsRequestOptionalFields: [] = [] +export type GetUserOperationsRequestOptionalField = 'user_id' +export const GetUserOperationsRequestOptionalFields: GetUserOperationsRequestOptionalField[] = ['user_id'] export type GetUserOperationsRequestOptions = OptionsBaseMessage & { - checkOptionalsAreSet?: [] + checkOptionalsAreSet?: GetUserOperationsRequestOptionalField[] latestIncomingInvoice_Options?: OperationsCursorOptions latestIncomingTx_Options?: OperationsCursorOptions latestIncomingUserToUserPayment_Options?: OperationsCursorOptions @@ -2111,6 +2154,7 @@ export type GetUserOperationsRequestOptions = OptionsBaseMessage & { latestOutgoingTx_Options?: OperationsCursorOptions latestOutgoingUserToUserPayment_Options?: OperationsCursorOptions max_size_CustomCheck?: (v: number) => boolean + user_id_CustomCheck?: (v?: string) => boolean } export const GetUserOperationsRequestValidate = (o?: GetUserOperationsRequest, opts: GetUserOperationsRequestOptions = {}, path: string = 'GetUserOperationsRequest::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') @@ -2143,6 +2187,9 @@ export const GetUserOperationsRequestValidate = (o?: GetUserOperationsRequest, o if (typeof o.max_size !== 'number') return new Error(`${path}.max_size: is not a number`) if (opts.max_size_CustomCheck && !opts.max_size_CustomCheck(o.max_size)) return new Error(`${path}.max_size: custom check failed`) + if ((o.user_id || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('user_id')) && typeof o.user_id !== 'string') return new Error(`${path}.user_id: is not a string`) + if (opts.user_id_CustomCheck && !opts.user_id_CustomCheck(o.user_id)) return new Error(`${path}.user_id: custom check failed`) + return null } @@ -2153,16 +2200,19 @@ export type GetUserOperationsResponse = { latestOutgoingInvoiceOperations: UserOperations latestOutgoingTxOperations: UserOperations latestOutgoingUserToUserPayemnts: UserOperations + user_id?: string } -export const GetUserOperationsResponseOptionalFields: [] = [] +export type GetUserOperationsResponseOptionalField = 'user_id' +export const GetUserOperationsResponseOptionalFields: GetUserOperationsResponseOptionalField[] = ['user_id'] export type GetUserOperationsResponseOptions = OptionsBaseMessage & { - checkOptionalsAreSet?: [] + checkOptionalsAreSet?: GetUserOperationsResponseOptionalField[] latestIncomingInvoiceOperations_Options?: UserOperationsOptions latestIncomingTxOperations_Options?: UserOperationsOptions latestIncomingUserToUserPayemnts_Options?: UserOperationsOptions latestOutgoingInvoiceOperations_Options?: UserOperationsOptions latestOutgoingTxOperations_Options?: UserOperationsOptions latestOutgoingUserToUserPayemnts_Options?: UserOperationsOptions + user_id_CustomCheck?: (v?: string) => boolean } export const GetUserOperationsResponseValidate = (o?: GetUserOperationsResponse, opts: GetUserOperationsResponseOptions = {}, path: string = 'GetUserOperationsResponse::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') @@ -2192,6 +2242,9 @@ export const GetUserOperationsResponseValidate = (o?: GetUserOperationsResponse, if (latestOutgoingUserToUserPayemntsErr !== null) return latestOutgoingUserToUserPayemntsErr + if ((o.user_id || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('user_id')) && typeof o.user_id !== 'string') return new Error(`${path}.user_id: is not a string`) + if (opts.user_id_CustomCheck && !opts.user_id_CustomCheck(o.user_id)) return new Error(`${path}.user_id: custom check failed`) + return null } @@ -5118,6 +5171,50 @@ export const UseInviteLinkRequestValidate = (o?: UseInviteLinkRequest, opts: Use return null } +export type UserAdminInfo = { + app_users: AppUserAdminInfo[] + balance: number + locked: boolean + owner_of_app_id?: string + user_id: string +} +export type UserAdminInfoOptionalField = 'owner_of_app_id' +export const UserAdminInfoOptionalFields: UserAdminInfoOptionalField[] = ['owner_of_app_id'] +export type UserAdminInfoOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: UserAdminInfoOptionalField[] + app_users_ItemOptions?: AppUserAdminInfoOptions + app_users_CustomCheck?: (v: AppUserAdminInfo[]) => boolean + balance_CustomCheck?: (v: number) => boolean + locked_CustomCheck?: (v: boolean) => boolean + owner_of_app_id_CustomCheck?: (v?: string) => boolean + user_id_CustomCheck?: (v: string) => boolean +} +export const UserAdminInfoValidate = (o?: UserAdminInfo, opts: UserAdminInfoOptions = {}, path: string = 'UserAdminInfo::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (!Array.isArray(o.app_users)) return new Error(`${path}.app_users: is not an array`) + for (let index = 0; index < o.app_users.length; index++) { + const app_usersErr = AppUserAdminInfoValidate(o.app_users[index], opts.app_users_ItemOptions, `${path}.app_users[${index}]`) + if (app_usersErr !== null) return app_usersErr + } + if (opts.app_users_CustomCheck && !opts.app_users_CustomCheck(o.app_users)) return new Error(`${path}.app_users: custom check failed`) + + if (typeof o.balance !== 'number') return new Error(`${path}.balance: is not a number`) + if (opts.balance_CustomCheck && !opts.balance_CustomCheck(o.balance)) return new Error(`${path}.balance: custom check failed`) + + if (typeof o.locked !== 'boolean') return new Error(`${path}.locked: is not a boolean`) + if (opts.locked_CustomCheck && !opts.locked_CustomCheck(o.locked)) return new Error(`${path}.locked: custom check failed`) + + if ((o.owner_of_app_id || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('owner_of_app_id')) && typeof o.owner_of_app_id !== 'string') return new Error(`${path}.owner_of_app_id: is not a string`) + if (opts.owner_of_app_id_CustomCheck && !opts.owner_of_app_id_CustomCheck(o.owner_of_app_id)) return new Error(`${path}.owner_of_app_id: custom check failed`) + + if (typeof o.user_id !== 'string') return new Error(`${path}.user_id: is not a string`) + if (opts.user_id_CustomCheck && !opts.user_id_CustomCheck(o.user_id)) return new Error(`${path}.user_id: custom check failed`) + + return null +} + export type UserHealthState = { downtime_reason: string } @@ -5340,6 +5437,58 @@ export const UserOperationsValidate = (o?: UserOperations, opts: UserOperationsO return null } +export type UsersAdminInfo = { + total: number + users: UserAdminInfo[] +} +export const UsersAdminInfoOptionalFields: [] = [] +export type UsersAdminInfoOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + total_CustomCheck?: (v: number) => boolean + users_ItemOptions?: UserAdminInfoOptions + users_CustomCheck?: (v: UserAdminInfo[]) => boolean +} +export const UsersAdminInfoValidate = (o?: UsersAdminInfo, opts: UsersAdminInfoOptions = {}, path: string = 'UsersAdminInfo::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.total !== 'number') return new Error(`${path}.total: is not a number`) + if (opts.total_CustomCheck && !opts.total_CustomCheck(o.total)) return new Error(`${path}.total: custom check failed`) + + if (!Array.isArray(o.users)) return new Error(`${path}.users: is not an array`) + for (let index = 0; index < o.users.length; index++) { + const usersErr = UserAdminInfoValidate(o.users[index], opts.users_ItemOptions, `${path}.users[${index}]`) + if (usersErr !== null) return usersErr + } + if (opts.users_CustomCheck && !opts.users_CustomCheck(o.users)) return new Error(`${path}.users: custom check failed`) + + return null +} + +export type UsersAdminInfoRequest = { + skip?: number + take?: number +} +export type UsersAdminInfoRequestOptionalField = 'skip' | 'take' +export const UsersAdminInfoRequestOptionalFields: UsersAdminInfoRequestOptionalField[] = ['skip', 'take'] +export type UsersAdminInfoRequestOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: UsersAdminInfoRequestOptionalField[] + skip_CustomCheck?: (v?: number) => boolean + take_CustomCheck?: (v?: number) => boolean +} +export const UsersAdminInfoRequestValidate = (o?: UsersAdminInfoRequest, opts: UsersAdminInfoRequestOptions = {}, path: string = 'UsersAdminInfoRequest::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.skip || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('skip')) && typeof o.skip !== 'number') return new Error(`${path}.skip: is not a number`) + if (opts.skip_CustomCheck && !opts.skip_CustomCheck(o.skip)) return new Error(`${path}.skip: custom check failed`) + + if ((o.take || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('take')) && typeof o.take !== 'number') return new Error(`${path}.take: is not a number`) + if (opts.take_CustomCheck && !opts.take_CustomCheck(o.take)) return new Error(`${path}.take: custom check failed`) + + return null +} + export type UsersInfo = { always_been_inactive: number balance_avg: number diff --git a/proto/service/methods.proto b/proto/service/methods.proto index cfdd7b347..646aaacdb 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -238,6 +238,37 @@ service LightningPub { option (nostr) = true; } + rpc CreateOneTimeInviteLink(structs.CreateOneTimeInviteLinkRequest) returns (structs.CreateOneTimeInviteLinkResponse) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/app/invite/create"; + option (nostr) = true; + } + + rpc GetInviteLinkState(structs.GetInviteTokenStateRequest) returns (structs.GetInviteTokenStateResponse) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/app/invite/get"; + option (nostr) = true; + } + + rpc GetUsersAdminInfo(structs.UsersAdminInfoRequest) returns (structs.UsersAdminInfo) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/users/info"; + option (nostr) = true; + } + + rpc GetUserOperationsFromAdmin(structs.GetUserOperationsRequest) returns (structs.GetUserOperationsResponse) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/user/operations"; + option (nostr) = true; + } + + // + // + rpc GetUsageMetrics(structs.LatestUsageMetricReq) returns (structs.UsageMetrics) { option (auth_type) = "Metrics"; option (http_method) = "post"; @@ -333,22 +364,11 @@ service LightningPub { option (nostr) = true; } - rpc CreateOneTimeInviteLink(structs.CreateOneTimeInviteLinkRequest) returns (structs.CreateOneTimeInviteLinkResponse) { - option (auth_type) = "Admin"; - option (http_method) = "post"; - option (http_route) = "/api/admin/app/invite/create"; - option (nostr) = true; - } - rpc GetInviteLinkState(structs.GetInviteTokenStateRequest) returns (structs.GetInviteTokenStateResponse) { - option (auth_type) = "Admin"; - option (http_method) = "post"; - option (http_route) = "/api/admin/app/invite/get"; - option (nostr) = true; - } - // + // + // rpc Health(structs.Empty) returns (structs.Empty){ diff --git a/proto/service/structs.proto b/proto/service/structs.proto index d306bad63..f00631aa9 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -45,6 +45,31 @@ message BumpTx { int64 sat_per_vbyte = 3; } +message AppUserAdminInfo { + string app_user_id = 1; + string npub = 2; + bool has_callback_url = 3; + bool has_topic_id = 4; +} + +message UserAdminInfo { + string user_id = 1; + int64 balance = 2; + bool locked = 3; + repeated AppUserAdminInfo app_users = 4; + optional string owner_of_app_id = 5; +} + +message UsersAdminInfoRequest { + optional int64 skip = 1; + optional int64 take = 2; +} + +message UsersAdminInfo { + repeated UserAdminInfo users = 1; + int64 total = 2; +} + enum TrackedOperationType { USER = 0; @@ -636,6 +661,7 @@ message GetUserOperationsRequest{ OperationsCursor latestIncomingUserToUserPayment = 5; OperationsCursor latestOutgoingUserToUserPayment = 6; int64 max_size = 7; + optional string user_id = 8; } enum UserOperationType { INCOMING_TX =0; @@ -671,6 +697,7 @@ message GetUserOperationsResponse{ UserOperations latestIncomingTxOperations=4; UserOperations latestOutgoingUserToUserPayemnts=5; UserOperations latestIncomingUserToUserPayemnts=6; + optional string user_id = 7; } message AddProductRequest { diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 6c2ce08f4..0a5a92f4d 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -480,10 +480,10 @@ export class AdminManager { } const filter = req.lnd_providers.find(p => p.pubkey === provider.provider_pubkey) - const startOffset = filter?.skip_payments || 0 - const limit = filter?.limit_payments || 50 + const paymentsStart = filter?.skip_payments || 0 + const paymentsLimit = filter?.limit_payments || 50 - const latestLndPayments = await this.lnd.GetAllPayments(limit, startOffset) + const latestLndPayments = await this.lnd.GetAllPayments(paymentsLimit, paymentsStart) const payments: Types.AssetOperation[] = [] for (const payment of latestLndPayments.payments) { if (payment.status !== Payment_PaymentStatus.SUCCEEDED) { @@ -493,7 +493,9 @@ export class AdminManager { payments.push(assetOp) } const invoices: Types.AssetOperation[] = [] - const paidInvoices = await this.lnd.GetAllInvoices(limit, startOffset) + const invoicesStart = filter?.skip_invoices || 0 + const invoicesLimit = filter?.limit_invoices || 50 + const paidInvoices = await this.lnd.GetAllInvoices(invoicesLimit, invoicesStart) for (const invoiceEntry of paidInvoices.invoices) { if (invoiceEntry.state !== Invoice_InvoiceState.SETTLED) { continue diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 4a7848fe1..56576697c 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -120,6 +120,34 @@ export default class { await this.storage.applicationStorage.UpdateAppUserMessagingToken(user.identifier, req.device_id, req.firebase_messaging_token); } + async GetUsersAdminInfo(req: Types.UsersAdminInfoRequest): Promise { + const { users, total } = await this.storage.userStorage.GetUsers(req) + const applications = await this.storage.applicationStorage.GetApplications() + + const apps: Record = {} + applications.forEach(a => apps[a.owner.user_id] = a.app_id) + + const usersInfo: Types.UserAdminInfo[] = [] + for (const user of users) { + const appUsers = await this.storage.applicationStorage.GetAllAppUsersFromUser(user.user_id) + const appUsersInfo: Types.AppUserAdminInfo[] = appUsers.map(a => ({ + app_user_id: a.identifier, + npub: a.nostr_public_key || "", + has_callback_url: a.callback_url !== "", + has_topic_id: a.topic_id !== "", + })) + usersInfo.push({ + user_id: user.user_id, + balance: user.balance_sats, + locked: user.locked, + app_users: appUsersInfo, + owner_of_app_id: apps[user.user_id] + }) + } + + return { users: usersInfo, total } + } + async CleanupInactiveUsers() { this.log("Cleaning up inactive users") const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(365) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 4d3b6f7ca..149a7066b 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -1074,9 +1074,9 @@ export default class { } } - async GetUserOperations(userId: string, req: Types.GetUserOperationsRequest): Promise { + async GetUserOperations(userId: string, req: Types.GetUserOperationsRequest, admin: boolean = false): Promise { const user = await this.storage.userStorage.GetUser(userId) - if (user.locked) { + if (user.locked && !admin) { throw new Error("user is banned, cannot retrieve operations") } const [outgoingInvoices, outgoingTransactions, incomingInvoices, incomingTransactions, incomingUserToUser, outgoingUserToUser] = await Promise.all([ @@ -1093,7 +1093,8 @@ export default class { latestOutgoingInvoiceOperations: this.mapOperations(outgoingInvoices, Types.UserOperationType.OUTGOING_INVOICE, false), latestOutgoingTxOperations: this.mapOperations(outgoingTransactions, Types.UserOperationType.OUTGOING_TX, false), latestIncomingUserToUserPayemnts: this.mapOperations(incomingUserToUser, Types.UserOperationType.INCOMING_USER_TO_USER, true), - latestOutgoingUserToUserPayemnts: this.mapOperations(outgoingUserToUser, Types.UserOperationType.OUTGOING_USER_TO_USER, false) + latestOutgoingUserToUserPayemnts: this.mapOperations(outgoingUserToUser, Types.UserOperationType.OUTGOING_USER_TO_USER, false), + user_id: userId } } diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 16fae73e6..9f4ee031a 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -162,6 +162,13 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.appUserManager.BanUser(req.user_id) }, + GetUsersAdminInfo: async ({ ctx, req }) => { + return mainHandler.appUserManager.GetUsersAdminInfo(req) + }, + GetUserOperationsFromAdmin: async ({ req }) => { + if (!req.user_id) throw new Error("user_id is required") + return mainHandler.paymentManager.GetUserOperations(req.user_id, req, true) + }, GetSeed: async ({ ctx }) => { return mainHandler.unlocker.GetSeed() }, @@ -181,6 +188,7 @@ export default (mainHandler: Main): Types.ServerMethods => { return mainHandler.appUserManager.UpdateCallbackUrl(ctx, req) }, GetUserOperations: async ({ ctx, req }) => { + if (req.user_id) delete req.user_id return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req) }, diff --git a/src/services/storage/db/storageInterface.ts b/src/services/storage/db/storageInterface.ts index 976ec20d6..b901455e6 100644 --- a/src/services/storage/db/storageInterface.ts +++ b/src/services/storage/db/storageInterface.ts @@ -4,7 +4,7 @@ import { DbSettings, MainDbNames } from './db.js'; import { DeepPartial, FindOptionsWhere } from 'typeorm'; import { ConnectOperation, DeleteOperation, RemoveOperation, FindOneOperation, - FindOperation, UpdateOperation, CreateAndSaveOperation, StartTxOperation, + FindOperation, FindAndCountOperation, UpdateOperation, CreateAndSaveOperation, StartTxOperation, EndTxOperation, QueryOptions, OperationResponse, IStorageOperation, IncrementOperation, @@ -111,6 +111,12 @@ export class StorageInterface extends EventEmitter { return this.handleOp(findOp) } + FindAndCount(entity: DBNames, q: QueryOptions, txId?: string, debug = false): Promise<[T[], number]> { + const opId = Math.random().toString() + const findAndCountOp: FindAndCountOperation = { type: 'findAndCount', entity, opId, q, txId, debug } + return this.handleOp<[T[], number]>(findAndCountOp) + } + Sum(entity: DBNames, columnName: PickKeysByType, q: WhereCondition, txId?: string): Promise { const opId = Math.random().toString() const sumOp: SumOperation = { type: 'sum', entity, opId, columnName, q, txId } diff --git a/src/services/storage/db/storageProcessor.ts b/src/services/storage/db/storageProcessor.ts index 49ee54b11..36e74ef29 100644 --- a/src/services/storage/db/storageProcessor.ts +++ b/src/services/storage/db/storageProcessor.ts @@ -110,6 +110,15 @@ export type FindOperation = { debug?: boolean } +export type FindAndCountOperation = { + type: 'findAndCount' + entity: DBNames + opId: string + q: QueryOptions + txId?: string + debug?: boolean +} + export type SumOperation = { type: 'sum' entity: DBNames @@ -139,7 +148,7 @@ export interface IStorageOperation { } export type StorageOperation = ConnectOperation | StartTxOperation | EndTxOperation | DeleteOperation | RemoveOperation | UpdateOperation | - FindOneOperation | FindOperation | CreateAndSaveOperation | IncrementOperation | DecrementOperation | SumOperation | PingOperation + FindOneOperation | FindOperation | FindAndCountOperation | CreateAndSaveOperation | IncrementOperation | DecrementOperation | SumOperation | PingOperation export type SuccessOperationResponse = { success: true, type: string, data: T, opId: string } export type OperationResponse = SuccessOperationResponse | ErrorOperationResponse @@ -222,6 +231,9 @@ class StorageProcessor { case 'find': await this.handleFind(operation); break; + case 'findAndCount': + await this.handleFindAndCount(operation); + break; case 'sum': await this.handleSum(operation); break; @@ -460,6 +472,18 @@ class StorageProcessor { }); } + private async handleFindAndCount(operation: FindAndCountOperation) { + const res = await this.handleRead(operation.txId, eM => { + return eM.getRepository(this.getEntity(operation.entity)).findAndCount(operation.q) + }) + this.sendResponse({ + success: true, + type: 'findAndCount', + data: res, + opId: operation.opId + }); + } + private async handleSum(operation: SumOperation) { const res = await this.handleRead(operation.txId, eM => { return eM.getRepository(this.getEntity(operation.entity)).sum(operation.columnName, operation.q) diff --git a/src/services/storage/userStorage.ts b/src/services/storage/userStorage.ts index 632faa783..3711f8322 100644 --- a/src/services/storage/userStorage.ts +++ b/src/services/storage/userStorage.ts @@ -25,6 +25,11 @@ export default class { }, txId) } + async GetUsers(filter: { skip?: number, take?: number }): Promise<{ users: User[], total: number }> { + const [users, total] = await this.dbs.FindAndCount('User', { skip: filter.skip, take: filter.take || 20, order: { updated_at: 'DESC' } }) + return { users, total } + } + /* async AddBasicUser(name: string, secret: string): Promise { return this.DB.transaction(async tx => { const user = await this.AddUser(0, tx) From 3bb2aef67360ea1db4f76bb3b686d08706064270 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 1 Jul 2026 16:47:45 +0000 Subject: [PATCH 03/22] deb --- src/services/main/adminManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 0a5a92f4d..4e3571271 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -378,6 +378,7 @@ export class AdminManager { } async GetAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq): Promise { + this.log("Getting assets and liabilities") const providers = await this.storage.liquidityStorage.GetTrackedProviders() const lnds: Types.LndAssetProvider[] = [] @@ -392,6 +393,7 @@ export class AdminManager { } } const usersBalance = await this.storage.paymentStorage.GetTotalUsersBalance(true) + this.log("Got assets and liabilities") return { users_balance: usersBalance, lnds, From 3f8b62f802672d09023b7aa8b327732def3ec9a4 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 1 Jul 2026 16:57:39 +0000 Subject: [PATCH 04/22] deb --- src/services/lnd/lnd.ts | 2 +- src/services/main/adminManager.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 18adb4736..d5903817d 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -643,7 +643,7 @@ export default class { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return { payments: [] } } - const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: BigInt(startOffset), maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }) + const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: BigInt(startOffset), maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response } diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 4e3571271..a2712b102 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -385,10 +385,14 @@ export class AdminManager { const liquidityProviders: Types.LiquidityAssetProvider[] = [] for (const provider of providers) { if (provider.provider_type === 'lnd') { + this.log("Getting lnd assets and liabilities") const lndEntry = await this.GetLndAssetsAndLiabilities(req, provider) + this.log("Got lnd assets and liabilities") lnds.push(lndEntry) } else if (provider.provider_type === 'lnPub') { + this.log("Getting liquidity provider assets and liabilities") const liquidityEntry = await this.GetProviderAssetsAndLiabilities(req, provider) + this.log("Got liquidity provider assets and liabilities") liquidityProviders.push(liquidityEntry) } } From 88698fc6b0b2c795e25e5f8f8727a7569ea53f21 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 1 Jul 2026 17:06:13 +0000 Subject: [PATCH 05/22] ops timeout --- src/services/main/liquidityProvider.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 02697fa53..4b48a7807 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -317,6 +317,15 @@ export class LiquidityProvider { return res } + newGetUserOperationsRequest = (incoming: Types.OperationsCursor, outgoing: Types.OperationsCursor, limit: number) => { + return { + latestIncomingInvoice: incoming, + latestOutgoingInvoice: outgoing, + latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { ts: 0, id: 0 }, + latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: limit + } + } + GetOperations = async (incoming: Types.OperationsCursor | undefined, outgoing: Types.OperationsCursor | undefined, limit: number | undefined) => { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") @@ -324,11 +333,13 @@ export class LiquidityProvider { const latestIncomingInvoice = incoming || { ts: 0, id: 0 } const latestOutgoingInvoice = outgoing || { ts: 0, id: 0 } const maxSize = limit || 200 - const res = await this.client.GetUserOperations({ - latestIncomingInvoice, latestOutgoingInvoice, - latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { ts: 0, id: 0 }, - latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: maxSize - }) + const res = await Promise.race([ + this.client.GetUserOperations(this.newGetUserOperationsRequest(latestIncomingInvoice, latestOutgoingInvoice, maxSize)), + new Promise(res => setTimeout(() => res(false), 5 * 1000)) + ]) + if (res === false) { + throw new Error("timeout getting operations") + } if (res.status === 'ERROR') { this.log("error getting operations", res.reason) throw new Error(res.reason) From 32caf6eefadbf632723915877ae4183e91822569 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 1 Jul 2026 17:10:41 +0000 Subject: [PATCH 06/22] deb --- src/services/main/adminManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index a2712b102..9f8aa8dd2 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -380,7 +380,7 @@ export class AdminManager { async GetAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq): Promise { this.log("Getting assets and liabilities") const providers = await this.storage.liquidityStorage.GetTrackedProviders() - + this.log("Got providers", providers.map(p => p.provider_type + ": " + p.provider_pubkey).join(", ")) const lnds: Types.LndAssetProvider[] = [] const liquidityProviders: Types.LiquidityAssetProvider[] = [] for (const provider of providers) { From e61058a1ed19cb2b12fdf3a92eb67c96fc543430 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 1 Jul 2026 17:43:42 +0000 Subject: [PATCH 07/22] deb --- src/services/main/adminManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 9f8aa8dd2..12eb49172 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -412,6 +412,10 @@ export class AdminManager { if (this.liquidityProvider.GetProviderPubkey() !== provider.provider_pubkey) { return { pubkey: provider.provider_pubkey, tracked: undefined } } + const userState = await this.liquidityProvider.GetUserState() + if (userState.status === 'ERROR') { + throw new Error("error getting provider user state " + userState.reason) + } const filter = req.liquidity_providers.find(p => p.pubkey === provider.provider_pubkey) const incoming = filter?.latestIncomingInvoice const outgoing = filter?.latestOutgoingInvoice From d7f23fa2a1067fdd9e9fd25e770fe69b14fc0e6a Mon Sep 17 00:00:00 2001 From: boufni95 Date: Thu, 2 Jul 2026 15:21:46 +0000 Subject: [PATCH 08/22] deb --- src/services/main/adminManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 12eb49172..5f3120448 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -416,6 +416,7 @@ export class AdminManager { if (userState.status === 'ERROR') { throw new Error("error getting provider user state " + userState.reason) } + this.log("user state", userState) const filter = req.liquidity_providers.find(p => p.pubkey === provider.provider_pubkey) const incoming = filter?.latestIncomingInvoice const outgoing = filter?.latestOutgoingInvoice From dcc34a15fcf8bcb01865cfca177ad081f0450806 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Thu, 2 Jul 2026 15:31:32 +0000 Subject: [PATCH 09/22] deb --- src/services/main/liquidityProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 4b48a7807..415f39451 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -332,7 +332,7 @@ export class LiquidityProvider { } const latestIncomingInvoice = incoming || { ts: 0, id: 0 } const latestOutgoingInvoice = outgoing || { ts: 0, id: 0 } - const maxSize = limit || 200 + const maxSize = limit || 20 const res = await Promise.race([ this.client.GetUserOperations(this.newGetUserOperationsRequest(latestIncomingInvoice, latestOutgoingInvoice, maxSize)), new Promise(res => setTimeout(() => res(false), 5 * 1000)) From 40b5be5dc07c700904aab8d2652dbc580e3f5242 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Thu, 2 Jul 2026 16:11:48 +0000 Subject: [PATCH 10/22] cleanup --- src/services/main/adminManager.ts | 10 ---------- src/services/main/liquidityProvider.ts | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 5f3120448..efd09e7d1 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -378,21 +378,16 @@ export class AdminManager { } async GetAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq): Promise { - this.log("Getting assets and liabilities") const providers = await this.storage.liquidityStorage.GetTrackedProviders() this.log("Got providers", providers.map(p => p.provider_type + ": " + p.provider_pubkey).join(", ")) const lnds: Types.LndAssetProvider[] = [] const liquidityProviders: Types.LiquidityAssetProvider[] = [] for (const provider of providers) { if (provider.provider_type === 'lnd') { - this.log("Getting lnd assets and liabilities") const lndEntry = await this.GetLndAssetsAndLiabilities(req, provider) - this.log("Got lnd assets and liabilities") lnds.push(lndEntry) } else if (provider.provider_type === 'lnPub') { - this.log("Getting liquidity provider assets and liabilities") const liquidityEntry = await this.GetProviderAssetsAndLiabilities(req, provider) - this.log("Got liquidity provider assets and liabilities") liquidityProviders.push(liquidityEntry) } } @@ -412,11 +407,6 @@ export class AdminManager { if (this.liquidityProvider.GetProviderPubkey() !== provider.provider_pubkey) { return { pubkey: provider.provider_pubkey, tracked: undefined } } - const userState = await this.liquidityProvider.GetUserState() - if (userState.status === 'ERROR') { - throw new Error("error getting provider user state " + userState.reason) - } - this.log("user state", userState) const filter = req.liquidity_providers.find(p => p.pubkey === provider.provider_pubkey) const incoming = filter?.latestIncomingInvoice const outgoing = filter?.latestOutgoingInvoice diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 415f39451..948b92a5f 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -335,7 +335,7 @@ export class LiquidityProvider { const maxSize = limit || 20 const res = await Promise.race([ this.client.GetUserOperations(this.newGetUserOperationsRequest(latestIncomingInvoice, latestOutgoingInvoice, maxSize)), - new Promise(res => setTimeout(() => res(false), 5 * 1000)) + new Promise(res => setTimeout(() => res(false), 10 * 1000)) ]) if (res === false) { throw new Error("timeout getting operations") From dc741dacc8a6105b52803b4cb49b64c242d2409c Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 6 Jul 2026 18:13:18 +0000 Subject: [PATCH 11/22] pagination fix --- proto/autogenerated/client.md | 28 +++-- proto/autogenerated/go/types.go | 42 ++++--- proto/autogenerated/ts/types.ts | 187 +++++++++++++++++++----------- proto/service/structs.proto | 30 +++-- src/services/lnd/lnd.ts | 14 +-- src/services/main/adminManager.ts | 166 ++++++++++++++++++-------- 6 files changed, 314 insertions(+), 153 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index d4a7ef794..e476f5366 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1499,6 +1499,11 @@ The nostr server will send back a message response, and inside the body there wi ### LinkNPubThroughTokenRequest - __token__: _string_ +### LiquidityAssetOperationsPage + - __has_more__: _boolean_ + - __next_cursor__: _[OperationsCursor](#OperationsCursor)_ *this field is optional + - __operations__: ARRAY of: _[AssetOperation](#AssetOperation)_ + ### LiquidityAssetProvider - __pubkey__: _string_ - __tracked__: _[TrackedLiquidityProvider](#TrackedLiquidityProvider)_ *this field is optional @@ -1523,6 +1528,11 @@ The nostr server will send back a message response, and inside the body there wi - __latest_balance__: _number_ - __operation__: _[UserOperation](#UserOperation)_ +### LndAssetOperationsPage + - __has_more__: _boolean_ + - __next_index_offset__: _number_ *this field is optional + - __operations__: ARRAY of: _[AssetOperation](#AssetOperation)_ + ### LndAssetProvider - __pubkey__: _string_ - __tracked__: _[TrackedLndProvider](#TrackedLndProvider)_ *this field is optional @@ -1573,11 +1583,13 @@ The nostr server will send back a message response, and inside the body there wi - __root_ops__: ARRAY of: _[RootOperation](#RootOperation)_ ### LndProviderFilter + - __invoice_index_offset__: _number_ *this field is optional - __limit_invoices__: _number_ *this field is optional - __limit_payments__: _number_ *this field is optional + - __limit_transactions__: _number_ *this field is optional + - __payment_index_offset__: _number_ *this field is optional - __pubkey__: _string_ - - __skip_invoices__: _number_ *this field is optional - - __skip_payments__: _number_ *this field is optional + - __tx_index_offset__: _number_ *this field is optional ### LndSeed - __seed__: ARRAY of: _string_ @@ -1859,16 +1871,16 @@ The nostr server will send back a message response, and inside the body there wi ### TrackedLiquidityProvider - __balance__: _number_ - - __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_ - - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __invoices__: _[LiquidityAssetOperationsPage](#LiquidityAssetOperationsPage)_ + - __payments__: _[LiquidityAssetOperationsPage](#LiquidityAssetOperationsPage)_ ### TrackedLndProvider - __channels_balance__: _number_ - __confirmed_balance__: _number_ - - __incoming_tx__: ARRAY of: _[AssetOperation](#AssetOperation)_ - - __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_ - - __outgoing_tx__: ARRAY of: _[AssetOperation](#AssetOperation)_ - - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __incoming_tx__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ + - __invoices__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ + - __outgoing_tx__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ + - __payments__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ - __unconfirmed_balance__: _number_ ### TrackedOperation diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index c111a3d0f..157c25803 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -435,6 +435,11 @@ type LatestUsageMetricReq struct { type LinkNPubThroughTokenRequest struct { Token string `json:"token"` } +type LiquidityAssetOperationsPage struct { + Has_more bool `json:"has_more"` + Next_cursor *OperationsCursor `json:"next_cursor"` + Operations []AssetOperation `json:"operations"` +} type LiquidityAssetProvider struct { Pubkey string `json:"pubkey"` Tracked *TrackedLiquidityProvider `json:"tracked"` @@ -459,6 +464,11 @@ type LiveUserOperation struct { Latest_balance int64 `json:"latest_balance"` Operation *UserOperation `json:"operation"` } +type LndAssetOperationsPage struct { + Has_more bool `json:"has_more"` + Next_index_offset int64 `json:"next_index_offset"` + Operations []AssetOperation `json:"operations"` +} type LndAssetProvider struct { Pubkey string `json:"pubkey"` Tracked *TrackedLndProvider `json:"tracked"` @@ -509,11 +519,13 @@ type LndNodeMetrics struct { Root_ops []RootOperation `json:"root_ops"` } type LndProviderFilter struct { - Limit_invoices int64 `json:"limit_invoices"` - Limit_payments int64 `json:"limit_payments"` - Pubkey string `json:"pubkey"` - Skip_invoices int64 `json:"skip_invoices"` - Skip_payments int64 `json:"skip_payments"` + Invoice_index_offset int64 `json:"invoice_index_offset"` + Limit_invoices int64 `json:"limit_invoices"` + Limit_payments int64 `json:"limit_payments"` + Limit_transactions int64 `json:"limit_transactions"` + Payment_index_offset int64 `json:"payment_index_offset"` + Pubkey string `json:"pubkey"` + Tx_index_offset int64 `json:"tx_index_offset"` } type LndSeed struct { Seed []string `json:"seed"` @@ -794,18 +806,18 @@ type SingleMetricReq struct { Request_id int64 `json:"request_id"` } type TrackedLiquidityProvider struct { - Balance int64 `json:"balance"` - Invoices []AssetOperation `json:"invoices"` - Payments []AssetOperation `json:"payments"` + Balance int64 `json:"balance"` + Invoices *LiquidityAssetOperationsPage `json:"invoices"` + Payments *LiquidityAssetOperationsPage `json:"payments"` } type TrackedLndProvider struct { - Channels_balance int64 `json:"channels_balance"` - Confirmed_balance int64 `json:"confirmed_balance"` - Incoming_tx []AssetOperation `json:"incoming_tx"` - Invoices []AssetOperation `json:"invoices"` - Outgoing_tx []AssetOperation `json:"outgoing_tx"` - Payments []AssetOperation `json:"payments"` - Unconfirmed_balance int64 `json:"unconfirmed_balance"` + Channels_balance int64 `json:"channels_balance"` + Confirmed_balance int64 `json:"confirmed_balance"` + Incoming_tx *LndAssetOperationsPage `json:"incoming_tx"` + Invoices *LndAssetOperationsPage `json:"invoices"` + Outgoing_tx *LndAssetOperationsPage `json:"outgoing_tx"` + Payments *LndAssetOperationsPage `json:"payments"` + Unconfirmed_balance int64 `json:"unconfirmed_balance"` } type TrackedOperation struct { Amount int64 `json:"amount"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 91920829d..e8ab0ab1f 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -2573,6 +2573,43 @@ export const LinkNPubThroughTokenRequestValidate = (o?: LinkNPubThroughTokenRequ return null } +export type LiquidityAssetOperationsPage = { + has_more: boolean + next_cursor?: OperationsCursor + operations: AssetOperation[] +} +export type LiquidityAssetOperationsPageOptionalField = 'next_cursor' +export const LiquidityAssetOperationsPageOptionalFields: LiquidityAssetOperationsPageOptionalField[] = ['next_cursor'] +export type LiquidityAssetOperationsPageOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: LiquidityAssetOperationsPageOptionalField[] + has_more_CustomCheck?: (v: boolean) => boolean + next_cursor_Options?: OperationsCursorOptions + operations_ItemOptions?: AssetOperationOptions + operations_CustomCheck?: (v: AssetOperation[]) => boolean +} +export const LiquidityAssetOperationsPageValidate = (o?: LiquidityAssetOperationsPage, opts: LiquidityAssetOperationsPageOptions = {}, path: string = 'LiquidityAssetOperationsPage::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.has_more !== 'boolean') return new Error(`${path}.has_more: is not a boolean`) + if (opts.has_more_CustomCheck && !opts.has_more_CustomCheck(o.has_more)) return new Error(`${path}.has_more: custom check failed`) + + if (typeof o.next_cursor === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('next_cursor')) { + const next_cursorErr = OperationsCursorValidate(o.next_cursor, opts.next_cursor_Options, `${path}.next_cursor`) + if (next_cursorErr !== null) return next_cursorErr + } + + + if (!Array.isArray(o.operations)) return new Error(`${path}.operations: is not an array`) + for (let index = 0; index < o.operations.length; index++) { + const operationsErr = AssetOperationValidate(o.operations[index], opts.operations_ItemOptions, `${path}.operations[${index}]`) + if (operationsErr !== null) return operationsErr + } + if (opts.operations_CustomCheck && !opts.operations_CustomCheck(o.operations)) return new Error(`${path}.operations: custom check failed`) + + return null +} + export type LiquidityAssetProvider = { pubkey: string tracked?: TrackedLiquidityProvider @@ -2722,6 +2759,40 @@ export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserO return null } +export type LndAssetOperationsPage = { + has_more: boolean + next_index_offset?: number + operations: AssetOperation[] +} +export type LndAssetOperationsPageOptionalField = 'next_index_offset' +export const LndAssetOperationsPageOptionalFields: LndAssetOperationsPageOptionalField[] = ['next_index_offset'] +export type LndAssetOperationsPageOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: LndAssetOperationsPageOptionalField[] + has_more_CustomCheck?: (v: boolean) => boolean + next_index_offset_CustomCheck?: (v?: number) => boolean + operations_ItemOptions?: AssetOperationOptions + operations_CustomCheck?: (v: AssetOperation[]) => boolean +} +export const LndAssetOperationsPageValidate = (o?: LndAssetOperationsPage, opts: LndAssetOperationsPageOptions = {}, path: string = 'LndAssetOperationsPage::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.has_more !== 'boolean') return new Error(`${path}.has_more: is not a boolean`) + if (opts.has_more_CustomCheck && !opts.has_more_CustomCheck(o.has_more)) return new Error(`${path}.has_more: custom check failed`) + + if ((o.next_index_offset || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('next_index_offset')) && typeof o.next_index_offset !== 'number') return new Error(`${path}.next_index_offset: is not a number`) + if (opts.next_index_offset_CustomCheck && !opts.next_index_offset_CustomCheck(o.next_index_offset)) return new Error(`${path}.next_index_offset: custom check failed`) + + if (!Array.isArray(o.operations)) return new Error(`${path}.operations: is not an array`) + for (let index = 0; index < o.operations.length; index++) { + const operationsErr = AssetOperationValidate(o.operations[index], opts.operations_ItemOptions, `${path}.operations[${index}]`) + if (operationsErr !== null) return operationsErr + } + if (opts.operations_CustomCheck && !opts.operations_CustomCheck(o.operations)) return new Error(`${path}.operations: custom check failed`) + + return null +} + export type LndAssetProvider = { pubkey: string tracked?: TrackedLndProvider @@ -3045,40 +3116,50 @@ export const LndNodeMetricsValidate = (o?: LndNodeMetrics, opts: LndNodeMetricsO } export type LndProviderFilter = { + invoice_index_offset?: number limit_invoices?: number limit_payments?: number + limit_transactions?: number + payment_index_offset?: number pubkey: string - skip_invoices?: number - skip_payments?: number + tx_index_offset?: number } -export type LndProviderFilterOptionalField = 'limit_invoices' | 'limit_payments' | 'skip_invoices' | 'skip_payments' -export const LndProviderFilterOptionalFields: LndProviderFilterOptionalField[] = ['limit_invoices', 'limit_payments', 'skip_invoices', 'skip_payments'] +export type LndProviderFilterOptionalField = 'invoice_index_offset' | 'limit_invoices' | 'limit_payments' | 'limit_transactions' | 'payment_index_offset' | 'tx_index_offset' +export const LndProviderFilterOptionalFields: LndProviderFilterOptionalField[] = ['invoice_index_offset', 'limit_invoices', 'limit_payments', 'limit_transactions', 'payment_index_offset', 'tx_index_offset'] export type LndProviderFilterOptions = OptionsBaseMessage & { checkOptionalsAreSet?: LndProviderFilterOptionalField[] + invoice_index_offset_CustomCheck?: (v?: number) => boolean limit_invoices_CustomCheck?: (v?: number) => boolean limit_payments_CustomCheck?: (v?: number) => boolean + limit_transactions_CustomCheck?: (v?: number) => boolean + payment_index_offset_CustomCheck?: (v?: number) => boolean pubkey_CustomCheck?: (v: string) => boolean - skip_invoices_CustomCheck?: (v?: number) => boolean - skip_payments_CustomCheck?: (v?: number) => boolean + tx_index_offset_CustomCheck?: (v?: number) => boolean } export const LndProviderFilterValidate = (o?: LndProviderFilter, opts: LndProviderFilterOptions = {}, path: string = 'LndProviderFilter::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + if ((o.invoice_index_offset || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('invoice_index_offset')) && typeof o.invoice_index_offset !== 'number') return new Error(`${path}.invoice_index_offset: is not a number`) + if (opts.invoice_index_offset_CustomCheck && !opts.invoice_index_offset_CustomCheck(o.invoice_index_offset)) return new Error(`${path}.invoice_index_offset: custom check failed`) + if ((o.limit_invoices || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_invoices')) && typeof o.limit_invoices !== 'number') return new Error(`${path}.limit_invoices: is not a number`) if (opts.limit_invoices_CustomCheck && !opts.limit_invoices_CustomCheck(o.limit_invoices)) return new Error(`${path}.limit_invoices: custom check failed`) if ((o.limit_payments || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_payments')) && typeof o.limit_payments !== 'number') return new Error(`${path}.limit_payments: is not a number`) if (opts.limit_payments_CustomCheck && !opts.limit_payments_CustomCheck(o.limit_payments)) return new Error(`${path}.limit_payments: custom check failed`) + if ((o.limit_transactions || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_transactions')) && typeof o.limit_transactions !== 'number') return new Error(`${path}.limit_transactions: is not a number`) + if (opts.limit_transactions_CustomCheck && !opts.limit_transactions_CustomCheck(o.limit_transactions)) return new Error(`${path}.limit_transactions: custom check failed`) + + if ((o.payment_index_offset || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('payment_index_offset')) && typeof o.payment_index_offset !== 'number') return new Error(`${path}.payment_index_offset: is not a number`) + if (opts.payment_index_offset_CustomCheck && !opts.payment_index_offset_CustomCheck(o.payment_index_offset)) return new Error(`${path}.payment_index_offset: custom check failed`) + if (typeof o.pubkey !== 'string') return new Error(`${path}.pubkey: is not a string`) if (opts.pubkey_CustomCheck && !opts.pubkey_CustomCheck(o.pubkey)) return new Error(`${path}.pubkey: custom check failed`) - if ((o.skip_invoices || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('skip_invoices')) && typeof o.skip_invoices !== 'number') return new Error(`${path}.skip_invoices: is not a number`) - if (opts.skip_invoices_CustomCheck && !opts.skip_invoices_CustomCheck(o.skip_invoices)) return new Error(`${path}.skip_invoices: custom check failed`) - - if ((o.skip_payments || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('skip_payments')) && typeof o.skip_payments !== 'number') return new Error(`${path}.skip_payments: is not a number`) - if (opts.skip_payments_CustomCheck && !opts.skip_payments_CustomCheck(o.skip_payments)) return new Error(`${path}.skip_payments: custom check failed`) + if ((o.tx_index_offset || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('tx_index_offset')) && typeof o.tx_index_offset !== 'number') return new Error(`${path}.tx_index_offset: is not a number`) + if (opts.tx_index_offset_CustomCheck && !opts.tx_index_offset_CustomCheck(o.tx_index_offset)) return new Error(`${path}.tx_index_offset: custom check failed`) return null } @@ -4695,17 +4776,15 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR export type TrackedLiquidityProvider = { balance: number - invoices: AssetOperation[] - payments: AssetOperation[] + invoices: LiquidityAssetOperationsPage + payments: LiquidityAssetOperationsPage } export const TrackedLiquidityProviderOptionalFields: [] = [] export type TrackedLiquidityProviderOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] balance_CustomCheck?: (v: number) => boolean - invoices_ItemOptions?: AssetOperationOptions - invoices_CustomCheck?: (v: AssetOperation[]) => boolean - payments_ItemOptions?: AssetOperationOptions - payments_CustomCheck?: (v: AssetOperation[]) => boolean + invoices_Options?: LiquidityAssetOperationsPageOptions + payments_Options?: LiquidityAssetOperationsPageOptions } export const TrackedLiquidityProviderValidate = (o?: TrackedLiquidityProvider, opts: TrackedLiquidityProviderOptions = {}, path: string = 'TrackedLiquidityProvider::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') @@ -4714,19 +4793,13 @@ export const TrackedLiquidityProviderValidate = (o?: TrackedLiquidityProvider, o if (typeof o.balance !== 'number') return new Error(`${path}.balance: is not a number`) if (opts.balance_CustomCheck && !opts.balance_CustomCheck(o.balance)) return new Error(`${path}.balance: custom check failed`) - if (!Array.isArray(o.invoices)) return new Error(`${path}.invoices: is not an array`) - for (let index = 0; index < o.invoices.length; index++) { - const invoicesErr = AssetOperationValidate(o.invoices[index], opts.invoices_ItemOptions, `${path}.invoices[${index}]`) - if (invoicesErr !== null) return invoicesErr - } - if (opts.invoices_CustomCheck && !opts.invoices_CustomCheck(o.invoices)) return new Error(`${path}.invoices: custom check failed`) + const invoicesErr = LiquidityAssetOperationsPageValidate(o.invoices, opts.invoices_Options, `${path}.invoices`) + if (invoicesErr !== null) return invoicesErr + - if (!Array.isArray(o.payments)) return new Error(`${path}.payments: is not an array`) - for (let index = 0; index < o.payments.length; index++) { - const paymentsErr = AssetOperationValidate(o.payments[index], opts.payments_ItemOptions, `${path}.payments[${index}]`) - if (paymentsErr !== null) return paymentsErr - } - if (opts.payments_CustomCheck && !opts.payments_CustomCheck(o.payments)) return new Error(`${path}.payments: custom check failed`) + const paymentsErr = LiquidityAssetOperationsPageValidate(o.payments, opts.payments_Options, `${path}.payments`) + if (paymentsErr !== null) return paymentsErr + return null } @@ -4734,10 +4807,10 @@ export const TrackedLiquidityProviderValidate = (o?: TrackedLiquidityProvider, o export type TrackedLndProvider = { channels_balance: number confirmed_balance: number - incoming_tx: AssetOperation[] - invoices: AssetOperation[] - outgoing_tx: AssetOperation[] - payments: AssetOperation[] + incoming_tx: LndAssetOperationsPage + invoices: LndAssetOperationsPage + outgoing_tx: LndAssetOperationsPage + payments: LndAssetOperationsPage unconfirmed_balance: number } export const TrackedLndProviderOptionalFields: [] = [] @@ -4745,14 +4818,10 @@ export type TrackedLndProviderOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] channels_balance_CustomCheck?: (v: number) => boolean confirmed_balance_CustomCheck?: (v: number) => boolean - incoming_tx_ItemOptions?: AssetOperationOptions - incoming_tx_CustomCheck?: (v: AssetOperation[]) => boolean - invoices_ItemOptions?: AssetOperationOptions - invoices_CustomCheck?: (v: AssetOperation[]) => boolean - outgoing_tx_ItemOptions?: AssetOperationOptions - outgoing_tx_CustomCheck?: (v: AssetOperation[]) => boolean - payments_ItemOptions?: AssetOperationOptions - payments_CustomCheck?: (v: AssetOperation[]) => boolean + incoming_tx_Options?: LndAssetOperationsPageOptions + invoices_Options?: LndAssetOperationsPageOptions + outgoing_tx_Options?: LndAssetOperationsPageOptions + payments_Options?: LndAssetOperationsPageOptions unconfirmed_balance_CustomCheck?: (v: number) => boolean } export const TrackedLndProviderValidate = (o?: TrackedLndProvider, opts: TrackedLndProviderOptions = {}, path: string = 'TrackedLndProvider::root.'): Error | null => { @@ -4765,33 +4834,21 @@ export const TrackedLndProviderValidate = (o?: TrackedLndProvider, opts: Tracked if (typeof o.confirmed_balance !== 'number') return new Error(`${path}.confirmed_balance: is not a number`) if (opts.confirmed_balance_CustomCheck && !opts.confirmed_balance_CustomCheck(o.confirmed_balance)) return new Error(`${path}.confirmed_balance: custom check failed`) - if (!Array.isArray(o.incoming_tx)) return new Error(`${path}.incoming_tx: is not an array`) - for (let index = 0; index < o.incoming_tx.length; index++) { - const incoming_txErr = AssetOperationValidate(o.incoming_tx[index], opts.incoming_tx_ItemOptions, `${path}.incoming_tx[${index}]`) - if (incoming_txErr !== null) return incoming_txErr - } - if (opts.incoming_tx_CustomCheck && !opts.incoming_tx_CustomCheck(o.incoming_tx)) return new Error(`${path}.incoming_tx: custom check failed`) + const incoming_txErr = LndAssetOperationsPageValidate(o.incoming_tx, opts.incoming_tx_Options, `${path}.incoming_tx`) + if (incoming_txErr !== null) return incoming_txErr + - if (!Array.isArray(o.invoices)) return new Error(`${path}.invoices: is not an array`) - for (let index = 0; index < o.invoices.length; index++) { - const invoicesErr = AssetOperationValidate(o.invoices[index], opts.invoices_ItemOptions, `${path}.invoices[${index}]`) - if (invoicesErr !== null) return invoicesErr - } - if (opts.invoices_CustomCheck && !opts.invoices_CustomCheck(o.invoices)) return new Error(`${path}.invoices: custom check failed`) + const invoicesErr = LndAssetOperationsPageValidate(o.invoices, opts.invoices_Options, `${path}.invoices`) + if (invoicesErr !== null) return invoicesErr + - if (!Array.isArray(o.outgoing_tx)) return new Error(`${path}.outgoing_tx: is not an array`) - for (let index = 0; index < o.outgoing_tx.length; index++) { - const outgoing_txErr = AssetOperationValidate(o.outgoing_tx[index], opts.outgoing_tx_ItemOptions, `${path}.outgoing_tx[${index}]`) - if (outgoing_txErr !== null) return outgoing_txErr - } - if (opts.outgoing_tx_CustomCheck && !opts.outgoing_tx_CustomCheck(o.outgoing_tx)) return new Error(`${path}.outgoing_tx: custom check failed`) + const outgoing_txErr = LndAssetOperationsPageValidate(o.outgoing_tx, opts.outgoing_tx_Options, `${path}.outgoing_tx`) + if (outgoing_txErr !== null) return outgoing_txErr + - if (!Array.isArray(o.payments)) return new Error(`${path}.payments: is not an array`) - for (let index = 0; index < o.payments.length; index++) { - const paymentsErr = AssetOperationValidate(o.payments[index], opts.payments_ItemOptions, `${path}.payments[${index}]`) - if (paymentsErr !== null) return paymentsErr - } - if (opts.payments_CustomCheck && !opts.payments_CustomCheck(o.payments)) return new Error(`${path}.payments: custom check failed`) + const paymentsErr = LndAssetOperationsPageValidate(o.payments, opts.payments_Options, `${path}.payments`) + if (paymentsErr !== null) return paymentsErr + if (typeof o.unconfirmed_balance !== 'number') return new Error(`${path}.unconfirmed_balance: is not a number`) if (opts.unconfirmed_balance_CustomCheck && !opts.unconfirmed_balance_CustomCheck(o.unconfirmed_balance)) return new Error(`${path}.unconfirmed_balance: custom check failed`) diff --git a/proto/service/structs.proto b/proto/service/structs.proto index f00631aa9..29d626d63 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -22,9 +22,11 @@ message EncryptionExchangeRequest { message LndProviderFilter { string pubkey = 1; optional int64 limit_invoices = 2; - optional int64 skip_invoices = 3; + optional int64 invoice_index_offset = 3; optional int64 limit_payments = 4; - optional int64 skip_payments = 5; + optional int64 payment_index_offset = 5; + optional int64 limit_transactions = 6; + optional int64 tx_index_offset = 7; } message LiquidityProviderFilter { @@ -89,20 +91,32 @@ message AssetOperation { optional TrackedOperation tracked = 3; } +message LndAssetOperationsPage { + repeated AssetOperation operations = 1; + bool has_more = 2; + optional int64 next_index_offset = 3; +} + +message LiquidityAssetOperationsPage { + repeated AssetOperation operations = 1; + bool has_more = 2; + optional OperationsCursor next_cursor = 3; +} + message TrackedLndProvider { int64 confirmed_balance = 1; int64 unconfirmed_balance = 2; int64 channels_balance = 3; - repeated AssetOperation payments = 4; - repeated AssetOperation invoices = 5; - repeated AssetOperation incoming_tx = 6; - repeated AssetOperation outgoing_tx = 7; + LndAssetOperationsPage payments = 4; + LndAssetOperationsPage invoices = 5; + LndAssetOperationsPage incoming_tx = 6; + LndAssetOperationsPage outgoing_tx = 7; } message TrackedLiquidityProvider { int64 balance = 1; - repeated AssetOperation payments = 2; - repeated AssetOperation invoices = 3; + LiquidityAssetOperationsPage payments = 2; + LiquidityAssetOperationsPage invoices = 3; } message LndAssetProvider { diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index d5903817d..efae3b0b8 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -9,7 +9,7 @@ import { LightningClient } from '../../../proto/lnd/lightning.client.js' import { InvoicesClient } from '../../../proto/lnd/invoices.client.js' import { RouterClient } from '../../../proto/lnd/router.client.js' import { ChainNotifierClient } from '../../../proto/lnd/chainnotifier.client.js' -import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse, CoinSelectionStrategy, OpenStatusUpdate, CloseStatusUpdate, PendingUpdate, ChannelEventUpdate_UpdateType } from '../../../proto/lnd/lightning.js' +import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse, CoinSelectionStrategy, OpenStatusUpdate, CloseStatusUpdate, PendingUpdate, ChannelEventUpdate_UpdateType, ListPaymentsResponse, ListInvoiceResponse } from '../../../proto/lnd/lightning.js' import { OpenChannelReq } from './openChannelReq.js'; import { AddInvoiceReq } from './addInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js'; @@ -544,9 +544,9 @@ export default class { } } - async GetTransactions(startHeight: number): Promise { + async GetTransactions(startHeight: number, indexOffset = 0, maxTransactions = 0): Promise { this.log(DEBUG, "Getting transactions") - const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "", indexOffset: 0, maxTransactions: 0 }, DeadLineMetadata()) + const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "", indexOffset, maxTransactions }, DeadLineMetadata()) return res.response } @@ -630,18 +630,18 @@ export default class { return response } - async GetAllInvoices(max: number, startOffset = 0) { + async GetAllInvoices(max: number, startOffset = 0): Promise { this.log(DEBUG, "Getting all paid invoices") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { - return { invoices: [] } + return { invoices: [], firstIndexOffset: 0n, lastIndexOffset: 0n } } const res = await this.lightning.listInvoices({ indexOffset: BigInt(startOffset), numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response } - async GetAllPayments(max: number, startOffset = 0) { + async GetAllPayments(max: number, startOffset = 0): Promise { this.log(DEBUG, "Getting all payments") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { - return { payments: [] } + return { payments: [], firstIndexOffset: 0n, lastIndexOffset: 0n, totalNumPayments: 0n } } const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: BigInt(startOffset), maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index efd09e7d1..c00e9c9a1 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -410,19 +410,18 @@ export class AdminManager { const filter = req.liquidity_providers.find(p => p.pubkey === provider.provider_pubkey) const incoming = filter?.latestIncomingInvoice const outgoing = filter?.latestOutgoingInvoice - const limit = filter?.limit + const limit = filter?.limit || 20 const providerOps = await this.liquidityProvider.GetOperations(incoming, outgoing, limit) - // we only care about invoices cuz they are the only ops we can generate with a provider - const invoices: Types.AssetOperation[] = [] - const payments: Types.AssetOperation[] = [] - for (const op of providerOps.latestIncomingInvoiceOperations.operations) { - const assetOp = await this.GetProviderInvoiceAssetOperation(op) - invoices.push(assetOp) - } - for (const op of providerOps.latestOutgoingInvoiceOperations.operations) { - const assetOp = await this.GetProviderPaymentAssetOperation(op) - payments.push(assetOp) - } + const invoices = await this.BuildLiquidityAssetOperationsPage( + providerOps.latestIncomingInvoiceOperations, + limit, + op => this.GetProviderInvoiceAssetOperation(op) + ) + const payments = await this.BuildLiquidityAssetOperationsPage( + providerOps.latestOutgoingInvoiceOperations, + limit, + op => this.GetProviderPaymentAssetOperation(op) + ) const balance = await this.liquidityProvider.GetUserState() return { pubkey: provider.provider_pubkey, @@ -434,6 +433,23 @@ export class AdminManager { } } + private async BuildLiquidityAssetOperationsPage( + userOps: Types.UserOperations, + limit: number, + mapper: (op: Types.UserOperation) => Promise + ): Promise { + const operations: Types.AssetOperation[] = [] + for (const op of userOps.operations) { + operations.push(await mapper(op)) + } + const hasMore = userOps.operations.length >= limit + return { + operations, + has_more: hasMore, + next_cursor: hasMore ? userOps.toIndex : undefined, + } + } + async GetProviderInvoiceAssetOperation(op: Types.UserOperation): Promise { const ts = Number(op.paidAtUnix) const amount = Number(op.amount) @@ -481,59 +497,109 @@ export class AdminManager { } const filter = req.lnd_providers.find(p => p.pubkey === provider.provider_pubkey) - const paymentsStart = filter?.skip_payments || 0 const paymentsLimit = filter?.limit_payments || 50 - - const latestLndPayments = await this.lnd.GetAllPayments(paymentsLimit, paymentsStart) - const payments: Types.AssetOperation[] = [] - for (const payment of latestLndPayments.payments) { - if (payment.status !== Payment_PaymentStatus.SUCCEEDED) { - continue + const paymentsOffset = filter?.payment_index_offset || 0 + const invoicesLimit = filter?.limit_invoices || 50 + const invoicesOffset = filter?.invoice_index_offset || 0 + const txLimit = filter?.limit_transactions || 50 + const txOffset = filter?.tx_index_offset || 0 + + const [payments, invoices, txPages, balance] = await Promise.all([ + this.FetchLndPaymentPage(paymentsLimit, paymentsOffset), + this.FetchLndInvoicePage(invoicesLimit, invoicesOffset), + this.FetchLndTransactionPages(info.blockHeight, txLimit, txOffset), + this.lnd.GetBalance(), + ]) + const channelsBalance = balance.channelsBalance.reduce((acc, c) => acc + Number(c.localBalanceSats), 0) + return { + pubkey: provider.provider_pubkey, + tracked: { + confirmed_balance: Number(balance.confirmedBalance), + unconfirmed_balance: Number(balance.unconfirmedBalance), + channels_balance: channelsBalance, + payments, + invoices, + incoming_tx: txPages.incoming_tx, + outgoing_tx: txPages.outgoing_tx, } - const assetOp = await this.GetPaymentAssetOperation(payment) - payments.push(assetOp) } - const invoices: Types.AssetOperation[] = [] - const invoicesStart = filter?.skip_invoices || 0 - const invoicesLimit = filter?.limit_invoices || 50 - const paidInvoices = await this.lnd.GetAllInvoices(invoicesLimit, invoicesStart) - for (const invoiceEntry of paidInvoices.invoices) { - if (invoiceEntry.state !== Invoice_InvoiceState.SETTLED) { + } + + private async BuildLndAssetOperationsPage( + limit: number, + batch: T[], + firstIndexOffset: number | undefined, + include: (item: T) => boolean, + toAssetOp: (item: T) => Promise, + ): Promise { + const operations: Types.AssetOperation[] = [] + for (const item of batch) { + if (!include(item)) { continue } - const assetOp = await this.GetInvoiceAssetOperation(invoiceEntry) - invoices.push(assetOp) + operations.push(await toAssetOp(item)) } - const latestLndTransactions = await this.lnd.GetTransactions(info.blockHeight) - const txOuts: Types.AssetOperation[] = [] - const txIns: Types.AssetOperation[] = [] - for (const transaction of latestLndTransactions.transactions) { + const hasMore = batch.length >= limit + return { + operations, + has_more: hasMore, + next_index_offset: hasMore ? firstIndexOffset : undefined, + } + } + + private async FetchLndPaymentPage(limit: number, indexOffset: number): Promise { + const res = await this.lnd.GetAllPayments(limit, indexOffset) + const firstIndexOffset = Number(res.firstIndexOffset) + return this.BuildLndAssetOperationsPage( + limit, + res.payments, + firstIndexOffset, + payment => payment.status === Payment_PaymentStatus.SUCCEEDED, + payment => this.GetPaymentAssetOperation(payment), + ) + } + + private async FetchLndInvoicePage(limit: number, indexOffset: number): Promise { + const res = await this.lnd.GetAllInvoices(limit, indexOffset) + const firstIndexOffset = Number(res.firstIndexOffset) + return this.BuildLndAssetOperationsPage( + limit, + res.invoices, + firstIndexOffset, + invoice => invoice.state === Invoice_InvoiceState.SETTLED, + invoice => this.GetInvoiceAssetOperation(invoice), + ) + } + + private async FetchLndTransactionPages( + blockHeight: number, + limit: number, + indexOffset: number + ): Promise<{ incoming_tx: Types.LndAssetOperationsPage, outgoing_tx: Types.LndAssetOperationsPage }> { + const res = await this.lnd.GetTransactions(blockHeight, indexOffset, limit) + const incoming: Types.AssetOperation[] = [] + const outgoing: Types.AssetOperation[] = [] + for (const transaction of res.transactions) { for (const output of transaction.outputDetails) { if (output.isOurAddress) { - const assetOp = await this.GetTxOutAssetOperation(transaction, output) - txOuts.push(assetOp) + incoming.push(await this.GetTxOutAssetOperation(transaction, output)) } } - // we only produce TXs with a single output const input = transaction.previousOutpoints.find(p => p.isOurOutput) if (input) { - const assetOp = await this.GetTxInAssetOperation(transaction) - txIns.push(assetOp) + outgoing.push(await this.GetTxInAssetOperation(transaction)) } } - const balance = await this.lnd.GetBalance() - const channelsBalance = balance.channelsBalance.reduce((acc, c) => acc + Number(c.localBalanceSats), 0) + const hasMore = res.transactions.length >= limit + const nextIndexOffset = hasMore && res.transactions.length > 0 ? Number(res.firstIndex) : undefined + const toPage = (operations: Types.AssetOperation[]): Types.LndAssetOperationsPage => ({ + operations, + has_more: hasMore, + next_index_offset: nextIndexOffset, + }) return { - pubkey: provider.provider_pubkey, - tracked: { - confirmed_balance: Number(balance.confirmedBalance), - unconfirmed_balance: Number(balance.unconfirmedBalance), - channels_balance: channelsBalance, - payments, - invoices, - incoming_tx: txOuts, // tx outputs, are incoming sats - outgoing_tx: txIns, // tx inputs, are outgoing sats - } + incoming_tx: toPage(incoming), + outgoing_tx: toPage(outgoing), } } From 6000d6aee0fcda3b74429e1e1a5d50ade15aae34 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 6 Jul 2026 18:23:00 +0000 Subject: [PATCH 12/22] bounds on take --- src/services/main/adminManager.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index c00e9c9a1..6998aeb9f 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -46,6 +46,13 @@ type ProviderAssetProvider = { } */ const ROOT_OP = Types.TrackedOperationType.ROOT const USER_OP = Types.TrackedOperationType.USER +const DEFAULT_LND_PAGE_SIZE = 50 +const MAX_LND_PAGE_SIZE = 100 +const DEFAULT_LIQUIDITY_PAGE_SIZE = 20 +const MAX_LIQUIDITY_PAGE_SIZE = 40 + +const clampPageLimit = (value: number | undefined, defaultLimit: number, maxLimit: number): number => + Math.min(Math.max(value ?? defaultLimit, 1), maxLimit) export class AdminManager { settings: SettingsManager liquidityProvider: LiquidityProvider | null = null @@ -410,7 +417,7 @@ export class AdminManager { const filter = req.liquidity_providers.find(p => p.pubkey === provider.provider_pubkey) const incoming = filter?.latestIncomingInvoice const outgoing = filter?.latestOutgoingInvoice - const limit = filter?.limit || 20 + const limit = clampPageLimit(filter?.limit, DEFAULT_LIQUIDITY_PAGE_SIZE, MAX_LIQUIDITY_PAGE_SIZE) const providerOps = await this.liquidityProvider.GetOperations(incoming, outgoing, limit) const invoices = await this.BuildLiquidityAssetOperationsPage( providerOps.latestIncomingInvoiceOperations, @@ -497,11 +504,11 @@ export class AdminManager { } const filter = req.lnd_providers.find(p => p.pubkey === provider.provider_pubkey) - const paymentsLimit = filter?.limit_payments || 50 + const paymentsLimit = clampPageLimit(filter?.limit_payments, DEFAULT_LND_PAGE_SIZE, MAX_LND_PAGE_SIZE) const paymentsOffset = filter?.payment_index_offset || 0 - const invoicesLimit = filter?.limit_invoices || 50 + const invoicesLimit = clampPageLimit(filter?.limit_invoices, DEFAULT_LND_PAGE_SIZE, MAX_LND_PAGE_SIZE) const invoicesOffset = filter?.invoice_index_offset || 0 - const txLimit = filter?.limit_transactions || 50 + const txLimit = clampPageLimit(filter?.limit_transactions, DEFAULT_LND_PAGE_SIZE, MAX_LND_PAGE_SIZE) const txOffset = filter?.tx_index_offset || 0 const [payments, invoices, txPages, balance] = await Promise.all([ From aa78892d20ed08885c43b9cdaab2e7c1db4f8606 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Tue, 7 Jul 2026 17:41:57 +0000 Subject: [PATCH 13/22] fixes and tests --- src/services/helpers/pageLimit.ts | 8 ++ src/services/main/adminManager.ts | 18 +--- src/services/main/appUserManager.ts | 6 +- src/services/main/paymentManager.ts | 14 +-- src/services/serverMethods/index.ts | 5 +- src/tests/assetsAndLiabilities.spec.ts | 144 +++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 src/services/helpers/pageLimit.ts create mode 100644 src/tests/assetsAndLiabilities.spec.ts diff --git a/src/services/helpers/pageLimit.ts b/src/services/helpers/pageLimit.ts new file mode 100644 index 000000000..77878eec9 --- /dev/null +++ b/src/services/helpers/pageLimit.ts @@ -0,0 +1,8 @@ +export const DEFAULT_PAGE_SIZE = 20 +export const DEFAULT_LND_PAGE_SIZE = 50 +export const MAX_PAGE_SIZE = 100 +export const MAX_LIQUIDITY_PAGE_SIZE = 40 + +export const clampPageLimit = (value: number | undefined, defaultLimit: number, maxLimit: number): number => + Math.min(Math.max(value ?? defaultLimit, 1), maxLimit) + diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 6998aeb9f..25b84889b 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -11,6 +11,7 @@ import { TrackedProvider } from "../storage/entity/TrackedProvider.js"; import { NodeInfo } from "../lnd/settings.js"; import { Invoice, Payment, OutputDetail, Transaction, Payment_PaymentStatus, Invoice_InvoiceState } from "../../../proto/lnd/lightning.js"; import { LiquidityProvider } from "./liquidityProvider.js"; +import { clampPageLimit, DEFAULT_LND_PAGE_SIZE, DEFAULT_PAGE_SIZE, MAX_LIQUIDITY_PAGE_SIZE, MAX_PAGE_SIZE } from "../helpers/pageLimit.js"; /* type TrackedOperation = { ts: number amount: number @@ -46,13 +47,6 @@ type ProviderAssetProvider = { } */ const ROOT_OP = Types.TrackedOperationType.ROOT const USER_OP = Types.TrackedOperationType.USER -const DEFAULT_LND_PAGE_SIZE = 50 -const MAX_LND_PAGE_SIZE = 100 -const DEFAULT_LIQUIDITY_PAGE_SIZE = 20 -const MAX_LIQUIDITY_PAGE_SIZE = 40 - -const clampPageLimit = (value: number | undefined, defaultLimit: number, maxLimit: number): number => - Math.min(Math.max(value ?? defaultLimit, 1), maxLimit) export class AdminManager { settings: SettingsManager liquidityProvider: LiquidityProvider | null = null @@ -386,7 +380,6 @@ export class AdminManager { async GetAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq): Promise { const providers = await this.storage.liquidityStorage.GetTrackedProviders() - this.log("Got providers", providers.map(p => p.provider_type + ": " + p.provider_pubkey).join(", ")) const lnds: Types.LndAssetProvider[] = [] const liquidityProviders: Types.LiquidityAssetProvider[] = [] for (const provider of providers) { @@ -399,7 +392,6 @@ export class AdminManager { } } const usersBalance = await this.storage.paymentStorage.GetTotalUsersBalance(true) - this.log("Got assets and liabilities") return { users_balance: usersBalance, lnds, @@ -417,7 +409,7 @@ export class AdminManager { const filter = req.liquidity_providers.find(p => p.pubkey === provider.provider_pubkey) const incoming = filter?.latestIncomingInvoice const outgoing = filter?.latestOutgoingInvoice - const limit = clampPageLimit(filter?.limit, DEFAULT_LIQUIDITY_PAGE_SIZE, MAX_LIQUIDITY_PAGE_SIZE) + const limit = clampPageLimit(filter?.limit, DEFAULT_PAGE_SIZE, MAX_LIQUIDITY_PAGE_SIZE) const providerOps = await this.liquidityProvider.GetOperations(incoming, outgoing, limit) const invoices = await this.BuildLiquidityAssetOperationsPage( providerOps.latestIncomingInvoiceOperations, @@ -504,11 +496,11 @@ export class AdminManager { } const filter = req.lnd_providers.find(p => p.pubkey === provider.provider_pubkey) - const paymentsLimit = clampPageLimit(filter?.limit_payments, DEFAULT_LND_PAGE_SIZE, MAX_LND_PAGE_SIZE) + const paymentsLimit = clampPageLimit(filter?.limit_payments, DEFAULT_LND_PAGE_SIZE, MAX_PAGE_SIZE) const paymentsOffset = filter?.payment_index_offset || 0 - const invoicesLimit = clampPageLimit(filter?.limit_invoices, DEFAULT_LND_PAGE_SIZE, MAX_LND_PAGE_SIZE) + const invoicesLimit = clampPageLimit(filter?.limit_invoices, DEFAULT_LND_PAGE_SIZE, MAX_PAGE_SIZE) const invoicesOffset = filter?.invoice_index_offset || 0 - const txLimit = clampPageLimit(filter?.limit_transactions, DEFAULT_LND_PAGE_SIZE, MAX_LND_PAGE_SIZE) + const txLimit = clampPageLimit(filter?.limit_transactions, DEFAULT_LND_PAGE_SIZE, MAX_PAGE_SIZE) const txOffset = filter?.tx_index_offset || 0 const [payments, invoices, txPages, balance] = await Promise.all([ diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 56576697c..b82f6f685 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -7,6 +7,7 @@ import { OfferPriceType, ndebitEncode, nmanageEncode, nofferEncode } from '@shoc import { getLogger } from '../helpers/logger.js' import SettingsManager from './settingsManager.js' import { assertCallbackUrlAllowed } from '../helpers/safeOutboundFetch.js' +import { clampPageLimit, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '../helpers/pageLimit.js' export default class { storage: Storage @@ -121,7 +122,10 @@ export default class { } async GetUsersAdminInfo(req: Types.UsersAdminInfoRequest): Promise { - const { users, total } = await this.storage.userStorage.GetUsers(req) + const { users, total } = await this.storage.userStorage.GetUsers({ + skip: req.skip, + take: clampPageLimit(req.take, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE), + }) const applications = await this.storage.applicationStorage.GetApplications() const apps: Record = {} diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 149a7066b..531fbedcd 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -23,6 +23,7 @@ import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js' import { LndAddress } from '../lnd/lnd.js' import Metrics from '../metrics/index.js' import { TxPointSettings } from '../storage/tlv/stateBundler.js' +import { clampPageLimit, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '../helpers/pageLimit.js' import { PaymentSideEffects } from './paymentSideEffects.js' interface UserOperationInfo { serial_id: number @@ -1079,13 +1080,14 @@ export default class { if (user.locked && !admin) { throw new Error("user is banned, cannot retrieve operations") } + const maxSize = admin ? clampPageLimit(req.max_size, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE) : req.max_size const [outgoingInvoices, outgoingTransactions, incomingInvoices, incomingTransactions, incomingUserToUser, outgoingUserToUser] = await Promise.all([ - this.storage.paymentStorage.GetUserInvoicePayments(userId, req.latestOutgoingInvoice.id, req.max_size), // - this.storage.paymentStorage.GetUserTransactionPayments(userId, req.latestOutgoingTx.id, req.max_size), - this.storage.paymentStorage.GetUserInvoicesFlaggedAsPaid(user.serial_id, req.latestIncomingInvoice.id, req.latestIncomingInvoice.ts, req.max_size), - this.storage.paymentStorage.GetUserReceivingTransactions(userId, req.latestIncomingTx.id, req.max_size), - this.storage.paymentStorage.GetUserToUserReceivedPayments(userId, req.latestIncomingUserToUserPayment.id, req.max_size), - this.storage.paymentStorage.GetUserToUserSentPayments(userId, req.latestOutgoingUserToUserPayment.id, req.max_size) + this.storage.paymentStorage.GetUserInvoicePayments(userId, req.latestOutgoingInvoice.id, maxSize), // + this.storage.paymentStorage.GetUserTransactionPayments(userId, req.latestOutgoingTx.id, maxSize), + this.storage.paymentStorage.GetUserInvoicesFlaggedAsPaid(user.serial_id, req.latestIncomingInvoice.id, req.latestIncomingInvoice.ts, maxSize), + this.storage.paymentStorage.GetUserReceivingTransactions(userId, req.latestIncomingTx.id, maxSize), + this.storage.paymentStorage.GetUserToUserReceivedPayments(userId, req.latestIncomingUserToUserPayment.id, maxSize), + this.storage.paymentStorage.GetUserToUserSentPayments(userId, req.latestOutgoingUserToUserPayment.id, maxSize) ]) return { latestIncomingInvoiceOperations: this.mapOperations(incomingInvoices, Types.UserOperationType.INCOMING_INVOICE, true), diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 9f4ee031a..62a70bbf5 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -188,8 +188,9 @@ export default (mainHandler: Main): Types.ServerMethods => { return mainHandler.appUserManager.UpdateCallbackUrl(ctx, req) }, GetUserOperations: async ({ ctx, req }) => { - if (req.user_id) delete req.user_id - return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req) + const sanitizedReq = { ...req } + delete sanitizedReq.user_id + return mainHandler.paymentManager.GetUserOperations(ctx.user_id, sanitizedReq) }, GetPaymentState: async ({ ctx, req }) => { diff --git a/src/tests/assetsAndLiabilities.spec.ts b/src/tests/assetsAndLiabilities.spec.ts new file mode 100644 index 000000000..f370bf86d --- /dev/null +++ b/src/tests/assetsAndLiabilities.spec.ts @@ -0,0 +1,144 @@ +import * as Types from "../../proto/autogenerated/ts/types.js" +import { defaultInvoiceExpiry } from "../services/storage/paymentStorage.js" +import Main from "../services/main/index.js" +import { runSanityCheck, safelySetUserBalance, TestBase, TestUserData } from "./testBase.js" +import { initBootstrappedInstance } from "./setupBootstrapped.js" + +export const ignore = false +export const dev = false + +const emptyReq = (): Types.AssetsAndLiabilitiesReq => ({ + lnd_providers: [], + liquidity_providers: [], +}) + +const getAssets = (T: TestBase, req = emptyReq()) => + T.main.adminManager.GetAssetsAndLiabilities(req) + +export default async (T: TestBase) => { + await testUsersBalance(T) + await testLndProviderTracked(T) + await testLndTrackedUserPayment(T) + await testLndPagination(T) + await testLiquidityProviderTracked(T) + await testLiquidityProviderUntrackedOnMain(T) + await runSanityCheck(T) +} + +const testUsersBalance = async (T: TestBase) => { + T.d("starting testUsersBalance") + await safelySetUserBalance(T, T.user1, 2000) + await safelySetUserBalance(T, T.user2, 500) + const res = await getAssets(T) + T.expect(Types.AssetsAndLiabilitiesValidate(res)).to.equal(null) + T.expect(res.users_balance).to.equal(2500) + T.d("users_balance matches total user balances") +} + +const testLndProviderTracked = async (T: TestBase) => { + T.d("starting testLndProviderTracked") + const lndInfo = await T.main.lnd.GetInfo() + const res = await getAssets(T, { + lnd_providers: [{ pubkey: lndInfo.identityPubkey }], + liquidity_providers: [], + }) + const lnd = res.lnds.find(p => p.pubkey === lndInfo.identityPubkey) + T.expect(lnd).to.not.equal(undefined) + T.expect(lnd!.tracked).to.not.equal(undefined) + T.expect(lnd!.tracked!.confirmed_balance).to.be.a("number") + T.expect(lnd!.tracked!.payments.operations).to.be.an("array") + T.expect(lnd!.tracked!.invoices.operations).to.be.an("array") + T.d("lnd provider returns tracked balances and operation pages") +} + +const testLndTrackedUserPayment = async (T: TestBase) => { + T.d("starting testLndTrackedUserPayment") + const invoice = await T.externalAccessToOtherLnd.NewInvoice( + 500, "assets-liabilities", defaultInvoiceExpiry, { from: "system", useProvider: false } + ) + await T.main.appUserManager.PayInvoice( + { app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, + { invoice: invoice.payRequest, amount: 0 } + ) + + const lndInfo = await T.main.lnd.GetInfo() + const res = await getAssets(T, { + lnd_providers: [{ pubkey: lndInfo.identityPubkey, limit_payments: 50 }], + liquidity_providers: [], + }) + const lnd = res.lnds.find(p => p.pubkey === lndInfo.identityPubkey) + const userPayment = lnd!.tracked!.payments.operations.find( + op => op.tracked?.type === Types.TrackedOperationType.USER + && op.tracked.user_id === T.user1.userId + && op.amount === 500 + ) + T.expect(userPayment).to.not.equal(undefined) + T.d("lnd payment is tracked as a user operation") +} + +const testLndPagination = async (T: TestBase) => { + T.d("starting testLndPagination") + const lndInfo = await T.main.lnd.GetInfo() + const res = await getAssets(T, { + lnd_providers: [{ pubkey: lndInfo.identityPubkey, limit_payments: 1 }], + liquidity_providers: [], + }) + const payments = res.lnds[0].tracked!.payments + T.expect(payments.operations.length).to.be.at.most(1) + if (payments.has_more) { + T.expect(payments.next_index_offset).to.be.a("number") + } + T.d("lnd payment pagination respects limit") +} + +const testLiquidityProviderTracked = async (T: TestBase) => { + T.d("starting testLiquidityProviderTracked") + const { bootstrapped, bootstrappedUser, stop } = await initBootstrappedInstance(T) + try { + await inboundPaymentOnBootstrapped(T, bootstrapped, bootstrappedUser) + const res = await bootstrapped.adminManager.GetAssetsAndLiabilities({ + lnd_providers: [], + liquidity_providers: [{ pubkey: T.app.publicKey, limit: 50 }], + }) + const lp = res.liquidity_providers.find(p => p.pubkey === T.app.publicKey) + T.expect(lp).to.not.equal(undefined) + T.expect(lp!.tracked).to.not.equal(undefined) + T.expect(lp!.tracked!.balance).to.equal(1000) + const userInvoice = lp!.tracked!.invoices.operations.find( + op => op.tracked?.type === Types.TrackedOperationType.USER + && op.tracked.user_id === bootstrappedUser.userId + && op.amount === 1000 + ) + T.expect(userInvoice).to.not.equal(undefined) + T.d("liquidity provider returns tracked balance and user invoice") + } finally { + stop() + } +} + +const testLiquidityProviderUntrackedOnMain = async (T: TestBase) => { + T.d("starting testLiquidityProviderUntrackedOnMain") + const { stop } = await initBootstrappedInstance(T) + try { + const res = await getAssets(T, { + lnd_providers: [], + liquidity_providers: [{ pubkey: T.app.publicKey, limit: 50 }], + }) + const lp = res.liquidity_providers.find(p => p.pubkey === T.app.publicKey) + T.expect(lp).to.not.equal(undefined) + T.expect(lp!.tracked).to.equal(undefined) + T.d("main instance does not track remote liquidity provider details") + } finally { + stop() + } +} + +const inboundPaymentOnBootstrapped = async (T: TestBase, bootstrapped: Main, user: TestUserData) => { + const ctx = { app_id: user.appId, user_id: user.userId, app_user_id: user.appUserIdentifier } + const invoiceRes = await bootstrapped.appUserManager.NewInvoice(ctx, { amountSats: 1000, memo: "assetsLiabilities" }) + await T.externalAccessToOtherLnd.PayInvoice( + invoiceRes.invoice, 0, { routingFeeLimit: 100, serviceFee: 100 }, 1000, + { from: "system", useProvider: false } + ) + await new Promise(resolve => setTimeout(resolve, 200)) +} From 9195913798aed40d6ccb6540a1a3d5dc7cccff4d Mon Sep 17 00:00:00 2001 From: boufni95 Date: Tue, 7 Jul 2026 18:18:14 +0000 Subject: [PATCH 14/22] lnd only tests --- src/tests/assetsAndLiabilities.spec.ts | 44 -------------------------- 1 file changed, 44 deletions(-) diff --git a/src/tests/assetsAndLiabilities.spec.ts b/src/tests/assetsAndLiabilities.spec.ts index f370bf86d..1e1b53c78 100644 --- a/src/tests/assetsAndLiabilities.spec.ts +++ b/src/tests/assetsAndLiabilities.spec.ts @@ -20,8 +20,6 @@ export default async (T: TestBase) => { await testLndProviderTracked(T) await testLndTrackedUserPayment(T) await testLndPagination(T) - await testLiquidityProviderTracked(T) - await testLiquidityProviderUntrackedOnMain(T) await runSanityCheck(T) } @@ -91,48 +89,6 @@ const testLndPagination = async (T: TestBase) => { T.d("lnd payment pagination respects limit") } -const testLiquidityProviderTracked = async (T: TestBase) => { - T.d("starting testLiquidityProviderTracked") - const { bootstrapped, bootstrappedUser, stop } = await initBootstrappedInstance(T) - try { - await inboundPaymentOnBootstrapped(T, bootstrapped, bootstrappedUser) - const res = await bootstrapped.adminManager.GetAssetsAndLiabilities({ - lnd_providers: [], - liquidity_providers: [{ pubkey: T.app.publicKey, limit: 50 }], - }) - const lp = res.liquidity_providers.find(p => p.pubkey === T.app.publicKey) - T.expect(lp).to.not.equal(undefined) - T.expect(lp!.tracked).to.not.equal(undefined) - T.expect(lp!.tracked!.balance).to.equal(1000) - const userInvoice = lp!.tracked!.invoices.operations.find( - op => op.tracked?.type === Types.TrackedOperationType.USER - && op.tracked.user_id === bootstrappedUser.userId - && op.amount === 1000 - ) - T.expect(userInvoice).to.not.equal(undefined) - T.d("liquidity provider returns tracked balance and user invoice") - } finally { - stop() - } -} - -const testLiquidityProviderUntrackedOnMain = async (T: TestBase) => { - T.d("starting testLiquidityProviderUntrackedOnMain") - const { stop } = await initBootstrappedInstance(T) - try { - const res = await getAssets(T, { - lnd_providers: [], - liquidity_providers: [{ pubkey: T.app.publicKey, limit: 50 }], - }) - const lp = res.liquidity_providers.find(p => p.pubkey === T.app.publicKey) - T.expect(lp).to.not.equal(undefined) - T.expect(lp!.tracked).to.equal(undefined) - T.d("main instance does not track remote liquidity provider details") - } finally { - stop() - } -} - const inboundPaymentOnBootstrapped = async (T: TestBase, bootstrapped: Main, user: TestUserData) => { const ctx = { app_id: user.appId, user_id: user.userId, app_user_id: user.appUserIdentifier } const invoiceRes = await bootstrapped.appUserManager.NewInvoice(ctx, { amountSats: 1000, memo: "assetsLiabilities" }) From 6df861f61e2f401f9b9d4346195dff4dbf482f75 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Tue, 7 Jul 2026 19:07:52 +0000 Subject: [PATCH 15/22] db interface fix --- .../storage/db/serializationHelpers.ts | 4 +- src/tests/storageOperators.spec.ts | 428 ++++++++++++++++++ 2 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 src/tests/storageOperators.spec.ts diff --git a/src/services/storage/db/serializationHelpers.ts b/src/services/storage/db/serializationHelpers.ts index ee19201f0..ab6a1b0c7 100644 --- a/src/services/storage/db/serializationHelpers.ts +++ b/src/services/storage/db/serializationHelpers.ts @@ -10,7 +10,9 @@ type SerializedFindOperator = { export function serializeFindOperator(operator: FindOperator): SerializedFindOperator { let value: any; if (Array.isArray(operator['value']) && operator['type'] !== 'between') { - value = operator['value'].map(serializeFindOperator); + value = operator['value'].map(item => + item instanceof FindOperator ? serializeFindOperator(item) : item + ); } else if ((operator as any).child !== undefined) { // Not(IsNull()) etc.: TypeORM's .value getter unwraps nested FindOperators, so we'd lose the inner operator. Use .child to serialize the nested operator. value = serializeFindOperator((operator as any).child); diff --git a/src/tests/storageOperators.spec.ts b/src/tests/storageOperators.spec.ts new file mode 100644 index 000000000..2c2f6fdc8 --- /dev/null +++ b/src/tests/storageOperators.spec.ts @@ -0,0 +1,428 @@ +import { + And, Between, Equal, ILike, In, IsNull, LessThan, LessThanOrEqual, + MoreThan, MoreThanOrEqual, Not, Like, +} from 'typeorm' +import { User } from '../services/storage/entity/User.js' +import { Application } from '../services/storage/entity/Application.js' +import { InviteToken } from '../services/storage/entity/InviteToken.js' +import { StorageTestBase } from './testBase.js' +import crypto from 'crypto' + +export const ignore = false +export const dev = false +export const requires = 'storage' + +type SeedData = { + low: User + mid: User + high: User + locked: User + app: Application + inviteWithSats: InviteToken + inviteWithoutSats: InviteToken +} + +const userIds = (users: User[]) => users.map(u => u.user_id).sort() + +export default async (T: StorageTestBase) => { + const seed = await seedData(T) + await testPing(T) + await testComparisonOperators(T, seed) + await testBetweenOperator(T, seed) + await testInOperator(T, seed) + await testEqualAndNotOperators(T, seed) + await testAndOperator(T, seed) + await testLikeOperators(T, seed) + await testIsNullOperator(T, seed) + await testOrConditions(T, seed) + await testFindAndCount(T, seed) + await testSum(T, seed) + await testIncrementDecrement(T, seed) + await testDeleteById(T) + await testRemove(T) + await testDateRoundTrip(T, seed) + await testQueryPagination(T, seed) + await testOperatorsInsideTransaction(T, seed) +} + +const seedData = async (T: StorageTestBase): Promise => { + const suffix = Date.now().toString() + const low = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-low-${suffix}`, + balance_sats: 100, + locked: false, + }) + const mid = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-mid-${suffix}`, + balance_sats: 500, + locked: false, + }) + const high = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-high-${suffix}`, + balance_sats: 1000, + locked: false, + }) + const locked = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-locked-${suffix}`, + balance_sats: 200, + locked: true, + }) + const app = await T.storage.dbs.CreateAndSave('Application', { + app_id: crypto.randomBytes(32).toString('hex'), + name: `op-test-app-${suffix}`, + owner: low, + allow_user_creation: false, + }) + const inviteWithSats = await T.storage.dbs.CreateAndSave('InviteToken', { + inviteToken: `token-with-sats-${suffix}`, + used: false, + sats: 42, + application: app, + }) + const inviteWithoutSats = await T.storage.dbs.CreateAndSave('InviteToken', { + inviteToken: `token-without-sats-${suffix}`, + used: false, + sats: undefined, + application: app, + }) + return { low, mid, high, locked, app, inviteWithSats, inviteWithoutSats } +} + +const testPing = async (T: StorageTestBase) => { + T.d('Starting testPing') + await T.storage.dbs.Ping() + T.d('Finished testPing') +} + +const testComparisonOperators = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testComparisonOperators') + const { low, mid, high, locked } = seed + + const lessThan = await T.storage.dbs.Find('User', { + where: { balance_sats: LessThan(500) }, + }) + T.expect(userIds(lessThan)).to.deep.equal(userIds([low, locked])) + + const moreThan = await T.storage.dbs.Find('User', { + where: { balance_sats: MoreThan(400) }, + }) + T.expect(userIds(moreThan)).to.deep.equal(userIds([mid, high])) + + const lessThanOrEqual = await T.storage.dbs.Find('User', { + where: { balance_sats: LessThanOrEqual(200) }, + }) + T.expect(userIds(lessThanOrEqual)).to.deep.equal(userIds([low, locked])) + + const moreThanOrEqual = await T.storage.dbs.Find('User', { + where: { balance_sats: MoreThanOrEqual(500) }, + }) + T.expect(userIds(moreThanOrEqual)).to.deep.equal(userIds([mid, high])) + + const one = await T.storage.dbs.FindOne('User', { + where: { user_id: low.user_id, balance_sats: LessThan(200) }, + }) + T.expect(one?.user_id).to.equal(low.user_id) + + T.d('Finished testComparisonOperators') +} + +const testBetweenOperator = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testBetweenOperator') + const { low, mid, locked } = seed + + const found = await T.storage.dbs.Find('User', { + where: { balance_sats: Between(100, 500) }, + }) + T.expect(userIds(found)).to.deep.equal(userIds([low, mid, locked])) + + T.d('Finished testBetweenOperator') +} + +const testInOperator = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testInOperator') + const { low, high } = seed + + const found = await T.storage.dbs.Find('User', { + where: { balance_sats: In([100, 1000]) }, + }) + T.expect(userIds(found)).to.deep.equal(userIds([low, high])) + + T.d('Finished testInOperator') +} + +const testEqualAndNotOperators = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testEqualAndNotOperators') + const { low, mid, high, locked } = seed + + const lockedUsers = await T.storage.dbs.Find('User', { + where: { locked: Equal(true) }, + }) + T.expect(userIds(lockedUsers)).to.deep.equal([locked.user_id]) + + const unlocked = await T.storage.dbs.Find('User', { + where: { locked: Not(Equal(true)) }, + }) + T.expect(userIds(unlocked)).to.deep.equal(userIds([low, mid, high])) + + T.d('Finished testEqualAndNotOperators') +} + +const testAndOperator = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testAndOperator') + const { mid } = seed + + const found = await T.storage.dbs.Find('User', { + where: { + balance_sats: And(MoreThan(400), LessThan(600)), + locked: Equal(false), + }, + }) + T.expect(found.length).to.equal(1) + T.expect(found[0].user_id).to.equal(mid.user_id) + + T.d('Finished testAndOperator') +} + +const testLikeOperators = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testLikeOperators') + const { low, mid, high, locked } = seed + + const likeFound = await T.storage.dbs.Find('User', { + where: { user_id: Like('op-test-%') }, + }) + T.expect(userIds(likeFound)).to.deep.equal(userIds([low, mid, high, locked])) + + const ilikeFound = await T.storage.dbs.Find('User', { + where: { user_id: ILike('OP-TEST-LOW-%') }, + }) + T.expect(ilikeFound.length).to.equal(1) + T.expect(ilikeFound[0].user_id).to.equal(low.user_id) + + T.d('Finished testLikeOperators') +} + +const testIsNullOperator = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testIsNullOperator') + const { inviteWithSats, inviteWithoutSats } = seed + + const nullSats = await T.storage.dbs.Find('InviteToken', { + where: { sats: IsNull() }, + }) + T.expect(nullSats.length).to.equal(1) + T.expect(nullSats[0].inviteToken).to.equal(inviteWithoutSats.inviteToken) + + const notNullSats = await T.storage.dbs.Find('InviteToken', { + where: { sats: Not(IsNull()) }, + }) + T.expect(notNullSats.length).to.equal(1) + T.expect(notNullSats[0].inviteToken).to.equal(inviteWithSats.inviteToken) + + T.d('Finished testIsNullOperator') +} + +const testOrConditions = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testOrConditions') + const { low, high } = seed + + const found = await T.storage.dbs.Find('User', { + where: [ + { balance_sats: LessThan(150) }, + { balance_sats: MoreThan(900) }, + ], + }) + T.expect(userIds(found)).to.deep.equal(userIds([low, high])) + + T.d('Finished testOrConditions') +} + +const testFindAndCount = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testFindAndCount') + const { low, mid, high, locked } = seed + + const [users, count] = await T.storage.dbs.FindAndCount('User', { + where: { locked: Equal(false) }, + order: { balance_sats: 'ASC' }, + }) + T.expect(count).to.equal(3) + T.expect(userIds(users)).to.deep.equal(userIds([low, mid, high])) + + T.d('Finished testFindAndCount') +} + +const testSum = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testSum') + const { low, mid, high, locked } = seed + const expected = low.balance_sats + mid.balance_sats + high.balance_sats + locked.balance_sats + + const total = await T.storage.dbs.Sum('User', 'balance_sats', { + user_id: Like('op-test-%'), + }) + T.expect(total).to.equal(expected) + + const unlockedTotal = await T.storage.dbs.Sum('User', 'balance_sats', { + locked: Equal(false), + user_id: Like('op-test-%'), + }) + T.expect(unlockedTotal).to.equal(low.balance_sats + mid.balance_sats + high.balance_sats) + + T.d('Finished testSum') +} + +const testIncrementDecrement = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testIncrementDecrement') + const { low } = seed + + const incAffected = await T.storage.dbs.Increment('User', + { user_id: low.user_id }, + 'balance_sats', + 50, + ) + T.expect(incAffected).to.equal(1) + + const afterInc = await T.storage.dbs.FindOne('User', { + where: { user_id: low.user_id }, + }) + T.expect(afterInc?.balance_sats).to.equal(150) + + const decAffected = await T.storage.dbs.Decrement('User', + { user_id: low.user_id }, + 'balance_sats', + 25, + ) + T.expect(decAffected).to.equal(1) + + const afterDec = await T.storage.dbs.FindOne('User', { + where: { user_id: low.user_id }, + }) + T.expect(afterDec?.balance_sats).to.equal(125) + + T.d('Finished testIncrementDecrement') +} + +const testDeleteById = async (T: StorageTestBase) => { + T.d('Starting testDeleteById') + const user = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-delete-${Date.now()}`, + balance_sats: 1, + locked: false, + }) + + const affected = await T.storage.dbs.Delete('User', user.serial_id) + T.expect(affected).to.equal(1) + + const found = await T.storage.dbs.FindOne('User', { + where: { user_id: user.user_id }, + }) + T.expect(found).to.equal(null) + + T.d('Finished testDeleteById') +} + +const testRemove = async (T: StorageTestBase) => { + T.d('Starting testRemove') + const user = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-remove-${Date.now()}`, + balance_sats: 1, + locked: false, + }) + + const loaded = await T.storage.dbs.FindOne('User', { + where: { user_id: user.user_id }, + }) + T.expect(loaded).to.not.equal(null) + + const removed = await T.storage.dbs.Remove('User', loaded!) + T.expect(removed.user_id).to.equal(user.user_id) + + const found = await T.storage.dbs.FindOne('User', { + where: { user_id: user.user_id }, + }) + T.expect(found).to.equal(null) + + T.d('Finished testRemove') +} + +const testDateRoundTrip = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testDateRoundTrip') + const { low } = seed + + const found = await T.storage.dbs.FindOne('User', { + where: { + user_id: low.user_id, + created_at: MoreThan(new Date(0)), + }, + }) + T.expect(found).to.not.equal(null) + T.expect(found!.created_at).to.be.instanceOf(Date) + T.expect(found!.updated_at).to.be.instanceOf(Date) + + T.d('Finished testDateRoundTrip') +} + +const testQueryPagination = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testQueryPagination') + const { low, mid, high } = seed + + const page = await T.storage.dbs.Find('User', { + where: { locked: Equal(false), user_id: Like('op-test-%') }, + order: { balance_sats: 'ASC' }, + take: 2, + skip: 1, + }) + T.expect(page.length).to.equal(2) + T.expect(page[0].user_id).to.equal(mid.user_id) + T.expect(page[1].user_id).to.equal(high.user_id) + + T.d('Finished testQueryPagination') +} + +const testOperatorsInsideTransaction = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testOperatorsInsideTransaction') + const { mid } = seed + const originalBalance = mid.balance_sats + + await T.storage.StartTransaction(async txId => { + const found = await T.storage.dbs.FindOne('User', { + where: { user_id: mid.user_id, balance_sats: Equal(originalBalance) }, + }, txId) + T.expect(found?.user_id).to.equal(mid.user_id) + + await T.storage.dbs.Update('User', + { user_id: mid.user_id, balance_sats: Equal(originalBalance) }, + { balance_sats: 777 }, + txId, + ) + return null + }, 'storage-operators-tx') + + const updated = await T.storage.dbs.FindOne('User', { + where: { user_id: mid.user_id }, + }) + T.expect(updated?.balance_sats).to.equal(777) + + await T.storage.dbs.Update('User', + { user_id: mid.user_id }, + { balance_sats: originalBalance }, + ) + + try { + await T.storage.StartTransaction(async txId => { + await T.storage.dbs.Update('User', + { user_id: mid.user_id }, + { invalid_column: 'test' } as any, + txId, + ) + return null + }, 'storage-operators-tx-error') + T.expect.fail('Should have thrown an error') + } catch (error) { + T.expect(error).to.not.equal(null) + } + + const afterFailedTx = await T.storage.dbs.FindOne('User', { + where: { user_id: mid.user_id }, + }) + T.expect(afterFailedTx?.balance_sats).to.equal(originalBalance) + + T.d('Finished testOperatorsInsideTransaction') +} From ab16e929765956a9f0329ea12e0d36cfb174f247 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 8 Jul 2026 15:37:23 +0000 Subject: [PATCH 16/22] batch db ops --- src/services/main/adminManager.ts | 286 +++++++++++++--------- src/services/storage/metricsStorage.ts | 8 +- src/services/storage/paymentStorage.ts | 26 +- src/tests/batchAssetLookups.spec.ts | 323 +++++++++++++++++++++++++ 4 files changed, 533 insertions(+), 110 deletions(-) create mode 100644 src/tests/batchAssetLookups.spec.ts diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 25b84889b..5a60222c7 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -7,6 +7,11 @@ import LND from "../lnd/lnd.js"; import SettingsManager from "./settingsManager.js"; import { Swaps } from "../lnd/swaps/swaps.js"; import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"; +import { AddressReceivingTransaction } from "../storage/entity/AddressReceivingTransaction.js"; +import { RootOperation } from "../storage/entity/RootOperation.js"; +import { UserInvoicePayment } from "../storage/entity/UserInvoicePayment.js"; +import { UserReceivingInvoice } from "../storage/entity/UserReceivingInvoice.js"; +import { UserTransactionPayment } from "../storage/entity/UserTransactionPayment.js"; import { TrackedProvider } from "../storage/entity/TrackedProvider.js"; import { NodeInfo } from "../lnd/settings.js"; import { Invoice, Payment, OutputDetail, Transaction, Payment_PaymentStatus, Invoice_InvoiceState } from "../../../proto/lnd/lightning.js"; @@ -414,12 +419,12 @@ export class AdminManager { const invoices = await this.BuildLiquidityAssetOperationsPage( providerOps.latestIncomingInvoiceOperations, limit, - op => this.GetProviderInvoiceAssetOperation(op) + 'receiving_invoice', ) const payments = await this.BuildLiquidityAssetOperationsPage( providerOps.latestOutgoingInvoiceOperations, limit, - op => this.GetProviderPaymentAssetOperation(op) + 'payment', ) const balance = await this.liquidityProvider.GetUserState() return { @@ -435,12 +440,21 @@ export class AdminManager { private async BuildLiquidityAssetOperationsPage( userOps: Types.UserOperations, limit: number, - mapper: (op: Types.UserOperation) => Promise + lookupType: 'payment' | 'receiving_invoice', ): Promise { - const operations: Types.AssetOperation[] = [] - for (const op of userOps.operations) { - operations.push(await mapper(op)) - } + const tracker = await this.BuildAssetOperationTracker({ + paymentInvoices: lookupType === 'payment' ? userOps.operations.map(op => op.identifier) : [], + receivingInvoices: lookupType === 'receiving_invoice' ? userOps.operations.map(op => op.identifier) : [], + txHashes: [], + addressTxKeys: [], + }) + const operations = userOps.operations.map(op => { + const ts = Number(op.paidAtUnix) + const amount = Number(op.amount) + return lookupType === 'payment' + ? tracker.fromPayment(op.identifier, ts, amount) + : tracker.fromReceivingInvoice(op.identifier, ts, amount) + }) const hasMore = userOps.operations.length >= limit return { operations, @@ -449,44 +463,45 @@ export class AdminManager { } } - async GetProviderInvoiceAssetOperation(op: Types.UserOperation): Promise { - const ts = Number(op.paidAtUnix) - const amount = Number(op.amount) - const invoice = op.identifier - const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(invoice) - if (userInvoice) { - const tracked: Types.TrackedOperation = { - ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, - type: USER_OP, user_id: userInvoice.user.user_id - } - return { ts, amount, tracked } - } - const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice", invoice) - if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } - } - return { ts, amount, tracked: undefined } - } - - async GetProviderPaymentAssetOperation(op: Types.UserOperation): Promise { - const ts = Number(op.paidAtUnix) - const amount = Number(op.amount) - const invoice = op.identifier - const userInvoice = await this.storage.paymentStorage.GetPaymentOwner(invoice) - if (userInvoice) { - const tracked: Types.TrackedOperation = { - ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, - type: USER_OP, user_id: userInvoice.user.user_id - } - return { ts, amount, tracked } - } - const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice_payment", invoice) - if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } - } - return { ts, amount, tracked: undefined } + private async BuildAssetOperationTracker(keys: { + paymentInvoices: string[] + receivingInvoices: string[] + txHashes: string[] + addressTxKeys: { address: string, txHash: string, outputIndex: number }[] + }): Promise { + const paymentInvoices = [...new Set(keys.paymentInvoices)] + const receivingInvoices = [...new Set(keys.receivingInvoices)] + const txHashes = [...new Set(keys.txHashes)] + const chainIds = keys.addressTxKeys.map(k => `${k.address}:${k.txHash}:${k.outputIndex}`) + const [ + paymentOwners, + invoiceOwners, + txHashOwners, + addressTxs, + rootInvoicePayments, + rootInvoices, + rootChainPayments, + rootChainOps, + ] = await Promise.all([ + this.storage.paymentStorage.GetPaymentOwners(paymentInvoices), + this.storage.paymentStorage.GetInvoiceOwners(receivingInvoices), + this.storage.paymentStorage.GetTxHashPaymentOwners(txHashes), + this.storage.paymentStorage.GetAddressReceivingTransactionsByTxHashes(txHashes), + this.storage.metricsStorage.GetRootOperationsByIdentifiers('invoice_payment', paymentInvoices), + this.storage.metricsStorage.GetRootOperationsByIdentifiers('invoice', receivingInvoices), + this.storage.metricsStorage.GetRootOperationsByIdentifiers('chain_payment', txHashes), + this.storage.metricsStorage.GetRootOperationsByIdentifiers('chain', chainIds), + ]) + return new AssetOperationTracker( + paymentOwners, + invoiceOwners, + txHashOwners, + addressTxs, + rootInvoicePayments, + rootInvoices, + rootChainPayments, + rootChainOps, + ) } async GetLndAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq, provider: TrackedProvider): Promise { @@ -529,15 +544,20 @@ export class AdminManager { batch: T[], firstIndexOffset: number | undefined, include: (item: T) => boolean, - toAssetOp: (item: T) => Promise, + extractKeys: (items: T[]) => { + paymentInvoices: string[] + receivingInvoices: string[] + }, + toAssetOp: (item: T, tracker: AssetOperationTracker) => Types.AssetOperation, ): Promise { - const operations: Types.AssetOperation[] = [] - for (const item of batch) { - if (!include(item)) { - continue - } - operations.push(await toAssetOp(item)) - } + const included = batch.filter(include) + const keys = extractKeys(included) + const tracker = await this.BuildAssetOperationTracker({ + ...keys, + txHashes: [], + addressTxKeys: [], + }) + const operations = included.map(item => toAssetOp(item, tracker)) const hasMore = batch.length >= limit return { operations, @@ -554,7 +574,12 @@ export class AdminManager { res.payments, firstIndexOffset, payment => payment.status === Payment_PaymentStatus.SUCCEEDED, - payment => this.GetPaymentAssetOperation(payment), + payments => ({ paymentInvoices: payments.map(p => p.paymentRequest), receivingInvoices: [] }), + (payment, tracker) => tracker.fromPayment( + payment.paymentRequest, + Number(payment.creationTimeNs / (BigInt(1000_000_000))), + Number(payment.valueSat), + ), ) } @@ -566,7 +591,12 @@ export class AdminManager { res.invoices, firstIndexOffset, invoice => invoice.state === Invoice_InvoiceState.SETTLED, - invoice => this.GetInvoiceAssetOperation(invoice), + invoices => ({ paymentInvoices: [], receivingInvoices: invoices.map(i => i.paymentRequest) }), + (invoice, tracker) => tracker.fromReceivingInvoice( + invoice.paymentRequest, + Number(invoice.settleDate), + Number(invoice.amtPaidSat), + ), ) } @@ -576,19 +606,35 @@ export class AdminManager { indexOffset: number ): Promise<{ incoming_tx: Types.LndAssetOperationsPage, outgoing_tx: Types.LndAssetOperationsPage }> { const res = await this.lnd.GetTransactions(blockHeight, indexOffset, limit) - const incoming: Types.AssetOperation[] = [] - const outgoing: Types.AssetOperation[] = [] + const incomingItems: { transaction: Transaction, output: OutputDetail }[] = [] + const outgoingTxs: Transaction[] = [] for (const transaction of res.transactions) { for (const output of transaction.outputDetails) { if (output.isOurAddress) { - incoming.push(await this.GetTxOutAssetOperation(transaction, output)) + incomingItems.push({ transaction, output }) } } const input = transaction.previousOutpoints.find(p => p.isOurOutput) if (input) { - outgoing.push(await this.GetTxInAssetOperation(transaction)) + outgoingTxs.push(transaction) } } + const txHashes = [...new Set([ + ...incomingItems.map(({ transaction }) => transaction.txHash), + ...outgoingTxs.map(tx => tx.txHash), + ])] + const tracker = await this.BuildAssetOperationTracker({ + paymentInvoices: [], + receivingInvoices: [], + txHashes, + addressTxKeys: incomingItems.map(({ transaction, output }) => ({ + address: output.address, + txHash: transaction.txHash, + outputIndex: Number(output.outputIndex), + })), + }) + const incoming = incomingItems.map(({ transaction, output }) => tracker.fromTxOut(transaction, output)) + const outgoing = outgoingTxs.map(tx => tracker.fromTxIn(tx)) const hasMore = res.transactions.length >= limit const nextIndexOffset = hasMore && res.transactions.length > 0 ? Number(res.firstIndex) : undefined const toPage = (operations: Types.AssetOperation[]): Types.LndAssetOperationsPage => ({ @@ -602,90 +648,114 @@ export class AdminManager { } } - async GetPaymentAssetOperation(payment: Payment): Promise { - const invoice = payment.paymentRequest - const userInvoice = await this.storage.paymentStorage.GetPaymentOwner(invoice) - const ts = Number(payment.creationTimeNs / (BigInt(1000_000_000))) - const amount = Number(payment.valueSat) - if (userInvoice) { - const tracked: Types.TrackedOperation = { - ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, - type: USER_OP, user_id: userInvoice.user.user_id + async BumpTx(req: Types.BumpTx): Promise { + await this.lnd.BumpFee(req.txid, req.output_index, req.sat_per_vbyte) + } + +} + +class AssetOperationTracker { + private paymentOwnerMap: Map + private invoiceOwnerMap: Map + private txHashOwnerMap: Map + private addressTxMap: Map + private rootInvoicePaymentMap: Map + private rootInvoiceMap: Map + private rootChainPaymentMap: Map + private rootChainMap: Map + + constructor( + paymentOwners: UserInvoicePayment[], + invoiceOwners: UserReceivingInvoice[], + txHashOwners: UserTransactionPayment[], + addressTxs: AddressReceivingTransaction[], + rootInvoicePayments: RootOperation[], + rootInvoices: RootOperation[], + rootChainPayments: RootOperation[], + rootChainOps: RootOperation[], + ) { + this.paymentOwnerMap = new Map(paymentOwners.map(p => [p.invoice, p])) + this.invoiceOwnerMap = new Map(invoiceOwners.map(p => [p.invoice, p])) + this.txHashOwnerMap = new Map(txHashOwners.map(p => [p.tx_hash, p])) + this.addressTxMap = new Map(addressTxs.map(p => [`${p.user_address.address}:${p.tx_hash}`, p])) + this.rootInvoicePaymentMap = new Map(rootInvoicePayments.map(o => [o.operation_identifier, o])) + this.rootInvoiceMap = new Map(rootInvoices.map(o => [o.operation_identifier, o])) + this.rootChainPaymentMap = new Map(rootChainPayments.map(o => [o.operation_identifier, o])) + this.rootChainMap = new Map(rootChainOps.map(o => [o.operation_identifier, o])) + } + + fromPayment(invoice: string, ts: number, amount: number): Types.AssetOperation { + const userPayment = this.paymentOwnerMap.get(invoice) + if (userPayment) { + return { + ts, amount, tracked: { + ts: userPayment.paid_at_unix, amount: userPayment.paid_amount, + type: USER_OP, user_id: userPayment.user.user_id, + }, } - return { ts, amount, tracked } } - const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice_payment", invoice) + const rootOp = this.rootInvoicePaymentMap.get(invoice) if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } + return { ts, amount, tracked: { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } } } return { ts, amount, tracked: undefined } } - async GetInvoiceAssetOperation(invoiceEntry: Invoice): Promise { - const invoice = invoiceEntry.paymentRequest - const ts = Number(invoiceEntry.settleDate) - const amount = Number(invoiceEntry.amtPaidSat) - const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(invoice) + fromReceivingInvoice(invoice: string, ts: number, amount: number): Types.AssetOperation { + const userInvoice = this.invoiceOwnerMap.get(invoice) if (userInvoice) { - const tracked: Types.TrackedOperation = { - ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, - type: USER_OP, user_id: userInvoice.user.user_id + return { + ts, amount, tracked: { + ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, + type: USER_OP, user_id: userInvoice.user.user_id, + }, } - return { ts, amount, tracked } } - const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice", invoice) + const rootOp = this.rootInvoiceMap.get(invoice) if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } + return { ts, amount, tracked: { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } } } return { ts, amount, tracked: undefined } } - async GetTxInAssetOperation(tx: Transaction): Promise { + fromTxIn(tx: Transaction): Types.AssetOperation { const ts = Number(tx.timeStamp) const amount = Number(tx.amount) - const userOp = await this.storage.paymentStorage.GetTxHashPaymentOwner(tx.txHash) + const userOp = this.txHashOwnerMap.get(tx.txHash) if (userOp) { - // user transaction payments are actually deprecated from lnd, but we keep this for consstency - const tracked: Types.TrackedOperation = { - ts: userOp.paid_at_unix, amount: userOp.paid_amount, - type: USER_OP, user_id: userOp.user.user_id + return { + ts, amount, tracked: { + ts: userOp.paid_at_unix, amount: userOp.paid_amount, + type: USER_OP, user_id: userOp.user.user_id, + }, } - return { ts, amount, tracked } } - const rootOp = await this.storage.metricsStorage.GetRootOperation("chain_payment", tx.txHash) + const rootOp = this.rootChainPaymentMap.get(tx.txHash) if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } + return { ts, amount, tracked: { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } } } return { ts, amount, tracked: undefined } } - async GetTxOutAssetOperation(tx: Transaction, output: OutputDetail): Promise { + fromTxOut(tx: Transaction, output: OutputDetail): Types.AssetOperation { const ts = Number(tx.timeStamp) const amount = Number(output.amount) const outputIndex = Number(output.outputIndex) - const userOp = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner(output.address, tx.txHash) + const userOp = this.addressTxMap.get(`${output.address}:${tx.txHash}`) if (userOp) { - const tracked: Types.TrackedOperation = { - ts: userOp.paid_at_unix, amount: userOp.paid_amount, - type: USER_OP, user_id: userOp.user_address.user.user_id + return { + ts, amount, tracked: { + ts: userOp.paid_at_unix, amount: userOp.paid_amount, + type: USER_OP, user_id: userOp.user_address.user.user_id, + }, } - return { ts, amount, tracked } } - const rootOp = await this.storage.metricsStorage.GetRootAddressTransaction(output.address, tx.txHash, outputIndex) + const rootOp = this.rootChainMap.get(`${output.address}:${tx.txHash}:${outputIndex}`) if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } + return { ts, amount, tracked: { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } } } return { ts, amount, tracked: undefined } } - - async BumpTx(req: Types.BumpTx): Promise { - await this.lnd.BumpFee(req.txid, req.output_index, req.sat_per_vbyte) - } - } const getDataPath = (dataDir: string, dataPath: string) => { diff --git a/src/services/storage/metricsStorage.ts b/src/services/storage/metricsStorage.ts index d80d11c95..d85c0e994 100644 --- a/src/services/storage/metricsStorage.ts +++ b/src/services/storage/metricsStorage.ts @@ -1,4 +1,4 @@ -import { Between, DataSource, EntityManager, FindManyOptions, FindOperator, LessThanOrEqual, MoreThanOrEqual } from "typeorm" +import { Between, DataSource, EntityManager, FindManyOptions, FindOperator, In, LessThanOrEqual, MoreThanOrEqual } from "typeorm" import { BalanceEvent } from "./entity/BalanceEvent.js" import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js" import TransactionsQueue from "./db/transactionsQueue.js"; @@ -156,6 +156,12 @@ export default class { async GetRootOperation(opType: RootOperationType, id: string, txId?: string) { return this.dbs.FindOne('RootOperation', { where: { operation_type: opType, operation_identifier: id } }, txId) } + async GetRootOperationsByIdentifiers(opType: RootOperationType, ids: string[], txId?: string) { + if (ids.length === 0) { + return [] + } + return this.dbs.Find('RootOperation', { where: { operation_type: opType, operation_identifier: In(ids) } }, txId) + } async GetRootAddressTransaction(address: string, txHash: string, index: number) { return this.GetRootOperation("chain", `${address}:${txHash}:${index}`) } diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index fc21f1578..5f6f29672 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm" +import { And, Between, Equal, FindOperator, In, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm" import { User } from './entity/User.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; @@ -171,13 +171,37 @@ export default class { async GetTxHashPaymentOwner(txHash: string, txId?: string): Promise { return this.dbs.FindOne('UserTransactionPayment', { where: { tx_hash: txHash } }, txId) } + async GetTxHashPaymentOwners(txHashes: string[], txId?: string): Promise { + if (txHashes.length === 0) { + return [] + } + return this.dbs.Find('UserTransactionPayment', { where: { tx_hash: In(txHashes) } }, txId) + } + async GetAddressReceivingTransactionsByTxHashes(txHashes: string[], txId?: string): Promise { + if (txHashes.length === 0) { + return [] + } + return this.dbs.Find('AddressReceivingTransaction', { where: { tx_hash: In(txHashes) } }, txId) + } async GetInvoiceOwner(paymentRequest: string, txId?: string): Promise { return this.dbs.FindOne('UserReceivingInvoice', { where: { invoice: paymentRequest } }, txId) } + async GetInvoiceOwners(paymentRequests: string[], txId?: string): Promise { + if (paymentRequests.length === 0) { + return [] + } + return this.dbs.Find('UserReceivingInvoice', { where: { invoice: In(paymentRequests) } }, txId) + } async GetPaymentOwner(paymentRequest: string, txId?: string): Promise { return this.dbs.FindOne('UserInvoicePayment', { where: { invoice: paymentRequest } }, txId) } + async GetPaymentOwners(paymentRequests: string[], txId?: string): Promise { + if (paymentRequests.length === 0) { + return [] + } + return this.dbs.Find('UserInvoicePayment', { where: { invoice: In(paymentRequests) } }, txId) + } async GetUser2UserPayment(serialId: number, txId?: string): Promise { return this.dbs.FindOne('UserToUserPayment', { where: { serial_id: serialId } }, txId) } diff --git a/src/tests/batchAssetLookups.spec.ts b/src/tests/batchAssetLookups.spec.ts new file mode 100644 index 000000000..4efae0929 --- /dev/null +++ b/src/tests/batchAssetLookups.spec.ts @@ -0,0 +1,323 @@ +import crypto from 'crypto' +import { User } from '../services/storage/entity/User.js' +import { UserReceivingAddress } from '../services/storage/entity/UserReceivingAddress.js' +import { UserInvoicePayment } from '../services/storage/entity/UserInvoicePayment.js' +import { UserReceivingInvoice } from '../services/storage/entity/UserReceivingInvoice.js' +import { UserTransactionPayment } from '../services/storage/entity/UserTransactionPayment.js' +import { AddressReceivingTransaction } from '../services/storage/entity/AddressReceivingTransaction.js' +import { RootOperation } from '../services/storage/entity/RootOperation.js' +import { StorageTestBase } from './testBase.js' + +export const ignore = false +export const dev = false +export const requires = 'storage' + +type SeedData = { + userA: User + userB: User + addressA: UserReceivingAddress + paymentA: UserInvoicePayment + paymentB: UserInvoicePayment + invoiceA: UserReceivingInvoice + invoiceB: UserReceivingInvoice + txPaymentA: UserTransactionPayment + txPaymentB: UserTransactionPayment + addressTxA: AddressReceivingTransaction + addressTxB: AddressReceivingTransaction + rootInvoicePayment: RootOperation + rootInvoice: RootOperation + rootChainPayment: RootOperation + rootChain: RootOperation +} + +const invoicesFrom = (payments: UserInvoicePayment[]) => + payments.map(p => p.invoice).sort() + +const invoicesFromReceiving = (invoices: UserReceivingInvoice[]) => + invoices.map(i => i.invoice).sort() + +const txHashesFrom = (txs: UserTransactionPayment[]) => + txs.map(t => t.tx_hash).sort() + +export default async (T: StorageTestBase) => { + const seed = await seedData(T) + await testEmptyBatchLookups(T) + await testGetPaymentOwners(T, seed) + await testGetInvoiceOwners(T, seed) + await testGetTxHashPaymentOwners(T, seed) + await testGetAddressReceivingTransactionsByTxHashes(T, seed) + await testGetRootOperationsByIdentifiers(T, seed) +} + +const seedData = async (T: StorageTestBase): Promise => { + const suffix = Date.now().toString() + const userA = await T.storage.dbs.CreateAndSave('User', { + user_id: `batch-asset-user-a-${suffix}`, + balance_sats: 0, + locked: false, + }) + const userB = await T.storage.dbs.CreateAndSave('User', { + user_id: `batch-asset-user-b-${suffix}`, + balance_sats: 0, + locked: false, + }) + const addressA = await T.storage.dbs.CreateAndSave('UserReceivingAddress', { + user: userA, + address: `bcrt1batch${suffix}a`, + callbackUrl: '', + }) + const paymentA = await T.storage.dbs.CreateAndSave('UserInvoicePayment', { + user: userA, + invoice: `lnbc-payment-a-${suffix}`, + paid_amount: 100, + routing_fees: 1, + service_fees: 2, + paid_at_unix: 1_700_000_000, + }) + const paymentB = await T.storage.dbs.CreateAndSave('UserInvoicePayment', { + user: userB, + invoice: `lnbc-payment-b-${suffix}`, + paid_amount: 200, + routing_fees: 2, + service_fees: 3, + paid_at_unix: 1_700_000_100, + }) + const invoiceA = await T.storage.dbs.CreateAndSave('UserReceivingInvoice', { + user: userA, + invoice: `lnbc-invoice-a-${suffix}`, + expires_at_unix: 1_800_000_000, + paid_at_unix: 1_700_000_200, + paid_amount: 300, + }) + const invoiceB = await T.storage.dbs.CreateAndSave('UserReceivingInvoice', { + user: userB, + invoice: `lnbc-invoice-b-${suffix}`, + expires_at_unix: 1_800_000_100, + paid_at_unix: 1_700_000_300, + paid_amount: 400, + }) + const txHashA = crypto.randomBytes(32).toString('hex') + const txHashB = crypto.randomBytes(32).toString('hex') + const txPaymentA = await T.storage.dbs.CreateAndSave('UserTransactionPayment', { + user: userA, + address: addressA.address, + tx_hash: txHashA, + output_index: 0, + paid_amount: 500, + chain_fees: 10, + service_fees: 5, + paid_at_unix: 1_700_000_400, + }) + const txPaymentB = await T.storage.dbs.CreateAndSave('UserTransactionPayment', { + user: userB, + address: `bcrt1batch${suffix}b`, + tx_hash: txHashB, + output_index: 1, + paid_amount: 600, + chain_fees: 11, + service_fees: 6, + paid_at_unix: 1_700_000_500, + }) + const addressTxA = await T.storage.dbs.CreateAndSave('AddressReceivingTransaction', { + user_address: addressA, + tx_hash: txHashA, + output_index: 0, + paid_amount: 700, + service_fee: 7, + paid_at_unix: 1_700_000_600, + }) + const addressTxB = await T.storage.dbs.CreateAndSave('AddressReceivingTransaction', { + user_address: addressA, + tx_hash: txHashB, + output_index: 2, + paid_amount: 800, + service_fee: 8, + paid_at_unix: 1_700_000_700, + }) + const rootInvoicePayment = await T.storage.dbs.CreateAndSave('RootOperation', { + operation_type: 'invoice_payment', + operation_amount: 900, + operation_identifier: paymentA.invoice, + at_unix: 1_700_000_800, + }) + const rootInvoice = await T.storage.dbs.CreateAndSave('RootOperation', { + operation_type: 'invoice', + operation_amount: 1000, + operation_identifier: invoiceA.invoice, + at_unix: 1_700_000_900, + }) + const rootChainPayment = await T.storage.dbs.CreateAndSave('RootOperation', { + operation_type: 'chain_payment', + operation_amount: 1100, + operation_identifier: txHashA, + at_unix: 1_700_001_000, + }) + const rootChain = await T.storage.dbs.CreateAndSave('RootOperation', { + operation_type: 'chain', + operation_amount: 1200, + operation_identifier: `${addressA.address}:${txHashA}:0`, + at_unix: 1_700_001_100, + }) + return { + userA, + userB, + addressA, + paymentA, + paymentB, + invoiceA, + invoiceB, + txPaymentA, + txPaymentB, + addressTxA, + addressTxB, + rootInvoicePayment, + rootInvoice, + rootChainPayment, + rootChain, + } +} + +const testEmptyBatchLookups = async (T: StorageTestBase) => { + T.d('Starting testEmptyBatchLookups') + const paymentStorage = T.storage.paymentStorage + const metricsStorage = T.storage.metricsStorage + + T.expect(await paymentStorage.GetPaymentOwners([])).to.deep.equal([]) + T.expect(await paymentStorage.GetInvoiceOwners([])).to.deep.equal([]) + T.expect(await paymentStorage.GetTxHashPaymentOwners([])).to.deep.equal([]) + T.expect(await paymentStorage.GetAddressReceivingTransactionsByTxHashes([])).to.deep.equal([]) + T.expect(await metricsStorage.GetRootOperationsByIdentifiers('invoice_payment', [])).to.deep.equal([]) + T.expect(await metricsStorage.GetRootOperationsByIdentifiers('invoice', [])).to.deep.equal([]) + T.expect(await metricsStorage.GetRootOperationsByIdentifiers('chain_payment', [])).to.deep.equal([]) + T.expect(await metricsStorage.GetRootOperationsByIdentifiers('chain', [])).to.deep.equal([]) + + T.d('Finished testEmptyBatchLookups') +} + +const testGetPaymentOwners = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetPaymentOwners') + const { paymentA, paymentB, userA, userB } = seed + + const found = await T.storage.paymentStorage.GetPaymentOwners([ + paymentA.invoice, + paymentB.invoice, + 'lnbc-missing-invoice', + ]) + T.expect(invoicesFrom(found)).to.deep.equal(invoicesFrom([paymentA, paymentB])) + + const byUser = new Map(found.map(p => [p.invoice, p.user.user_id])) + T.expect(byUser.get(paymentA.invoice)).to.equal(userA.user_id) + T.expect(byUser.get(paymentB.invoice)).to.equal(userB.user_id) + + const single = await T.storage.paymentStorage.GetPaymentOwners([paymentA.invoice]) + T.expect(single.length).to.equal(1) + T.expect(single[0].invoice).to.equal(paymentA.invoice) + + T.d('Finished testGetPaymentOwners') +} + +const testGetInvoiceOwners = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetInvoiceOwners') + const { invoiceA, invoiceB, userA, userB } = seed + + const found = await T.storage.paymentStorage.GetInvoiceOwners([ + invoiceB.invoice, + invoiceA.invoice, + 'lnbc-missing-invoice', + ]) + T.expect(invoicesFromReceiving(found)).to.deep.equal(invoicesFromReceiving([invoiceA, invoiceB])) + + const byUser = new Map(found.map(i => [i.invoice, i.user.user_id])) + T.expect(byUser.get(invoiceA.invoice)).to.equal(userA.user_id) + T.expect(byUser.get(invoiceB.invoice)).to.equal(userB.user_id) + + T.d('Finished testGetInvoiceOwners') +} + +const testGetTxHashPaymentOwners = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetTxHashPaymentOwners') + const { txPaymentA, txPaymentB, userA, userB } = seed + + const found = await T.storage.paymentStorage.GetTxHashPaymentOwners([ + txPaymentB.tx_hash, + txPaymentA.tx_hash, + crypto.randomBytes(32).toString('hex'), + ]) + T.expect(txHashesFrom(found)).to.deep.equal(txHashesFrom([txPaymentA, txPaymentB])) + + const byUser = new Map(found.map(t => [t.tx_hash, t.user.user_id])) + T.expect(byUser.get(txPaymentA.tx_hash)).to.equal(userA.user_id) + T.expect(byUser.get(txPaymentB.tx_hash)).to.equal(userB.user_id) + + T.d('Finished testGetTxHashPaymentOwners') +} + +const testGetAddressReceivingTransactionsByTxHashes = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetAddressReceivingTransactionsByTxHashes') + const { addressTxA, addressTxB, userA } = seed + + const found = await T.storage.paymentStorage.GetAddressReceivingTransactionsByTxHashes([ + addressTxB.tx_hash, + addressTxA.tx_hash, + crypto.randomBytes(32).toString('hex'), + ]) + T.expect(found.length).to.equal(2) + + const byTxHash = new Map(found.map(t => [t.tx_hash, t])) + T.expect(byTxHash.get(addressTxA.tx_hash)?.paid_amount).to.equal(addressTxA.paid_amount) + T.expect(byTxHash.get(addressTxB.tx_hash)?.paid_amount).to.equal(addressTxB.paid_amount) + T.expect(byTxHash.get(addressTxA.tx_hash)?.user_address.user.user_id).to.equal(userA.user_id) + + T.d('Finished testGetAddressReceivingTransactionsByTxHashes') +} + +const testGetRootOperationsByIdentifiers = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetRootOperationsByIdentifiers') + const { + rootInvoicePayment, + rootInvoice, + rootChainPayment, + rootChain, + paymentA, + invoiceA, + txPaymentA, + addressA, + } = seed + + const invoicePayments = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'invoice_payment', + [paymentA.invoice, 'missing-invoice-payment'], + ) + T.expect(invoicePayments.length).to.equal(1) + T.expect(invoicePayments[0].operation_identifier).to.equal(rootInvoicePayment.operation_identifier) + T.expect(invoicePayments[0].operation_amount).to.equal(rootInvoicePayment.operation_amount) + + const invoices = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'invoice', + [invoiceA.invoice], + ) + T.expect(invoices.length).to.equal(1) + T.expect(invoices[0].operation_identifier).to.equal(rootInvoice.operation_identifier) + + const chainPayments = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'chain_payment', + [txPaymentA.tx_hash], + ) + T.expect(chainPayments.length).to.equal(1) + T.expect(chainPayments[0].operation_identifier).to.equal(rootChainPayment.operation_identifier) + + const chainOps = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'chain', + [`${addressA.address}:${txPaymentA.tx_hash}:0`, 'addr:tx:99'], + ) + T.expect(chainOps.length).to.equal(1) + T.expect(chainOps[0].operation_identifier).to.equal(rootChain.operation_identifier) + + const wrongType = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'invoice', + [paymentA.invoice], + ) + T.expect(wrongType.length).to.equal(0) + + T.d('Finished testGetRootOperationsByIdentifiers') +} From f160f845d5e9a4a8bd2683ed4c9a05cd47128565 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 8 Jul 2026 15:57:24 +0000 Subject: [PATCH 17/22] fix err --- src/tests/batchAssetLookups.spec.ts | 36 ++++++++++------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/tests/batchAssetLookups.spec.ts b/src/tests/batchAssetLookups.spec.ts index 4efae0929..3f09aa0ba 100644 --- a/src/tests/batchAssetLookups.spec.ts +++ b/src/tests/batchAssetLookups.spec.ts @@ -134,30 +134,18 @@ const seedData = async (T: StorageTestBase): Promise => { service_fee: 8, paid_at_unix: 1_700_000_700, }) - const rootInvoicePayment = await T.storage.dbs.CreateAndSave('RootOperation', { - operation_type: 'invoice_payment', - operation_amount: 900, - operation_identifier: paymentA.invoice, - at_unix: 1_700_000_800, - }) - const rootInvoice = await T.storage.dbs.CreateAndSave('RootOperation', { - operation_type: 'invoice', - operation_amount: 1000, - operation_identifier: invoiceA.invoice, - at_unix: 1_700_000_900, - }) - const rootChainPayment = await T.storage.dbs.CreateAndSave('RootOperation', { - operation_type: 'chain_payment', - operation_amount: 1100, - operation_identifier: txHashA, - at_unix: 1_700_001_000, - }) - const rootChain = await T.storage.dbs.CreateAndSave('RootOperation', { - operation_type: 'chain', - operation_amount: 1200, - operation_identifier: `${addressA.address}:${txHashA}:0`, - at_unix: 1_700_001_100, - }) + const rootInvoicePayment = await T.storage.metricsStorage.AddRootOperation( + 'invoice_payment', paymentA.invoice, 900, + ) + const rootInvoice = await T.storage.metricsStorage.AddRootOperation( + 'invoice', invoiceA.invoice, 1000, + ) + const rootChainPayment = await T.storage.metricsStorage.AddRootOperation( + 'chain_payment', txHashA, 1100, + ) + const rootChain = await T.storage.metricsStorage.AddRootOperation( + 'chain', `${addressA.address}:${txHashA}:0`, 1200, + ) return { userA, userB, From e2818941939c9829b0813ffc3139ca00c71a5bf7 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 13 Jul 2026 15:57:25 +0000 Subject: [PATCH 18/22] asset liabilities v2 --- proto/autogenerated/client.md | 44 ++++ proto/autogenerated/go/http_client.go | 30 +++ proto/autogenerated/go/types.go | 32 +++ proto/autogenerated/ts/express_server.ts | 22 ++ proto/autogenerated/ts/http_client.ts | 14 ++ proto/autogenerated/ts/nostr_client.ts | 15 ++ proto/autogenerated/ts/nostr_transport.ts | 16 ++ proto/autogenerated/ts/types.ts | 253 +++++++++++++++++++++- proto/service/methods.proto | 7 + proto/service/structs.proto | 40 +++- src/services/main/adminManager.ts | 63 +++++- src/services/serverMethods/index.ts | 3 + src/tests/assetsAndLiabilities.spec.ts | 26 ++- 13 files changed, 546 insertions(+), 19 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index e476f5366..d97294cef 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -118,6 +118,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq) - output: [AssetsAndLiabilities](#AssetsAndLiabilities) +- GetAssetsAndLiabilitiesV2 + - auth type: __Admin__ + - input: [AssetsAndLiabilitiesReqV2](#AssetsAndLiabilitiesReqV2) + - output: [AssetsAndLiabilitiesV2](#AssetsAndLiabilitiesV2) + - GetBundleMetrics - auth type: __Metrics__ - input: [LatestBundleMetricReq](#LatestBundleMetricReq) @@ -636,6 +641,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq) - output: [AssetsAndLiabilities](#AssetsAndLiabilities) +- GetAssetsAndLiabilitiesV2 + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/assets/liabilities/v2__ + - input: [AssetsAndLiabilitiesReqV2](#AssetsAndLiabilitiesReqV2) + - output: [AssetsAndLiabilitiesV2](#AssetsAndLiabilitiesV2) + - GetBundleMetrics - auth type: __Metrics__ - http method: __post__ @@ -1251,9 +1263,19 @@ The nostr server will send back a message response, and inside the body there wi - __users_balance__: _number_ ### AssetsAndLiabilitiesReq + - __limit_invoices__: _number_ *this field is optional + - __limit_payments__: _number_ *this field is optional + - __limit_providers__: _number_ *this field is optional + +### AssetsAndLiabilitiesReqV2 - __liquidity_providers__: ARRAY of: _[LiquidityProviderFilter](#LiquidityProviderFilter)_ - __lnd_providers__: ARRAY of: _[LndProviderFilter](#LndProviderFilter)_ +### AssetsAndLiabilitiesV2 + - __liquidity_providers__: ARRAY of: _[LiquidityAssetProviderV2](#LiquidityAssetProviderV2)_ + - __lnds__: ARRAY of: _[LndAssetProviderV2](#LndAssetProviderV2)_ + - __users_balance__: _number_ + ### AuthApp - __app__: _[Application](#Application)_ - __auth_token__: _string_ @@ -1508,6 +1530,10 @@ The nostr server will send back a message response, and inside the body there wi - __pubkey__: _string_ - __tracked__: _[TrackedLiquidityProvider](#TrackedLiquidityProvider)_ *this field is optional +### LiquidityAssetProviderV2 + - __pubkey__: _string_ + - __tracked__: _[TrackedLiquidityProviderV2](#TrackedLiquidityProviderV2)_ *this field is optional + ### LiquidityProviderFilter - __latestIncomingInvoice__: _[OperationsCursor](#OperationsCursor)_ *this field is optional - __latestOutgoingInvoice__: _[OperationsCursor](#OperationsCursor)_ *this field is optional @@ -1537,6 +1563,10 @@ The nostr server will send back a message response, and inside the body there wi - __pubkey__: _string_ - __tracked__: _[TrackedLndProvider](#TrackedLndProvider)_ *this field is optional +### LndAssetProviderV2 + - __pubkey__: _string_ + - __tracked__: _[TrackedLndProviderV2](#TrackedLndProviderV2)_ *this field is optional + ### LndChannels - __open_channels__: ARRAY of: _[OpenChannel](#OpenChannel)_ @@ -1870,11 +1900,25 @@ The nostr server will send back a message response, and inside the body there wi - __request_id__: _number_ *this field is optional ### TrackedLiquidityProvider + - __balance__: _number_ + - __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_ + +### TrackedLiquidityProviderV2 - __balance__: _number_ - __invoices__: _[LiquidityAssetOperationsPage](#LiquidityAssetOperationsPage)_ - __payments__: _[LiquidityAssetOperationsPage](#LiquidityAssetOperationsPage)_ ### TrackedLndProvider + - __channels_balance__: _number_ + - __confirmed_balance__: _number_ + - __incoming_tx__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __outgoing_tx__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __unconfirmed_balance__: _number_ + +### TrackedLndProviderV2 - __channels_balance__: _number_ - __confirmed_balance__: _number_ - __incoming_tx__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index e0794f738..88d833048 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -82,6 +82,7 @@ type Client struct { GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) GetAssetsAndLiabilities func(req AssetsAndLiabilitiesReq) (*AssetsAndLiabilities, error) + GetAssetsAndLiabilitiesV2 func(req AssetsAndLiabilitiesReqV2) (*AssetsAndLiabilitiesV2, error) GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) GetDebitAuthorizations func() (*DebitAuthorizations, error) GetErrorStats func() (*ErrorStats, error) @@ -899,6 +900,35 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + GetAssetsAndLiabilitiesV2: func(req AssetsAndLiabilitiesReqV2) (*AssetsAndLiabilitiesV2, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/assets/liabilities/v2" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := AssetsAndLiabilitiesV2{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, GetBundleMetrics: func(req LatestBundleMetricReq) (*BundleMetrics, error) { auth, err := params.RetrieveMetricsAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 157c25803..b9b872419 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -187,9 +187,19 @@ type AssetsAndLiabilities struct { Users_balance int64 `json:"users_balance"` } type AssetsAndLiabilitiesReq struct { + Limit_invoices int64 `json:"limit_invoices"` + Limit_payments int64 `json:"limit_payments"` + Limit_providers int64 `json:"limit_providers"` +} +type AssetsAndLiabilitiesReqV2 struct { Liquidity_providers []LiquidityProviderFilter `json:"liquidity_providers"` Lnd_providers []LndProviderFilter `json:"lnd_providers"` } +type AssetsAndLiabilitiesV2 struct { + Liquidity_providers []LiquidityAssetProviderV2 `json:"liquidity_providers"` + Lnds []LndAssetProviderV2 `json:"lnds"` + Users_balance int64 `json:"users_balance"` +} type AuthApp struct { App *Application `json:"app"` Auth_token string `json:"auth_token"` @@ -444,6 +454,10 @@ type LiquidityAssetProvider struct { Pubkey string `json:"pubkey"` Tracked *TrackedLiquidityProvider `json:"tracked"` } +type LiquidityAssetProviderV2 struct { + Pubkey string `json:"pubkey"` + Tracked *TrackedLiquidityProviderV2 `json:"tracked"` +} type LiquidityProviderFilter struct { Latestincominginvoice *OperationsCursor `json:"latestIncomingInvoice"` Latestoutgoinginvoice *OperationsCursor `json:"latestOutgoingInvoice"` @@ -473,6 +487,10 @@ type LndAssetProvider struct { Pubkey string `json:"pubkey"` Tracked *TrackedLndProvider `json:"tracked"` } +type LndAssetProviderV2 struct { + Pubkey string `json:"pubkey"` + Tracked *TrackedLndProviderV2 `json:"tracked"` +} type LndChannels struct { Open_channels []OpenChannel `json:"open_channels"` } @@ -806,11 +824,25 @@ type SingleMetricReq struct { Request_id int64 `json:"request_id"` } type TrackedLiquidityProvider struct { + Balance int64 `json:"balance"` + Invoices []AssetOperation `json:"invoices"` + Payments []AssetOperation `json:"payments"` +} +type TrackedLiquidityProviderV2 struct { Balance int64 `json:"balance"` Invoices *LiquidityAssetOperationsPage `json:"invoices"` Payments *LiquidityAssetOperationsPage `json:"payments"` } type TrackedLndProvider struct { + Channels_balance int64 `json:"channels_balance"` + Confirmed_balance int64 `json:"confirmed_balance"` + Incoming_tx []AssetOperation `json:"incoming_tx"` + Invoices []AssetOperation `json:"invoices"` + Outgoing_tx []AssetOperation `json:"outgoing_tx"` + Payments []AssetOperation `json:"payments"` + Unconfirmed_balance int64 `json:"unconfirmed_balance"` +} +type TrackedLndProviderV2 struct { Channels_balance int64 `json:"channels_balance"` Confirmed_balance int64 `json:"confirmed_balance"` Incoming_tx *LndAssetOperationsPage `json:"incoming_tx"` diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index c14292c6a..2e8315eb3 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -1042,6 +1042,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.GetAssetsAndLiabilitiesV2) throw new Error('method: GetAssetsAndLiabilitiesV2 is not implemented') + app.post('/api/admin/assets/liabilities/v2', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetAssetsAndLiabilitiesV2', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetAssetsAndLiabilitiesV2) throw new Error('method: GetAssetsAndLiabilitiesV2 is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.AssetsAndLiabilitiesReqV2Validate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.GetAssetsAndLiabilitiesV2({rpcName:'GetAssetsAndLiabilitiesV2', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented') app.post('/api/reports/bundle', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'GetBundleMetrics', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 7d4696da6..bf0453c83 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -382,6 +382,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAssetsAndLiabilitiesV2: async (request: Types.AssetsAndLiabilitiesReqV2): Promise => { + const auth = await params.retrieveAdminAuth() + if (auth === null) throw new Error('retrieveAdminAuth() returned null') + let finalRoute = '/api/admin/assets/liabilities/v2' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.AssetsAndLiabilitiesV2Validate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetBundleMetrics: async (request: Types.LatestBundleMetricReq): Promise => { const auth = await params.retrieveMetricsAuth() if (auth === null) throw new Error('retrieveMetricsAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index d08c58f57..a14681b4c 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -302,6 +302,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAssetsAndLiabilitiesV2: async (request: Types.AssetsAndLiabilitiesReqV2): Promise => { + const auth = await params.retrieveNostrAdminAuth() + if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + const data = await send(params.pubDestination, {rpcName:'GetAssetsAndLiabilitiesV2',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.AssetsAndLiabilitiesV2Validate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetBundleMetrics: async (request: Types.LatestBundleMetricReq): Promise => { const auth = await params.retrieveNostrMetricsAuth() if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index 905c51c59..1d1fd20e7 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -767,6 +767,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'GetAssetsAndLiabilitiesV2': + try { + if (!methods.GetAssetsAndLiabilitiesV2) throw new Error('method: GetAssetsAndLiabilitiesV2 is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.AssetsAndLiabilitiesReqV2Validate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.GetAssetsAndLiabilitiesV2({rpcName:'GetAssetsAndLiabilitiesV2', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'GetBundleMetrics': try { if (!methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index e8ab0ab1f..191642c6b 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: export type AdminContext = { admin_id: string } -export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | BumpTx_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminInvoiceSwapQuotes_Input | GetAdminTransactionSwapQuotes_Input | GetAssetsAndLiabilities_Input | GetInviteLinkState_Input | GetSeed_Input | GetUserOperationsFromAdmin_Input | GetUsersAdminInfo_Input | ListAdminInvoiceSwaps_Input | ListAdminTxSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminInvoiceSwap_Input | PayAdminTransactionSwap_Input | RefundAdminInvoiceSwap_Input | UpdateChannelPolicy_Input -export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | BumpTx_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminInvoiceSwapQuotes_Output | GetAdminTransactionSwapQuotes_Output | GetAssetsAndLiabilities_Output | GetInviteLinkState_Output | GetSeed_Output | GetUserOperationsFromAdmin_Output | GetUsersAdminInfo_Output | ListAdminInvoiceSwaps_Output | ListAdminTxSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminInvoiceSwap_Output | PayAdminTransactionSwap_Output | RefundAdminInvoiceSwap_Output | UpdateChannelPolicy_Output +export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | BumpTx_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminInvoiceSwapQuotes_Input | GetAdminTransactionSwapQuotes_Input | GetAssetsAndLiabilities_Input | GetAssetsAndLiabilitiesV2_Input | GetInviteLinkState_Input | GetSeed_Input | GetUserOperationsFromAdmin_Input | GetUsersAdminInfo_Input | ListAdminInvoiceSwaps_Input | ListAdminTxSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminInvoiceSwap_Input | PayAdminTransactionSwap_Input | RefundAdminInvoiceSwap_Input | UpdateChannelPolicy_Input +export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | BumpTx_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminInvoiceSwapQuotes_Output | GetAdminTransactionSwapQuotes_Output | GetAssetsAndLiabilities_Output | GetAssetsAndLiabilitiesV2_Output | GetInviteLinkState_Output | GetSeed_Output | GetUserOperationsFromAdmin_Output | GetUsersAdminInfo_Output | ListAdminInvoiceSwaps_Output | ListAdminTxSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminInvoiceSwap_Output | PayAdminTransactionSwap_Output | RefundAdminInvoiceSwap_Output | UpdateChannelPolicy_Output export type AppContext = { app_id: string } @@ -123,6 +123,9 @@ export type GetAppsMetrics_Output = ResultError | ({ status: 'OK' } & AppsMetric export type GetAssetsAndLiabilities_Input = {rpcName:'GetAssetsAndLiabilities', req: AssetsAndLiabilitiesReq} export type GetAssetsAndLiabilities_Output = ResultError | ({ status: 'OK' } & AssetsAndLiabilities) +export type GetAssetsAndLiabilitiesV2_Input = {rpcName:'GetAssetsAndLiabilitiesV2', req: AssetsAndLiabilitiesReqV2} +export type GetAssetsAndLiabilitiesV2_Output = ResultError | ({ status: 'OK' } & AssetsAndLiabilitiesV2) + export type GetBundleMetrics_Input = {rpcName:'GetBundleMetrics', req: LatestBundleMetricReq} export type GetBundleMetrics_Output = ResultError | ({ status: 'OK' } & BundleMetrics) @@ -389,6 +392,7 @@ export type ServerMethods = { GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise GetAppsMetrics?: (req: GetAppsMetrics_Input & {ctx: MetricsContext }) => Promise GetAssetsAndLiabilities?: (req: GetAssetsAndLiabilities_Input & {ctx: AdminContext }) => Promise + GetAssetsAndLiabilitiesV2?: (req: GetAssetsAndLiabilitiesV2_Input & {ctx: AdminContext }) => Promise GetBundleMetrics?: (req: GetBundleMetrics_Input & {ctx: MetricsContext }) => Promise GetDebitAuthorizations?: (req: GetDebitAuthorizations_Input & {ctx: UserContext }) => Promise GetErrorStats?: (req: GetErrorStats_Input & {ctx: MetricsContext }) => Promise @@ -1057,18 +1061,47 @@ export const AssetsAndLiabilitiesValidate = (o?: AssetsAndLiabilities, opts: Ass } export type AssetsAndLiabilitiesReq = { + limit_invoices?: number + limit_payments?: number + limit_providers?: number +} +export type AssetsAndLiabilitiesReqOptionalField = 'limit_invoices' | 'limit_payments' | 'limit_providers' +export const AssetsAndLiabilitiesReqOptionalFields: AssetsAndLiabilitiesReqOptionalField[] = ['limit_invoices', 'limit_payments', 'limit_providers'] +export type AssetsAndLiabilitiesReqOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: AssetsAndLiabilitiesReqOptionalField[] + limit_invoices_CustomCheck?: (v?: number) => boolean + limit_payments_CustomCheck?: (v?: number) => boolean + limit_providers_CustomCheck?: (v?: number) => boolean +} +export const AssetsAndLiabilitiesReqValidate = (o?: AssetsAndLiabilitiesReq, opts: AssetsAndLiabilitiesReqOptions = {}, path: string = 'AssetsAndLiabilitiesReq::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.limit_invoices || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_invoices')) && typeof o.limit_invoices !== 'number') return new Error(`${path}.limit_invoices: is not a number`) + if (opts.limit_invoices_CustomCheck && !opts.limit_invoices_CustomCheck(o.limit_invoices)) return new Error(`${path}.limit_invoices: custom check failed`) + + if ((o.limit_payments || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_payments')) && typeof o.limit_payments !== 'number') return new Error(`${path}.limit_payments: is not a number`) + if (opts.limit_payments_CustomCheck && !opts.limit_payments_CustomCheck(o.limit_payments)) return new Error(`${path}.limit_payments: custom check failed`) + + if ((o.limit_providers || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_providers')) && typeof o.limit_providers !== 'number') return new Error(`${path}.limit_providers: is not a number`) + if (opts.limit_providers_CustomCheck && !opts.limit_providers_CustomCheck(o.limit_providers)) return new Error(`${path}.limit_providers: custom check failed`) + + return null +} + +export type AssetsAndLiabilitiesReqV2 = { liquidity_providers: LiquidityProviderFilter[] lnd_providers: LndProviderFilter[] } -export const AssetsAndLiabilitiesReqOptionalFields: [] = [] -export type AssetsAndLiabilitiesReqOptions = OptionsBaseMessage & { +export const AssetsAndLiabilitiesReqV2OptionalFields: [] = [] +export type AssetsAndLiabilitiesReqV2Options = OptionsBaseMessage & { checkOptionalsAreSet?: [] liquidity_providers_ItemOptions?: LiquidityProviderFilterOptions liquidity_providers_CustomCheck?: (v: LiquidityProviderFilter[]) => boolean lnd_providers_ItemOptions?: LndProviderFilterOptions lnd_providers_CustomCheck?: (v: LndProviderFilter[]) => boolean } -export const AssetsAndLiabilitiesReqValidate = (o?: AssetsAndLiabilitiesReq, opts: AssetsAndLiabilitiesReqOptions = {}, path: string = 'AssetsAndLiabilitiesReq::root.'): Error | null => { +export const AssetsAndLiabilitiesReqV2Validate = (o?: AssetsAndLiabilitiesReqV2, opts: AssetsAndLiabilitiesReqV2Options = {}, path: string = 'AssetsAndLiabilitiesReqV2::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') @@ -1089,6 +1122,44 @@ export const AssetsAndLiabilitiesReqValidate = (o?: AssetsAndLiabilitiesReq, opt return null } +export type AssetsAndLiabilitiesV2 = { + liquidity_providers: LiquidityAssetProviderV2[] + lnds: LndAssetProviderV2[] + users_balance: number +} +export const AssetsAndLiabilitiesV2OptionalFields: [] = [] +export type AssetsAndLiabilitiesV2Options = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + liquidity_providers_ItemOptions?: LiquidityAssetProviderV2Options + liquidity_providers_CustomCheck?: (v: LiquidityAssetProviderV2[]) => boolean + lnds_ItemOptions?: LndAssetProviderV2Options + lnds_CustomCheck?: (v: LndAssetProviderV2[]) => boolean + users_balance_CustomCheck?: (v: number) => boolean +} +export const AssetsAndLiabilitiesV2Validate = (o?: AssetsAndLiabilitiesV2, opts: AssetsAndLiabilitiesV2Options = {}, path: string = 'AssetsAndLiabilitiesV2::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (!Array.isArray(o.liquidity_providers)) return new Error(`${path}.liquidity_providers: is not an array`) + for (let index = 0; index < o.liquidity_providers.length; index++) { + const liquidity_providersErr = LiquidityAssetProviderV2Validate(o.liquidity_providers[index], opts.liquidity_providers_ItemOptions, `${path}.liquidity_providers[${index}]`) + if (liquidity_providersErr !== null) return liquidity_providersErr + } + if (opts.liquidity_providers_CustomCheck && !opts.liquidity_providers_CustomCheck(o.liquidity_providers)) return new Error(`${path}.liquidity_providers: custom check failed`) + + if (!Array.isArray(o.lnds)) return new Error(`${path}.lnds: is not an array`) + for (let index = 0; index < o.lnds.length; index++) { + const lndsErr = LndAssetProviderV2Validate(o.lnds[index], opts.lnds_ItemOptions, `${path}.lnds[${index}]`) + if (lndsErr !== null) return lndsErr + } + if (opts.lnds_CustomCheck && !opts.lnds_CustomCheck(o.lnds)) return new Error(`${path}.lnds: custom check failed`) + + if (typeof o.users_balance !== 'number') return new Error(`${path}.users_balance: is not a number`) + if (opts.users_balance_CustomCheck && !opts.users_balance_CustomCheck(o.users_balance)) return new Error(`${path}.users_balance: custom check failed`) + + return null +} + export type AuthApp = { app: Application auth_token: string @@ -2637,6 +2708,33 @@ export const LiquidityAssetProviderValidate = (o?: LiquidityAssetProvider, opts: return null } +export type LiquidityAssetProviderV2 = { + pubkey: string + tracked?: TrackedLiquidityProviderV2 +} +export type LiquidityAssetProviderV2OptionalField = 'tracked' +export const LiquidityAssetProviderV2OptionalFields: LiquidityAssetProviderV2OptionalField[] = ['tracked'] +export type LiquidityAssetProviderV2Options = OptionsBaseMessage & { + checkOptionalsAreSet?: LiquidityAssetProviderV2OptionalField[] + pubkey_CustomCheck?: (v: string) => boolean + tracked_Options?: TrackedLiquidityProviderV2Options +} +export const LiquidityAssetProviderV2Validate = (o?: LiquidityAssetProviderV2, opts: LiquidityAssetProviderV2Options = {}, path: string = 'LiquidityAssetProviderV2::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.pubkey !== 'string') return new Error(`${path}.pubkey: is not a string`) + if (opts.pubkey_CustomCheck && !opts.pubkey_CustomCheck(o.pubkey)) return new Error(`${path}.pubkey: custom check failed`) + + if (typeof o.tracked === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('tracked')) { + const trackedErr = TrackedLiquidityProviderV2Validate(o.tracked, opts.tracked_Options, `${path}.tracked`) + if (trackedErr !== null) return trackedErr + } + + + return null +} + export type LiquidityProviderFilter = { latestIncomingInvoice?: OperationsCursor latestOutgoingInvoice?: OperationsCursor @@ -2820,6 +2918,33 @@ export const LndAssetProviderValidate = (o?: LndAssetProvider, opts: LndAssetPro return null } +export type LndAssetProviderV2 = { + pubkey: string + tracked?: TrackedLndProviderV2 +} +export type LndAssetProviderV2OptionalField = 'tracked' +export const LndAssetProviderV2OptionalFields: LndAssetProviderV2OptionalField[] = ['tracked'] +export type LndAssetProviderV2Options = OptionsBaseMessage & { + checkOptionalsAreSet?: LndAssetProviderV2OptionalField[] + pubkey_CustomCheck?: (v: string) => boolean + tracked_Options?: TrackedLndProviderV2Options +} +export const LndAssetProviderV2Validate = (o?: LndAssetProviderV2, opts: LndAssetProviderV2Options = {}, path: string = 'LndAssetProviderV2::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.pubkey !== 'string') return new Error(`${path}.pubkey: is not a string`) + if (opts.pubkey_CustomCheck && !opts.pubkey_CustomCheck(o.pubkey)) return new Error(`${path}.pubkey: custom check failed`) + + if (typeof o.tracked === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('tracked')) { + const trackedErr = TrackedLndProviderV2Validate(o.tracked, opts.tracked_Options, `${path}.tracked`) + if (trackedErr !== null) return trackedErr + } + + + return null +} + export type LndChannels = { open_channels: OpenChannel[] } @@ -4776,17 +4901,55 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR export type TrackedLiquidityProvider = { balance: number - invoices: LiquidityAssetOperationsPage - payments: LiquidityAssetOperationsPage + invoices: AssetOperation[] + payments: AssetOperation[] } export const TrackedLiquidityProviderOptionalFields: [] = [] export type TrackedLiquidityProviderOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + balance_CustomCheck?: (v: number) => boolean + invoices_ItemOptions?: AssetOperationOptions + invoices_CustomCheck?: (v: AssetOperation[]) => boolean + payments_ItemOptions?: AssetOperationOptions + payments_CustomCheck?: (v: AssetOperation[]) => boolean +} +export const TrackedLiquidityProviderValidate = (o?: TrackedLiquidityProvider, opts: TrackedLiquidityProviderOptions = {}, path: string = 'TrackedLiquidityProvider::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.balance !== 'number') return new Error(`${path}.balance: is not a number`) + if (opts.balance_CustomCheck && !opts.balance_CustomCheck(o.balance)) return new Error(`${path}.balance: custom check failed`) + + if (!Array.isArray(o.invoices)) return new Error(`${path}.invoices: is not an array`) + for (let index = 0; index < o.invoices.length; index++) { + const invoicesErr = AssetOperationValidate(o.invoices[index], opts.invoices_ItemOptions, `${path}.invoices[${index}]`) + if (invoicesErr !== null) return invoicesErr + } + if (opts.invoices_CustomCheck && !opts.invoices_CustomCheck(o.invoices)) return new Error(`${path}.invoices: custom check failed`) + + if (!Array.isArray(o.payments)) return new Error(`${path}.payments: is not an array`) + for (let index = 0; index < o.payments.length; index++) { + const paymentsErr = AssetOperationValidate(o.payments[index], opts.payments_ItemOptions, `${path}.payments[${index}]`) + if (paymentsErr !== null) return paymentsErr + } + if (opts.payments_CustomCheck && !opts.payments_CustomCheck(o.payments)) return new Error(`${path}.payments: custom check failed`) + + return null +} + +export type TrackedLiquidityProviderV2 = { + balance: number + invoices: LiquidityAssetOperationsPage + payments: LiquidityAssetOperationsPage +} +export const TrackedLiquidityProviderV2OptionalFields: [] = [] +export type TrackedLiquidityProviderV2Options = OptionsBaseMessage & { checkOptionalsAreSet?: [] balance_CustomCheck?: (v: number) => boolean invoices_Options?: LiquidityAssetOperationsPageOptions payments_Options?: LiquidityAssetOperationsPageOptions } -export const TrackedLiquidityProviderValidate = (o?: TrackedLiquidityProvider, opts: TrackedLiquidityProviderOptions = {}, path: string = 'TrackedLiquidityProvider::root.'): Error | null => { +export const TrackedLiquidityProviderV2Validate = (o?: TrackedLiquidityProviderV2, opts: TrackedLiquidityProviderV2Options = {}, path: string = 'TrackedLiquidityProviderV2::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') @@ -4805,6 +4968,74 @@ export const TrackedLiquidityProviderValidate = (o?: TrackedLiquidityProvider, o } export type TrackedLndProvider = { + channels_balance: number + confirmed_balance: number + incoming_tx: AssetOperation[] + invoices: AssetOperation[] + outgoing_tx: AssetOperation[] + payments: AssetOperation[] + unconfirmed_balance: number +} +export const TrackedLndProviderOptionalFields: [] = [] +export type TrackedLndProviderOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + channels_balance_CustomCheck?: (v: number) => boolean + confirmed_balance_CustomCheck?: (v: number) => boolean + incoming_tx_ItemOptions?: AssetOperationOptions + incoming_tx_CustomCheck?: (v: AssetOperation[]) => boolean + invoices_ItemOptions?: AssetOperationOptions + invoices_CustomCheck?: (v: AssetOperation[]) => boolean + outgoing_tx_ItemOptions?: AssetOperationOptions + outgoing_tx_CustomCheck?: (v: AssetOperation[]) => boolean + payments_ItemOptions?: AssetOperationOptions + payments_CustomCheck?: (v: AssetOperation[]) => boolean + unconfirmed_balance_CustomCheck?: (v: number) => boolean +} +export const TrackedLndProviderValidate = (o?: TrackedLndProvider, opts: TrackedLndProviderOptions = {}, path: string = 'TrackedLndProvider::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.channels_balance !== 'number') return new Error(`${path}.channels_balance: is not a number`) + if (opts.channels_balance_CustomCheck && !opts.channels_balance_CustomCheck(o.channels_balance)) return new Error(`${path}.channels_balance: custom check failed`) + + if (typeof o.confirmed_balance !== 'number') return new Error(`${path}.confirmed_balance: is not a number`) + if (opts.confirmed_balance_CustomCheck && !opts.confirmed_balance_CustomCheck(o.confirmed_balance)) return new Error(`${path}.confirmed_balance: custom check failed`) + + if (!Array.isArray(o.incoming_tx)) return new Error(`${path}.incoming_tx: is not an array`) + for (let index = 0; index < o.incoming_tx.length; index++) { + const incoming_txErr = AssetOperationValidate(o.incoming_tx[index], opts.incoming_tx_ItemOptions, `${path}.incoming_tx[${index}]`) + if (incoming_txErr !== null) return incoming_txErr + } + if (opts.incoming_tx_CustomCheck && !opts.incoming_tx_CustomCheck(o.incoming_tx)) return new Error(`${path}.incoming_tx: custom check failed`) + + if (!Array.isArray(o.invoices)) return new Error(`${path}.invoices: is not an array`) + for (let index = 0; index < o.invoices.length; index++) { + const invoicesErr = AssetOperationValidate(o.invoices[index], opts.invoices_ItemOptions, `${path}.invoices[${index}]`) + if (invoicesErr !== null) return invoicesErr + } + if (opts.invoices_CustomCheck && !opts.invoices_CustomCheck(o.invoices)) return new Error(`${path}.invoices: custom check failed`) + + if (!Array.isArray(o.outgoing_tx)) return new Error(`${path}.outgoing_tx: is not an array`) + for (let index = 0; index < o.outgoing_tx.length; index++) { + const outgoing_txErr = AssetOperationValidate(o.outgoing_tx[index], opts.outgoing_tx_ItemOptions, `${path}.outgoing_tx[${index}]`) + if (outgoing_txErr !== null) return outgoing_txErr + } + if (opts.outgoing_tx_CustomCheck && !opts.outgoing_tx_CustomCheck(o.outgoing_tx)) return new Error(`${path}.outgoing_tx: custom check failed`) + + if (!Array.isArray(o.payments)) return new Error(`${path}.payments: is not an array`) + for (let index = 0; index < o.payments.length; index++) { + const paymentsErr = AssetOperationValidate(o.payments[index], opts.payments_ItemOptions, `${path}.payments[${index}]`) + if (paymentsErr !== null) return paymentsErr + } + if (opts.payments_CustomCheck && !opts.payments_CustomCheck(o.payments)) return new Error(`${path}.payments: custom check failed`) + + if (typeof o.unconfirmed_balance !== 'number') return new Error(`${path}.unconfirmed_balance: is not a number`) + if (opts.unconfirmed_balance_CustomCheck && !opts.unconfirmed_balance_CustomCheck(o.unconfirmed_balance)) return new Error(`${path}.unconfirmed_balance: custom check failed`) + + return null +} + +export type TrackedLndProviderV2 = { channels_balance: number confirmed_balance: number incoming_tx: LndAssetOperationsPage @@ -4813,8 +5044,8 @@ export type TrackedLndProvider = { payments: LndAssetOperationsPage unconfirmed_balance: number } -export const TrackedLndProviderOptionalFields: [] = [] -export type TrackedLndProviderOptions = OptionsBaseMessage & { +export const TrackedLndProviderV2OptionalFields: [] = [] +export type TrackedLndProviderV2Options = OptionsBaseMessage & { checkOptionalsAreSet?: [] channels_balance_CustomCheck?: (v: number) => boolean confirmed_balance_CustomCheck?: (v: number) => boolean @@ -4824,7 +5055,7 @@ export type TrackedLndProviderOptions = OptionsBaseMessage & { payments_Options?: LndAssetOperationsPageOptions unconfirmed_balance_CustomCheck?: (v: number) => boolean } -export const TrackedLndProviderValidate = (o?: TrackedLndProvider, opts: TrackedLndProviderOptions = {}, path: string = 'TrackedLndProvider::root.'): Error | null => { +export const TrackedLndProviderV2Validate = (o?: TrackedLndProviderV2, opts: TrackedLndProviderV2Options = {}, path: string = 'TrackedLndProviderV2::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') diff --git a/proto/service/methods.proto b/proto/service/methods.proto index 646aaacdb..121689794 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -119,6 +119,13 @@ service LightningPub { option (nostr) = true; }; + rpc GetAssetsAndLiabilitiesV2(structs.AssetsAndLiabilitiesReqV2) returns (structs.AssetsAndLiabilitiesV2) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/assets/liabilities/v2"; + option (nostr) = true; + }; + rpc BumpTx(structs.BumpTx) returns (structs.Empty) { option (auth_type) = "Admin"; option (http_method) = "post"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 29d626d63..e4470fed4 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -37,6 +37,12 @@ message LiquidityProviderFilter { } message AssetsAndLiabilitiesReq { + optional int64 limit_invoices = 1; + optional int64 limit_payments = 2; + optional int64 limit_providers = 4; +} + +message AssetsAndLiabilitiesReqV2 { repeated LndProviderFilter lnd_providers = 1; repeated LiquidityProviderFilter liquidity_providers = 2; } @@ -104,6 +110,22 @@ message LiquidityAssetOperationsPage { } message TrackedLndProvider { + int64 confirmed_balance = 1; + int64 unconfirmed_balance = 2; + int64 channels_balance = 3; + repeated AssetOperation payments = 4; + repeated AssetOperation invoices = 5; + repeated AssetOperation incoming_tx = 6; + repeated AssetOperation outgoing_tx = 7; +} + +message TrackedLiquidityProvider { + int64 balance = 1; + repeated AssetOperation payments = 2; + repeated AssetOperation invoices = 3; +} + +message TrackedLndProviderV2 { int64 confirmed_balance = 1; int64 unconfirmed_balance = 2; int64 channels_balance = 3; @@ -113,7 +135,7 @@ message TrackedLndProvider { LndAssetOperationsPage outgoing_tx = 7; } -message TrackedLiquidityProvider { +message TrackedLiquidityProviderV2 { int64 balance = 1; LiquidityAssetOperationsPage payments = 2; LiquidityAssetOperationsPage invoices = 3; @@ -129,12 +151,28 @@ message LiquidityAssetProvider { optional TrackedLiquidityProvider tracked = 2; } +message LndAssetProviderV2 { + string pubkey = 1; + optional TrackedLndProviderV2 tracked = 2; +} + +message LiquidityAssetProviderV2 { + string pubkey = 1; + optional TrackedLiquidityProviderV2 tracked = 2; +} + message AssetsAndLiabilities { int64 users_balance = 1; repeated LndAssetProvider lnds = 2; repeated LiquidityAssetProvider liquidity_providers = 3; } +message AssetsAndLiabilitiesV2 { + int64 users_balance = 1; + repeated LndAssetProviderV2 lnds = 2; + repeated LiquidityAssetProviderV2 liquidity_providers = 3; +} + message UserHealthState { string downtime_reason = 1; } diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 5a60222c7..0e3ecf840 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -384,9 +384,15 @@ export class AdminManager { } async GetAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq): Promise { + const v2Req = await this.BuildV2ReqFromV1(req) + const v2Res = await this.GetAssetsAndLiabilitiesV2(v2Req) + return this.V2ToV1Response(v2Res) + } + + async GetAssetsAndLiabilitiesV2(req: Types.AssetsAndLiabilitiesReqV2): Promise { const providers = await this.storage.liquidityStorage.GetTrackedProviders() - const lnds: Types.LndAssetProvider[] = [] - const liquidityProviders: Types.LiquidityAssetProvider[] = [] + const lnds: Types.LndAssetProviderV2[] = [] + const liquidityProviders: Types.LiquidityAssetProviderV2[] = [] for (const provider of providers) { if (provider.provider_type === 'lnd') { const lndEntry = await this.GetLndAssetsAndLiabilities(req, provider) @@ -404,7 +410,56 @@ export class AdminManager { } } - async GetProviderAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq, provider: TrackedProvider): Promise { + private async BuildV2ReqFromV1(req: Types.AssetsAndLiabilitiesReq): Promise { + const providers = await this.storage.liquidityStorage.GetTrackedProviders() + const limitPayments = req.limit_payments ?? DEFAULT_LND_PAGE_SIZE + const limitInvoices = req.limit_invoices ?? 100 + const limitProviders = req.limit_providers ?? 100 + return { + lnd_providers: providers + .filter(p => p.provider_type === 'lnd') + .map(p => ({ + pubkey: p.provider_pubkey, + limit_payments: limitPayments, + limit_invoices: limitInvoices, + limit_transactions: MAX_PAGE_SIZE, + })), + liquidity_providers: providers + .filter(p => p.provider_type === 'lnPub') + .map(p => ({ + pubkey: p.provider_pubkey, + limit: limitProviders, + })), + } + } + + private V2ToV1Response(res: Types.AssetsAndLiabilitiesV2): Types.AssetsAndLiabilities { + return { + users_balance: res.users_balance, + lnds: res.lnds.map(lnd => ({ + pubkey: lnd.pubkey, + tracked: lnd.tracked ? { + confirmed_balance: lnd.tracked.confirmed_balance, + unconfirmed_balance: lnd.tracked.unconfirmed_balance, + channels_balance: lnd.tracked.channels_balance, + payments: lnd.tracked.payments.operations, + invoices: lnd.tracked.invoices.operations, + incoming_tx: lnd.tracked.incoming_tx.operations, + outgoing_tx: lnd.tracked.outgoing_tx.operations, + } : undefined, + })), + liquidity_providers: res.liquidity_providers.map(provider => ({ + pubkey: provider.pubkey, + tracked: provider.tracked ? { + balance: provider.tracked.balance, + payments: provider.tracked.payments.operations, + invoices: provider.tracked.invoices.operations, + } : undefined, + })), + } + } + + async GetProviderAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReqV2, provider: TrackedProvider): Promise { if (!this.liquidityProvider) { throw new Error("liquidity provider not attached") } @@ -504,7 +559,7 @@ export class AdminManager { ) } - async GetLndAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq, provider: TrackedProvider): Promise { + async GetLndAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReqV2, provider: TrackedProvider): Promise { const info = await this.lnd.GetInfo() if (provider.provider_pubkey !== info.identityPubkey) { return { pubkey: provider.provider_pubkey, tracked: undefined } diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 62a70bbf5..e5c13cb05 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -144,6 +144,9 @@ export default (mainHandler: Main): Types.ServerMethods => { GetAssetsAndLiabilities: async ({ ctx, req }) => { return mainHandler.adminManager.GetAssetsAndLiabilities(req) }, + GetAssetsAndLiabilitiesV2: async ({ ctx, req }) => { + return mainHandler.adminManager.GetAssetsAndLiabilitiesV2(req) + }, GetProvidersDisruption: async () => { return mainHandler.metricsManager.GetProvidersDisruption() }, diff --git a/src/tests/assetsAndLiabilities.spec.ts b/src/tests/assetsAndLiabilities.spec.ts index 1e1b53c78..fc248636d 100644 --- a/src/tests/assetsAndLiabilities.spec.ts +++ b/src/tests/assetsAndLiabilities.spec.ts @@ -7,19 +7,20 @@ import { initBootstrappedInstance } from "./setupBootstrapped.js" export const ignore = false export const dev = false -const emptyReq = (): Types.AssetsAndLiabilitiesReq => ({ +const emptyReq = (): Types.AssetsAndLiabilitiesReqV2 => ({ lnd_providers: [], liquidity_providers: [], }) const getAssets = (T: TestBase, req = emptyReq()) => - T.main.adminManager.GetAssetsAndLiabilities(req) + T.main.adminManager.GetAssetsAndLiabilitiesV2(req) export default async (T: TestBase) => { await testUsersBalance(T) await testLndProviderTracked(T) await testLndTrackedUserPayment(T) await testLndPagination(T) + await testV1Compatibility(T) await runSanityCheck(T) } @@ -28,7 +29,7 @@ const testUsersBalance = async (T: TestBase) => { await safelySetUserBalance(T, T.user1, 2000) await safelySetUserBalance(T, T.user2, 500) const res = await getAssets(T) - T.expect(Types.AssetsAndLiabilitiesValidate(res)).to.equal(null) + T.expect(Types.AssetsAndLiabilitiesV2Validate(res)).to.equal(null) T.expect(res.users_balance).to.equal(2500) T.d("users_balance matches total user balances") } @@ -89,6 +90,25 @@ const testLndPagination = async (T: TestBase) => { T.d("lnd payment pagination respects limit") } +const testV1Compatibility = async (T: TestBase) => { + T.d("starting testV1Compatibility") + await safelySetUserBalance(T, T.user1, 1000) + const res = await T.main.adminManager.GetAssetsAndLiabilities({}) + T.expect(Types.AssetsAndLiabilitiesValidate(res)).to.equal(null) + T.expect(res.users_balance).to.be.a("number") + const lnd = res.lnds.find(p => p.tracked !== undefined) + if (lnd?.tracked) { + T.expect(lnd.tracked.payments).to.be.an("array") + T.expect(lnd.tracked.invoices).to.be.an("array") + T.expect(lnd.tracked.incoming_tx).to.be.an("array") + T.expect(lnd.tracked.outgoing_tx).to.be.an("array") + for (const op of lnd.tracked.payments) { + T.expect(op.tracked?.user_id).to.equal(undefined) + } + } + T.d("v1 endpoint returns flat operation arrays") +} + const inboundPaymentOnBootstrapped = async (T: TestBase, bootstrapped: Main, user: TestUserData) => { const ctx = { app_id: user.appId, user_id: user.userId, app_user_id: user.appUserIdentifier } const invoiceRes = await bootstrapped.appUserManager.NewInvoice(ctx, { amountSats: 1000, memo: "assetsLiabilities" }) From 85803e3fabac20de47871c023374b742af15ff10 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 13 Jul 2026 16:10:31 +0000 Subject: [PATCH 19/22] fix err --- src/tests/assetsAndLiabilities.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/assetsAndLiabilities.spec.ts b/src/tests/assetsAndLiabilities.spec.ts index fc248636d..da57da37f 100644 --- a/src/tests/assetsAndLiabilities.spec.ts +++ b/src/tests/assetsAndLiabilities.spec.ts @@ -92,7 +92,6 @@ const testLndPagination = async (T: TestBase) => { const testV1Compatibility = async (T: TestBase) => { T.d("starting testV1Compatibility") - await safelySetUserBalance(T, T.user1, 1000) const res = await T.main.adminManager.GetAssetsAndLiabilities({}) T.expect(Types.AssetsAndLiabilitiesValidate(res)).to.equal(null) T.expect(res.users_balance).to.be.a("number") From 5cd6d78618ea27d0592c1c06dc53d340a458b4a1 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 13 Jul 2026 16:14:59 +0000 Subject: [PATCH 20/22] optional user is backw comp --- src/tests/assetsAndLiabilities.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tests/assetsAndLiabilities.spec.ts b/src/tests/assetsAndLiabilities.spec.ts index da57da37f..8af4a27ec 100644 --- a/src/tests/assetsAndLiabilities.spec.ts +++ b/src/tests/assetsAndLiabilities.spec.ts @@ -101,9 +101,6 @@ const testV1Compatibility = async (T: TestBase) => { T.expect(lnd.tracked.invoices).to.be.an("array") T.expect(lnd.tracked.incoming_tx).to.be.an("array") T.expect(lnd.tracked.outgoing_tx).to.be.an("array") - for (const op of lnd.tracked.payments) { - T.expect(op.tracked?.user_id).to.equal(undefined) - } } T.d("v1 endpoint returns flat operation arrays") } From 03b376b41b1716d7ba3da45317d294209fb0fea5 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 13 Jul 2026 16:32:01 +0000 Subject: [PATCH 21/22] users list fix --- src/services/helpers/pageLimit.ts | 4 +- src/services/main/appUserManager.ts | 29 +++-- src/services/storage/applicationStorage.ts | 9 +- src/tests/adminUsers.spec.ts | 117 +++++++++++++++++++++ 4 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 src/tests/adminUsers.spec.ts diff --git a/src/services/helpers/pageLimit.ts b/src/services/helpers/pageLimit.ts index 77878eec9..c0a86b121 100644 --- a/src/services/helpers/pageLimit.ts +++ b/src/services/helpers/pageLimit.ts @@ -1,6 +1,6 @@ export const DEFAULT_PAGE_SIZE = 20 -export const DEFAULT_LND_PAGE_SIZE = 50 -export const MAX_PAGE_SIZE = 100 +export const DEFAULT_LND_PAGE_SIZE = 20 +export const MAX_PAGE_SIZE = 50 export const MAX_LIQUIDITY_PAGE_SIZE = 40 export const clampPageLimit = (value: number | undefined, defaultLimit: number, maxLimit: number): number => diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index b82f6f685..e9a02718f 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -131,24 +131,31 @@ export default class { const apps: Record = {} applications.forEach(a => apps[a.owner.user_id] = a.app_id) - const usersInfo: Types.UserAdminInfo[] = [] - for (const user of users) { - const appUsers = await this.storage.applicationStorage.GetAllAppUsersFromUser(user.user_id) - const appUsersInfo: Types.AppUserAdminInfo[] = appUsers.map(a => ({ + const appUsers = await this.storage.applicationStorage.GetAppUsersForUsers(users.map(u => u.user_id)) + const appUsersByUserId = new Map() + for (const a of appUsers) { + const userId = a.user.user_id + let appUsersInfo = appUsersByUserId.get(userId) + if (!appUsersInfo) { + appUsersInfo = [] + appUsersByUserId.set(userId, appUsersInfo) + } + appUsersInfo.push({ app_user_id: a.identifier, npub: a.nostr_public_key || "", has_callback_url: a.callback_url !== "", has_topic_id: a.topic_id !== "", - })) - usersInfo.push({ - user_id: user.user_id, - balance: user.balance_sats, - locked: user.locked, - app_users: appUsersInfo, - owner_of_app_id: apps[user.user_id] }) } + const usersInfo: Types.UserAdminInfo[] = users.map(user => ({ + user_id: user.user_id, + balance: user.balance_sats, + locked: user.locked, + app_users: appUsersByUserId.get(user.user_id) ?? [], + owner_of_app_id: apps[user.user_id] + })) + return { users: usersInfo, total } } diff --git a/src/services/storage/applicationStorage.ts b/src/services/storage/applicationStorage.ts index 402e7773a..d2670e5cf 100644 --- a/src/services/storage/applicationStorage.ts +++ b/src/services/storage/applicationStorage.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { Between, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm" +import { Between, FindOperator, In, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm" import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { Application } from "./entity/Application.js" import UserStorage from './userStorage.js'; @@ -142,6 +142,13 @@ export default class { return this.dbs.Find('ApplicationUser', { where: { user: { user_id: userId } } }, txId) } + async GetAppUsersForUsers(userIds: string[], txId?: string): Promise { + if (userIds.length === 0) { + return [] + } + return this.dbs.Find('ApplicationUser', { where: { user: { user_id: In(userIds) } } }, txId) + } + async IsApplicationOwner(userId: string, txId?: string) { return this.dbs.FindOne('Application', { where: { owner: { user_id: userId } } }, txId) } diff --git a/src/tests/adminUsers.spec.ts b/src/tests/adminUsers.spec.ts new file mode 100644 index 000000000..89d516c22 --- /dev/null +++ b/src/tests/adminUsers.spec.ts @@ -0,0 +1,117 @@ +import * as Types from "../../proto/autogenerated/ts/types.js" +import { defaultInvoiceExpiry } from "../services/storage/paymentStorage.js" +import { expectThrowsAsync, runSanityCheck, safelySetUserBalance, TestBase } from "./testBase.js" + +export const ignore = false +export const dev = false + +const emptyCursor = (): Types.OperationsCursor => ({ id: 0, ts: 0 }) + +const getUsersAdminInfo = (T: TestBase, req: Types.UsersAdminInfoRequest = {}) => + T.main.appUserManager.GetUsersAdminInfo(req) + +const getUserOperationsFromAdmin = (T: TestBase, userId: string, maxSize = 50) => + T.main.paymentManager.GetUserOperations(userId, { + user_id: userId, + max_size: maxSize, + latestIncomingInvoice: emptyCursor(), + latestIncomingTx: emptyCursor(), + latestIncomingUserToUserPayment: emptyCursor(), + latestOutgoingInvoice: emptyCursor(), + latestOutgoingTx: emptyCursor(), + latestOutgoingUserToUserPayment: emptyCursor(), + }, true) + +export default async (T: TestBase) => { + await testGetUsersAdminInfo(T) + await testGetUsersAdminInfoPagination(T) + await testGetUserOperationsFromAdminOutgoingPayment(T) + await testGetUserOperationsFromAdminWorksForBannedUser(T) + await runSanityCheck(T) +} + +const testGetUsersAdminInfo = async (T: TestBase) => { + T.d("starting testGetUsersAdminInfo") + await safelySetUserBalance(T, T.user1, 2000) + await safelySetUserBalance(T, T.user2, 500) + + const res = await getUsersAdminInfo(T) + T.expect(Types.UsersAdminInfoValidate(res)).to.equal(null) + T.expect(res.total).to.be.at.least(3) + + const user1 = res.users.find(u => u.user_id === T.user1.userId) + const user2 = res.users.find(u => u.user_id === T.user2.userId) + T.expect(user1).to.not.equal(undefined) + T.expect(user2).to.not.equal(undefined) + T.expect(user1!.balance).to.equal(2000) + T.expect(user2!.balance).to.equal(500) + T.expect(user1!.locked).to.equal(false) + + const user1AppUser = user1!.app_users.find(a => a.app_user_id === T.user1.appUserIdentifier) + T.expect(user1AppUser).to.not.equal(undefined) + T.expect(user1AppUser!.has_topic_id).to.equal(true) + + const app = await T.main.storage.applicationStorage.GetApplication(T.app.appId) + const owner = res.users.find(u => u.user_id === app.owner.user_id) + T.expect(owner).to.not.equal(undefined) + T.expect(owner!.owner_of_app_id).to.equal(T.app.appId) + T.d("GetUsersAdminInfo returns balances, app users, and app ownership") +} + +const testGetUsersAdminInfoPagination = async (T: TestBase) => { + T.d("starting testGetUsersAdminInfoPagination") + const full = await getUsersAdminInfo(T) + const page = await getUsersAdminInfo(T, { skip: 0, take: 1 }) + + T.expect(Types.UsersAdminInfoValidate(page)).to.equal(null) + T.expect(page.users.length).to.equal(1) + T.expect(page.total).to.equal(full.total) + T.d("GetUsersAdminInfo pagination respects skip and take") +} + +const testGetUserOperationsFromAdminOutgoingPayment = async (T: TestBase) => { + T.d("starting testGetUserOperationsFromAdminOutgoingPayment") + await safelySetUserBalance(T, T.user1, 2000) + + const invoice = await T.externalAccessToOtherLnd.NewInvoice( + 500, "admin-user-ops", defaultInvoiceExpiry, { from: "system", useProvider: false } + ) + await T.main.appUserManager.PayInvoice( + { app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, + { invoice: invoice.payRequest, amount: 0 } + ) + + const res = await getUserOperationsFromAdmin(T, T.user1.userId) + T.expect(Types.GetUserOperationsResponseValidate(res)).to.equal(null) + T.expect(res.user_id).to.equal(T.user1.userId) + + const outgoingPayment = res.latestOutgoingInvoiceOperations.operations.find(op => + op.type === Types.UserOperationType.OUTGOING_INVOICE && op.amount === 500 + ) + T.expect(outgoingPayment).to.not.equal(undefined) + T.d("GetUserOperationsFromAdmin returns outgoing invoice payments") +} + +const testGetUserOperationsFromAdminWorksForBannedUser = async (T: TestBase) => { + T.d("starting testGetUserOperationsFromAdminWorksForBannedUser") + const opsReq = { + max_size: 50, + latestIncomingInvoice: emptyCursor(), + latestIncomingTx: emptyCursor(), + latestIncomingUserToUserPayment: emptyCursor(), + latestOutgoingInvoice: emptyCursor(), + latestOutgoingTx: emptyCursor(), + latestOutgoingUserToUserPayment: emptyCursor(), + } + + await T.main.appUserManager.BanUser(T.user1.userId) + await expectThrowsAsync( + T.main.paymentManager.GetUserOperations(T.user1.userId, opsReq, false), + "user is banned, cannot retrieve operations" + ) + + const res = await getUserOperationsFromAdmin(T, T.user1.userId) + T.expect(Types.GetUserOperationsResponseValidate(res)).to.equal(null) + T.expect(res.latestOutgoingInvoiceOperations.operations.length).to.be.at.least(1) + T.d("GetUserOperationsFromAdmin still works for banned users") +} From 8455219ef671c85f4b7537d011fc47df15241913 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 13 Jul 2026 16:39:23 +0000 Subject: [PATCH 22/22] test fix --- src/tests/adminUsers.spec.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/tests/adminUsers.spec.ts b/src/tests/adminUsers.spec.ts index 89d516c22..a7adc3031 100644 --- a/src/tests/adminUsers.spec.ts +++ b/src/tests/adminUsers.spec.ts @@ -5,8 +5,15 @@ import { expectThrowsAsync, runSanityCheck, safelySetUserBalance, TestBase } fro export const ignore = false export const dev = false +const USER1_FUNDING_SATS = 2000 +const USER2_FUNDING_SATS = 500 +const OUTGOING_PAYMENT_SATS = 500 + const emptyCursor = (): Types.OperationsCursor => ({ id: 0, ts: 0 }) +const outgoingInvoiceServiceFee = (T: TestBase, amount: number) => + T.main.paymentManager.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, amount, true) + const getUsersAdminInfo = (T: TestBase, req: Types.UsersAdminInfoRequest = {}) => T.main.appUserManager.GetUsersAdminInfo(req) @@ -23,6 +30,8 @@ const getUserOperationsFromAdmin = (T: TestBase, userId: string, maxSize = 50) = }, true) export default async (T: TestBase) => { + await safelySetUserBalance(T, T.user1, USER1_FUNDING_SATS) + await safelySetUserBalance(T, T.user2, USER2_FUNDING_SATS) await testGetUsersAdminInfo(T) await testGetUsersAdminInfoPagination(T) await testGetUserOperationsFromAdminOutgoingPayment(T) @@ -32,8 +41,6 @@ export default async (T: TestBase) => { const testGetUsersAdminInfo = async (T: TestBase) => { T.d("starting testGetUsersAdminInfo") - await safelySetUserBalance(T, T.user1, 2000) - await safelySetUserBalance(T, T.user2, 500) const res = await getUsersAdminInfo(T) T.expect(Types.UsersAdminInfoValidate(res)).to.equal(null) @@ -43,8 +50,8 @@ const testGetUsersAdminInfo = async (T: TestBase) => { const user2 = res.users.find(u => u.user_id === T.user2.userId) T.expect(user1).to.not.equal(undefined) T.expect(user2).to.not.equal(undefined) - T.expect(user1!.balance).to.equal(2000) - T.expect(user2!.balance).to.equal(500) + T.expect(user1!.balance).to.equal(USER1_FUNDING_SATS) + T.expect(user2!.balance).to.equal(USER2_FUNDING_SATS) T.expect(user1!.locked).to.equal(false) const user1AppUser = user1!.app_users.find(a => a.app_user_id === T.user1.appUserIdentifier) @@ -71,24 +78,33 @@ const testGetUsersAdminInfoPagination = async (T: TestBase) => { const testGetUserOperationsFromAdminOutgoingPayment = async (T: TestBase) => { T.d("starting testGetUserOperationsFromAdminOutgoingPayment") - await safelySetUserBalance(T, T.user1, 2000) + const serviceFee = outgoingInvoiceServiceFee(T, OUTGOING_PAYMENT_SATS) const invoice = await T.externalAccessToOtherLnd.NewInvoice( - 500, "admin-user-ops", defaultInvoiceExpiry, { from: "system", useProvider: false } + OUTGOING_PAYMENT_SATS, "admin-user-ops", defaultInvoiceExpiry, { from: "system", useProvider: false } ) await T.main.appUserManager.PayInvoice( { app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, { invoice: invoice.payRequest, amount: 0 } ) + const user1 = await T.main.storage.userStorage.GetUser(T.user1.userId) + T.expect(user1.balance_sats).to.equal(USER1_FUNDING_SATS - OUTGOING_PAYMENT_SATS - serviceFee) + const res = await getUserOperationsFromAdmin(T, T.user1.userId) T.expect(Types.GetUserOperationsResponseValidate(res)).to.equal(null) T.expect(res.user_id).to.equal(T.user1.userId) + const incomingFunding = res.latestIncomingInvoiceOperations.operations.find(op => + op.type === Types.UserOperationType.INCOMING_INVOICE && op.amount === USER1_FUNDING_SATS + ) + T.expect(incomingFunding).to.not.equal(undefined) + const outgoingPayment = res.latestOutgoingInvoiceOperations.operations.find(op => - op.type === Types.UserOperationType.OUTGOING_INVOICE && op.amount === 500 + op.type === Types.UserOperationType.OUTGOING_INVOICE && op.amount === OUTGOING_PAYMENT_SATS ) T.expect(outgoingPayment).to.not.equal(undefined) + T.expect(outgoingPayment!.service_fee).to.equal(serviceFee) T.d("GetUserOperationsFromAdmin returns outgoing invoice payments") } @@ -112,6 +128,7 @@ const testGetUserOperationsFromAdminWorksForBannedUser = async (T: TestBase) => const res = await getUserOperationsFromAdmin(T, T.user1.userId) T.expect(Types.GetUserOperationsResponseValidate(res)).to.equal(null) - T.expect(res.latestOutgoingInvoiceOperations.operations.length).to.be.at.least(1) + T.expect(res.latestOutgoingInvoiceOperations.operations).to.have.lengthOf(1) + T.expect(res.latestOutgoingInvoiceOperations.operations[0].amount).to.equal(OUTGOING_PAYMENT_SATS) T.d("GetUserOperationsFromAdmin still works for banned users") }