From 5dafc10398313477eeda2007495292f2ae5a2d17 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Thu, 2 Jul 2026 16:10:04 +0000 Subject: [PATCH 1/4] update dashboard UI --- src/Api/pub/autogenerated/ts/nostr_client.ts | 30 + .../pub/autogenerated/ts/nostr_transport.ts | 32 + src/Api/pub/autogenerated/ts/types.ts | 286 +++++- src/Layout2/Metrics/MetricsSubPageToolbar.tsx | 5 +- src/Pages/Metrics/AssetsAndLiab.tsx | 839 ++++++++++++------ src/Pages/Metrics/UserOperationsAdmin.tsx | 220 +++++ src/Pages/Metrics/UsersAdmin.tsx | 194 ++++ src/Pages/Metrics/adminOperationDisplay.tsx | 104 +++ src/Pages/Metrics/earnings.tsx | 116 ++- src/Pages/Metrics/index.tsx | 4 + src/Pages/Metrics/metricsDataTable.tsx | 109 +++ src/Pages/Metrics/metricsMain.tsx | 5 + 12 files changed, 1603 insertions(+), 341 deletions(-) create mode 100644 src/Pages/Metrics/UserOperationsAdmin.tsx create mode 100644 src/Pages/Metrics/UsersAdmin.tsx create mode 100644 src/Pages/Metrics/adminOperationDisplay.tsx create mode 100644 src/Pages/Metrics/metricsDataTable.tsx diff --git a/src/Api/pub/autogenerated/ts/nostr_client.ts b/src/Api/pub/autogenerated/ts/nostr_client.ts index 5bc489bf..8c68ddfd 100644 --- a/src/Api/pub/autogenerated/ts/nostr_client.ts +++ b/src/Api/pub/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/src/Api/pub/autogenerated/ts/nostr_transport.ts b/src/Api/pub/autogenerated/ts/nostr_transport.ts index ee22d185..c4c474fb 100644 --- a/src/Api/pub/autogenerated/ts/nostr_transport.ts +++ b/src/Api/pub/autogenerated/ts/nostr_transport.ts @@ -1162,6 +1162,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/src/Api/pub/autogenerated/ts/types.ts b/src/Api/pub/autogenerated/ts/types.ts index 146c1a4f..fa224979 100644 --- a/src/Api/pub/autogenerated/ts/types.ts +++ b/src/Api/pub/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 @@ -1016,30 +1057,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 } @@ -2096,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 @@ -2107,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') @@ -2139,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 } @@ -2149,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') @@ -2188,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 } @@ -2543,6 +2600,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 +3044,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 +4803,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 +4827,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 } @@ -5029,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 } @@ -5251,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/src/Layout2/Metrics/MetricsSubPageToolbar.tsx b/src/Layout2/Metrics/MetricsSubPageToolbar.tsx index 6c89ba31..1be23721 100644 --- a/src/Layout2/Metrics/MetricsSubPageToolbar.tsx +++ b/src/Layout2/Metrics/MetricsSubPageToolbar.tsx @@ -3,15 +3,16 @@ import { chevronBackOutline } from 'ionicons/icons'; interface BackHeaderProps { title?: string; + backHref?: string; } -const MetricsSubPageToolbar: React.FC = ({ title }: BackHeaderProps) => { +const MetricsSubPageToolbar: React.FC = ({ title, backHref = "/metrics" }: BackHeaderProps) => { return ( - + diff --git a/src/Pages/Metrics/AssetsAndLiab.tsx b/src/Pages/Metrics/AssetsAndLiab.tsx index a5309b2f..87c0dad7 100644 --- a/src/Pages/Metrics/AssetsAndLiab.tsx +++ b/src/Pages/Metrics/AssetsAndLiab.tsx @@ -1,10 +1,10 @@ -import { useEffect, useMemo, useState } from "react"; -import { fetcher, FetcherFuncs } from "./fetcher"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { fetcher } from "./fetcher"; import { useAppSelector } from "@/State/store/hooks"; import { selectAdminNprofileViews } from "@/State/scoped/backups/sources/selectors"; import { selectSelectedMetricsAdminSourceId } from "@/State/runtime/slice"; import { useIonLoading, useIonRouter } from "@ionic/react"; -import { IonButton, IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonContent, IonHeader, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonPage, IonText } from "@ionic/react"; +import { IonButton, IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonContent, IonHeader, IonIcon, IonPage, IonText } from "@ionic/react"; import { walletOutline, flashOutline, @@ -23,7 +23,12 @@ import { AssetsAndLiabilitiesReq, AssetOperation, LndAssetProvider, + LndProviderFilter, LiquidityAssetProvider, + LiquidityProviderFilter, + OperationsCursor, + TrackedLiquidityProvider, + TrackedLndProvider, TrackedOperationType, } from "@/Api/pub/autogenerated/ts/types"; import MetricsSubPageToolbar from "@/Layout2/Metrics/MetricsSubPageToolbar"; @@ -88,28 +93,110 @@ function formatSats(n: number): string { return n.toLocaleString() + " sats"; } -function formatTs(unix: number): string { +function formatTableTs(unix: number): string { if (!unix) return "—"; - return new Date(unix * 1000).toLocaleString(); + const d = new Date(unix * 1000); + const pad = (n: number) => String(n).padStart(2, "0"); + const day = pad(d.getDate()); + const month = pad(d.getMonth() + 1); + const year = String(d.getFullYear()).slice(-2); + const hours = pad(d.getHours()); + const minutes = pad(d.getMinutes()); + const seconds = pad(d.getSeconds()); + return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`; +} + +function formatTableAmount(n: number): string { + return n.toLocaleString("fr-FR"); } function trackedTypeLabel(type: TrackedOperationType): string { return type === TrackedOperationType.ROOT ? "ROOT" : "USER"; } -function hasDiscrepancy(op: AssetOperation): boolean { - if (!op.tracked) return false; - return op.amount !== op.tracked.amount || op.ts !== op.tracked.ts; +const TIME_MISMATCH_LEEWAY_S = 10; + +function timestampsMismatch(a: number, b: number): boolean { + return Math.abs(a - b) > TIME_MISMATCH_LEEWAY_S; } function getMismatch(op: AssetOperation): { amount: boolean; ts: boolean } | null { if (!op.tracked) return null; const amount = op.amount !== op.tracked.amount; - const ts = op.ts !== op.tracked.ts; + const ts = timestampsMismatch(op.ts, op.tracked.ts); if (!amount && !ts) return null; return { amount, ts }; } +function mismatchNotes(mismatch: { amount: boolean; ts: boolean } | null): string { + if (!mismatch) return ""; + if (mismatch.amount && mismatch.ts) return "amount + time mismatch"; + if (mismatch.amount) return "amount mismatch"; + return "time mismatch"; +} + +type ProviderKind = "lnd" | "lp"; +type AssetOpKind = LndOpType | LpOpType; + +type UnifiedAssetOperation = { + op: AssetOperation; + providerKind: ProviderKind; + providerPubkey: string; + opKind: AssetOpKind; +}; + +function collectAllOperations(data: AssetsAndLiabilities): UnifiedAssetOperation[] { + const rows: UnifiedAssetOperation[] = []; + + for (const lnd of data.lnds) { + if (!lnd.tracked) continue; + const { tracked } = lnd; + for (const op of tracked.invoices) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "invoice" }); + for (const op of tracked.payments) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "payment" }); + for (const op of tracked.incoming_tx) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "incoming_tx" }); + for (const op of tracked.outgoing_tx) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "outgoing_tx" }); + } + + for (const lp of data.liquidity_providers) { + if (!lp.tracked) continue; + const { tracked } = lp; + for (const op of tracked.invoices) rows.push({ op, providerKind: "lp", providerPubkey: lp.pubkey, opKind: "invoice" }); + for (const op of tracked.payments) rows.push({ op, providerKind: "lp", providerPubkey: lp.pubkey, opKind: "payment" }); + } + + return rows.sort((a, b) => b.op.ts - a.op.ts); +} + +function txTypeLabel(op: AssetOperation): string { + if (!op.tracked) return "—"; + if (op.tracked.type === TrackedOperationType.USER && op.tracked.user_id) { + return op.tracked.user_id; + } + return trackedTypeLabel(op.tracked.type); +} + +function trackedIconTitle(op: AssetOperation): string { + if (!op.tracked) return "Untracked"; + const mismatch = getMismatch(op); + if (mismatch) return mismatchNotes(mismatch); + return "Tracked"; +} + +function opKindLabel(kind: AssetOpKind): string { + if (kind === "invoice") return "Invoice"; + if (kind === "payment") return "Payment"; + if (kind === "incoming_tx") return "Incoming tx"; + return "Outgoing tx"; +} + +function providerLabel(kind: ProviderKind): string { + return kind === "lnd" ? "LND" : "LP"; +} + +function providerIcon(kind: ProviderKind) { + return kind === "lnd" ? flashOutline : linkOutline; +} + type LndOpType = "invoice" | "payment" | "incoming_tx" | "outgoing_tx"; type LpOpType = "invoice" | "payment"; @@ -119,12 +206,6 @@ const LND_OP_ICON: Record = { incoming_tx: arrowDownOutline, outgoing_tx: arrowUpOutline, }; -const LND_OP_LABEL: Record = { - invoice: "Invoice", - payment: "Payment", - incoming_tx: "Incoming tx", - outgoing_tx: "Outgoing tx", -}; const LND_OP_COLOR: Record = { invoice: "var(--ion-color-success)", payment: "var(--ion-color-danger)", @@ -140,7 +221,171 @@ const LP_OP_COLOR: Record = { payment: "var(--ion-color-danger)", }; -function LndAssetCard({ provider }: { provider: LndAssetProvider }) { +const OPERATIONS_PAGE_SIZE = 25; + +function operationKey(op: AssetOperation): string { + const tracked = op.tracked; + return `${op.ts}:${op.amount}:${tracked?.type ?? ""}:${tracked?.user_id ?? ""}`; +} + +function mergeOperations(existing: AssetOperation[], incoming: AssetOperation[]): AssetOperation[] { + if (incoming.length === 0) return existing; + const seen = new Set(existing.map(operationKey)); + const merged = [...existing]; + for (const op of incoming) { + const key = operationKey(op); + if (seen.has(key)) continue; + seen.add(key); + merged.push(op); + } + return merged.sort((a, b) => b.ts - a.ts); +} + +function oldestCursor(ops: AssetOperation[]): OperationsCursor | undefined { + if (ops.length === 0) return undefined; + const oldest = ops.reduce((a, b) => (a.ts <= b.ts ? a : b)); + return { ts: oldest.ts, id: 0 }; +} + +function countProviderOperations(tracked: TrackedLndProvider | TrackedLiquidityProvider): number { + if ("incoming_tx" in tracked) { + return tracked.invoices.length + tracked.payments.length + tracked.incoming_tx.length + tracked.outgoing_tx.length; + } + return tracked.invoices.length + tracked.payments.length; +} + +const TABLE_GRID = "minmax(80px, 1.1fr) 100px 52px 36px minmax(68px, 80px) minmax(68px, 80px)"; + +function AssetOperationsTable({ operations }: { operations: UnifiedAssetOperation[] }) { + if (operations.length === 0) { + return ( + + No operations + + ); + } + + return ( +
+
+
+ Tx type + Root time + Provider + + Tracked + Root +
+ {operations.map((row, i) => { + const { op, providerKind, providerPubkey, opKind } = row; + const isTracked = op.tracked != null; + const mismatch = getMismatch(op); + const opIcon = providerKind === "lnd" ? LND_OP_ICON[opKind as LndOpType] : LP_OP_ICON[opKind as LpOpType]; + const opColor = providerKind === "lnd" ? LND_OP_COLOR[opKind as LndOpType] : LP_OP_COLOR[opKind as LpOpType]; + + return ( +
+
+ + + {isTracked ? txTypeLabel(op) : ( + {opKindLabel(opKind)} + )} + +
+ {formatTableTs(op.ts)} +
+ + {providerLabel(providerKind)} +
+
+ {isTracked ? ( + + ) : ( + + )} +
+ + {op.tracked ? formatTableAmount(op.tracked.amount) : "—"} + + + {formatTableAmount(op.amount)} + +
+ ); + })} +
+
+ ); +} + +function LndAssetCard({ + provider, + operationCount, + hasMore, + loadingMore, + onLoadMore, +}: { + provider: LndAssetProvider; + operationCount: number; + hasMore: boolean; + loadingMore: boolean; + onLoadMore: () => void; +}) { const tracked = provider.tracked; if (!tracked) { return ( @@ -161,12 +406,6 @@ function LndAssetCard({ provider }: { provider: LndAssetProvider }) { tracked.confirmed_balance + tracked.unconfirmed_balance + tracked.channels_balance; - const opsWithType: { op: AssetOperation; type: LndOpType }[] = [ - ...tracked.invoices.map((op) => ({ op, type: "invoice" as LndOpType })), - ...tracked.payments.map((op) => ({ op, type: "payment" as LndOpType })), - ...tracked.incoming_tx.map((op) => ({ op, type: "incoming_tx" as LndOpType })), - ...tracked.outgoing_tx.map((op) => ({ op, type: "outgoing_tx" as LndOpType })), - ].sort((a, b) => b.op.ts - a.op.ts); return ( @@ -177,114 +416,72 @@ function LndAssetCard({ provider }: { provider: LndAssetProvider }) { -
+
+
+ Confirmed + {formatSats(tracked.confirmed_balance)} +
+
+ Unconfirmed + {formatSats(tracked.unconfirmed_balance)} +
+
+ Channels + {formatSats(tracked.channels_balance)} +
-
- Confirmed - {formatSats(tracked.confirmed_balance)} -
-
- Unconfirmed - {formatSats(tracked.unconfirmed_balance)} -
-
- Channels - {formatSats(tracked.channels_balance)} -
-
- Total - {formatSats(total)} -
+ Total + {formatSats(total)}
- {opsWithType.length > 0 && ( - <> - - Recent operations ({opsWithType.length}) - - - {opsWithType.slice(0, 10).map(({ op, type }, i) => { - const isTracked = op.tracked != null; - const discrepancy = hasDiscrepancy(op); - const mismatch = getMismatch(op); - return ( - - - -
- {formatSats(op.amount)} - - {formatTs(op.ts)} - -
- {mismatch && op.tracked && ( -
- {mismatch.amount && ( -
Amount: root {formatSats(op.amount)} → tracked {formatSats(op.tracked.amount)}
- )} - {mismatch.ts && ( -
Time: root {formatTs(op.ts)} → tracked {formatTs(op.tracked.ts)}
- )} -
- )} -
-
- {isTracked ? ( - <> -
- - - Tracked ({trackedTypeLabel(op.tracked!.type)}) - -
- {mismatch && ( - - {mismatch.amount && mismatch.ts ? "Amount + time mismatch" : mismatch.amount ? "Amount mismatch" : "Time mismatch"} - - )} - - ) : ( -
- - Untracked -
- )} -
-
- ); - })} -
- - )}
+ {operationCount > 0 && ( + + {operationCount} operation{operationCount === 1 ? "" : "s"} loaded + + )} + {hasMore && ( + + {loadingMore ? "Loading..." : "Load more operations"} + + )} ); } -function LiquidityAssetCard({ provider }: { provider: LiquidityAssetProvider }) { +function LiquidityAssetCard({ + provider, + operationCount, + hasMore, + loadingMore, + onLoadMore, +}: { + provider: LiquidityAssetProvider; + operationCount: number; + hasMore: boolean; + loadingMore: boolean; + onLoadMore: () => void; +}) { const tracked = provider.tracked; if (!tracked) { return ( @@ -301,10 +498,6 @@ function LiquidityAssetCard({ provider }: { provider: LiquidityAssetProvider }) ); } - const opsWithType: { op: AssetOperation; type: LpOpType }[] = [ - ...tracked.invoices.map((op) => ({ op, type: "invoice" as LpOpType })), - ...tracked.payments.map((op) => ({ op, type: "payment" as LpOpType })), - ].sort((a, b) => b.op.ts - a.op.ts); return ( @@ -315,88 +508,34 @@ function LiquidityAssetCard({ provider }: { provider: LiquidityAssetProvider }) -
-
-
- Balance - {formatSats(tracked.balance)} -
+
+
+ Balance + {formatSats(tracked.balance)}
- {opsWithType.length > 0 && ( - <> - - Invoices & payments ({opsWithType.length}) - - - {opsWithType.slice(0, 10).map(({ op, type }, i) => { - const isTracked = op.tracked != null; - const discrepancy = hasDiscrepancy(op); - const mismatch = getMismatch(op); - return ( - - - -
- {formatSats(op.amount)} - - {formatTs(op.ts)} - -
- {mismatch && op.tracked && ( -
- {mismatch.amount && ( -
Amount: root {formatSats(op.amount)} → tracked {formatSats(op.tracked.amount)}
- )} - {mismatch.ts && ( -
Time: root {formatTs(op.ts)} → tracked {formatTs(op.tracked.ts)}
- )} -
- )} -
-
- {isTracked ? ( - <> -
- - - Tracked ({trackedTypeLabel(op.tracked!.type)}) - -
- {mismatch && ( - - {mismatch.amount && mismatch.ts ? "Amount + time mismatch" : mismatch.amount ? "Amount mismatch" : "Time mismatch"} - - )} - - ) : ( -
- - Untracked -
- )} -
-
- ); - })} -
- - )}
+ {operationCount > 0 && ( + + {operationCount} operation{operationCount === 1 ? "" : "s"} loaded + + )} + {hasMore && ( + + {loadingMore ? "Loading..." : "Load more operations"} + + )} ); @@ -412,39 +551,210 @@ export const AssetsAndLiab = () => { ); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [limitInvoices, setLimitInvoices] = useState(0); - const [limitPayments, setLimitPayments] = useState(0); - const [limitLiquidityProviders, setLimitLiquidityProviders] = useState(0); const [assetsAndLiabilities, setAssetsAndLiabilities] = useState({ liquidity_providers: [], lnds: [], users_balance: 0 }); + const [providerHasMore, setProviderHasMore] = useState>({}); + const [loadingMorePubkey, setLoadingMorePubkey] = useState(null); const [presentLoading, dismissLoading] = useIonLoading(); + const fetchInFlightRef = useRef(false); + const autoFetchedForSourceIdRef = useRef(null); + const lpCursorRef = useRef>({}); + + const initProviderHasMore = useCallback((data: AssetsAndLiabilities) => { + const next: Record = {}; + for (const lnd of data.lnds) { + if (lnd.tracked) next[lnd.pubkey] = true; + } + for (const lp of data.liquidity_providers) { + if (lp.tracked) next[lp.pubkey] = true; + } + setProviderHasMore(next); + }, []); + + const runFetch = useCallback(async ( + req: AssetsAndLiabilitiesReq, + showFullLoading: boolean, + ): Promise => { + if (!adminSource || fetchInFlightRef.current) return null; + + fetchInFlightRef.current = true; + try { + const res = await fetcher( + { pubkey: adminSource.lpk, relays: adminSource.relays }, + adminSource.keys, + { + onStart: async () => { + setError(null); + if (showFullLoading) { + setLoading(true); + await dismissLoading(); + await presentLoading("Fetching assets and liabilities..."); + } + }, + onEnd: async () => { + if (showFullLoading) { + setLoading(false); + await dismissLoading(); + } + }, + onFail: (err) => { + setError(err); + toast.error(err); + }, + }, + [(client) => client.GetAssetsAndLiabilities(req)] + ); + if (!res) return null; + const [data] = res; + return data?.status === "OK" ? data : null; + } finally { + fetchInFlightRef.current = false; + } + }, [adminSource, dismissLoading, presentLoading]); + + const fetchDiscovery = useCallback(async () => { + const data = await runFetch({ lnd_providers: [], liquidity_providers: [] }, true); + if (!data) return; + setAssetsAndLiabilities(data); + lpCursorRef.current = {}; + initProviderHasMore(data); + }, [initProviderHasMore, runFetch]); - const fetch = (funcs: FetcherFuncs, loadingMessage: string) => { - if (!adminSource) return; - return fetcher({ pubkey: adminSource.lpk, relays: adminSource.relays }, adminSource.keys, { - onStart: async () => { setError(null); setLoading(true); await dismissLoading(); await presentLoading(loadingMessage); }, - onEnd: async () => { setLoading(false); await dismissLoading(); }, - onFail: (err) => { setError(err); toast.error(err); }, - }, funcs); - }; - - const fetchAssetsAndLiabilities = async () => { - const req: AssetsAndLiabilitiesReq = { - limit_invoices: limitInvoices || undefined, - limit_payments: limitPayments || undefined, - limit_providers: limitLiquidityProviders || undefined, + const loadMoreLnd = useCallback(async (pubkey: string) => { + const provider = assetsAndLiabilities.lnds.find((lnd) => lnd.pubkey === pubkey); + if (!provider?.tracked || fetchInFlightRef.current) return; + + const filter: LndProviderFilter = { + pubkey, + limit_invoices: OPERATIONS_PAGE_SIZE, + limit_payments: OPERATIONS_PAGE_SIZE, + skip_invoices: provider.tracked.invoices.length, + skip_payments: provider.tracked.payments.length, }; - const res = await fetch([(client) => client.GetAssetsAndLiabilities(req)], "Fetching assets and liabilities..."); - if (!res) return; - const [data] = res; - if (data && data.status === "OK") { - setAssetsAndLiabilities(data); - console.log(data); + + setLoadingMorePubkey(pubkey); + try { + const data = await runFetch({ lnd_providers: [filter], liquidity_providers: [] }, false); + if (!data) return; + + const incoming = data.lnds.find((lnd) => lnd.pubkey === pubkey); + if (!incoming?.tracked) { + setProviderHasMore((prev) => ({ ...prev, [pubkey]: false })); + return; + } + + const newInvoices = incoming.tracked.invoices; + const newPayments = incoming.tracked.payments; + const gotMore = newInvoices.length > 0 || newPayments.length > 0; + + setAssetsAndLiabilities((prev) => ({ + ...prev, + lnds: prev.lnds.map((lnd) => { + if (lnd.pubkey !== pubkey || !lnd.tracked) return lnd; + return { + ...lnd, + tracked: { + ...lnd.tracked, + confirmed_balance: incoming.tracked!.confirmed_balance, + unconfirmed_balance: incoming.tracked!.unconfirmed_balance, + channels_balance: incoming.tracked!.channels_balance, + invoices: mergeOperations(lnd.tracked.invoices, newInvoices), + payments: mergeOperations(lnd.tracked.payments, newPayments), + incoming_tx: mergeOperations(lnd.tracked.incoming_tx, incoming.tracked!.incoming_tx), + outgoing_tx: mergeOperations(lnd.tracked.outgoing_tx, incoming.tracked!.outgoing_tx), + }, + }; + }), + })); + + setProviderHasMore((prev) => ({ + ...prev, + [pubkey]: gotMore && (newInvoices.length >= OPERATIONS_PAGE_SIZE || newPayments.length >= OPERATIONS_PAGE_SIZE), + })); + } finally { + setLoadingMorePubkey(null); } - }; + }, [assetsAndLiabilities.lnds, runFetch]); + + const loadMoreLp = useCallback(async (pubkey: string) => { + const provider = assetsAndLiabilities.liquidity_providers.find((lp) => lp.pubkey === pubkey); + if (!provider?.tracked || fetchInFlightRef.current) return; + + const cursors = lpCursorRef.current[pubkey] ?? { + incoming: oldestCursor(provider.tracked.invoices), + outgoing: oldestCursor(provider.tracked.payments), + }; + + const filter: LiquidityProviderFilter = { + pubkey, + limit: OPERATIONS_PAGE_SIZE, + ...(cursors.incoming ? { latestIncomingInvoice: cursors.incoming } : {}), + ...(cursors.outgoing ? { latestOutgoingInvoice: cursors.outgoing } : {}), + }; + + setLoadingMorePubkey(pubkey); + try { + const data = await runFetch({ lnd_providers: [], liquidity_providers: [filter] }, false); + if (!data) return; + + const incoming = data.liquidity_providers.find((lp) => lp.pubkey === pubkey); + if (!incoming?.tracked) { + setProviderHasMore((prev) => ({ ...prev, [pubkey]: false })); + return; + } + + const newInvoices = incoming.tracked.invoices; + const newPayments = incoming.tracked.payments; + const gotMore = newInvoices.length > 0 || newPayments.length > 0; + + setAssetsAndLiabilities((prev) => ({ + ...prev, + liquidity_providers: prev.liquidity_providers.map((lp) => { + if (lp.pubkey !== pubkey || !lp.tracked) return lp; + const mergedInvoices = mergeOperations(lp.tracked.invoices, newInvoices); + const mergedPayments = mergeOperations(lp.tracked.payments, newPayments); + lpCursorRef.current[pubkey] = { + incoming: oldestCursor(mergedInvoices), + outgoing: oldestCursor(mergedPayments), + }; + return { + ...lp, + tracked: { + ...lp.tracked, + balance: incoming.tracked!.balance, + invoices: mergedInvoices, + payments: mergedPayments, + }, + }; + }), + })); + + setProviderHasMore((prev) => ({ + ...prev, + [pubkey]: gotMore && (newInvoices.length + newPayments.length) >= OPERATIONS_PAGE_SIZE, + })); + } finally { + setLoadingMorePubkey(null); + } + }, [assetsAndLiabilities.liquidity_providers, runFetch]); + + const fetchDiscoveryRef = useRef(fetchDiscovery); + fetchDiscoveryRef.current = fetchDiscovery; + + const sourceId = adminSource?.sourceId; + + useEffect(() => { + autoFetchedForSourceIdRef.current = null; + lpCursorRef.current = {}; + setProviderHasMore({}); + }, [selectedId]); useEffect(() => { - fetchAssetsAndLiabilities(); - }, [adminSource?.sourceId]); + if (!sourceId) return; + if (autoFetchedForSourceIdRef.current === sourceId) return; + + autoFetchedForSourceIdRef.current = sourceId; + void fetchDiscoveryRef.current(); + }, [sourceId]); const totalAssets = useMemo(() => { let sum = 0; @@ -462,6 +772,11 @@ export const AssetsAndLiab = () => { return sum; }, [assetsAndLiabilities]); + const allOperations = useMemo( + () => collectAllOperations(assetsAndLiabilities), + [assetsAndLiabilities] + ); + return ( @@ -482,7 +797,7 @@ export const AssetsAndLiab = () => {
Something went wrong
{error}
- void fetchAssetsAndLiabilities()}>Retry + void fetchDiscovery()}>Retry router.push("/metrics/select", "back")}> Change Source @@ -496,7 +811,12 @@ export const AssetsAndLiab = () => {
Summary - void fetchAssetsAndLiabilities()}> + void fetchDiscovery()} + > Refresh
@@ -590,36 +910,53 @@ export const AssetsAndLiab = () => { - - LND assets + + Provider balances - {assetsAndLiabilities.lnds.length === 0 && ( + {assetsAndLiabilities.lnds.length === 0 && assetsAndLiabilities.liquidity_providers.length === 0 && ( - - No LND providers + + No providers )} {assetsAndLiabilities.lnds.map((lnd, i) => ( - + void loadMoreLnd(lnd.pubkey)} + /> ))} - - - - Liquidity provider assets - - {assetsAndLiabilities.liquidity_providers.length === 0 && ( - - - - No liquidity providers - - - )} {assetsAndLiabilities.liquidity_providers.map((lp, i) => ( - + void loadMoreLp(lp.pubkey)} + /> ))} + + + + + Operations + {allOperations.length > 0 && ( + + ({allOperations.length}) + + )} + + + + + + )} diff --git a/src/Pages/Metrics/UserOperationsAdmin.tsx b/src/Pages/Metrics/UserOperationsAdmin.tsx new file mode 100644 index 00000000..ce64f376 --- /dev/null +++ b/src/Pages/Metrics/UserOperationsAdmin.tsx @@ -0,0 +1,220 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { RouteComponentProps } from "react-router-dom"; +import { + IonBadge, + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCardTitle, + IonContent, + IonHeader, + IonPage, + IonText, + useIonLoading, + useIonRouter, +} from "@ionic/react"; +import { toast } from "react-toastify"; +import { UserAdminInfo, UserOperation } from "@/Api/pub/autogenerated/ts/types"; +import MetricsSubPageToolbar from "@/Layout2/Metrics/MetricsSubPageToolbar"; +import { useAppSelector } from "@/State/store/hooks"; +import { selectAdminNprofileViews } from "@/State/scoped/backups/sources/selectors"; +import { selectSelectedMetricsAdminSourceId } from "@/State/runtime/slice"; +import { emptyCursor, parseOperationsResponse } from "@/State/scoped/backups/sources/history/helpers"; +import type { HistoryCursor } from "@/State/scoped/backups/sources/history/types"; +import { AdminOperationsList } from "./adminOperationDisplay"; +import { fetcher } from "./fetcher"; +import { formatTableAmount } from "./metricsDataTable"; + +type LocationState = { user?: UserAdminInfo }; + +function formatSats(n: number): string { + return formatTableAmount(n) + " sats"; +} + +function trimId(id: string): string { + return id.length <= 12 ? id : `${id.slice(0, 6)}…${id.slice(-4)}`; +} + +export default function UserOperationsAdmin({ match, location, history }: RouteComponentProps) { + const router = useIonRouter(); + const { userId } = match.params as { userId: string }; + const userFromState = (location.state as LocationState | undefined)?.user; + + const admins = useAppSelector(selectAdminNprofileViews); + const selectedId = useAppSelector(selectSelectedMetricsAdminSourceId); + const adminSource = useMemo( + () => admins.find((a) => a.sourceId === selectedId), + [admins, selectedId] + ); + + const [operations, setOperations] = useState([]); + const [cursor, setCursor] = useState(emptyCursor); + const [hasMore, setHasMore] = useState(false); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [error, setError] = useState(null); + const [presentLoading, dismissLoading] = useIonLoading(); + + const fetchOperations = useCallback(async (nextCursor: HistoryCursor, append: boolean) => { + if (!adminSource) return; + + const res = await fetcher( + { pubkey: adminSource.lpk, relays: adminSource.relays }, + adminSource.keys, + { + onStart: async () => { + setError(null); + if (append) { + setLoadingMore(true); + } else { + setLoading(true); + await dismissLoading(); + await presentLoading("Fetching operations..."); + } + }, + onEnd: async () => { + if (append) { + setLoadingMore(false); + } else { + setLoading(false); + await dismissLoading(); + } + }, + onFail: (err) => { + setError(err); + toast.error(err); + }, + }, + [(client) => client.GetUserOperationsFromAdmin({ ...nextCursor, user_id: userId })] + ); + + if (!res) return; + + const { operations: batch, newCursor, needMoreData } = parseOperationsResponse(res[0], nextCursor); + setOperations((prev) => (append ? [...prev, ...batch] : batch)); + setCursor(newCursor); + setHasMore(needMoreData); + }, [adminSource, dismissLoading, presentLoading, userId]); + + useEffect(() => { + setOperations([]); + setCursor(emptyCursor()); + setHasMore(false); + void fetchOperations(emptyCursor(), false); + }, [fetchOperations, userId]); + + const loadMore = () => { + if (loadingMore || !hasMore) return; + void fetchOperations(cursor, true); + }; + + return ( + + + + + + + {error && ( +
+
Something went wrong
+
{error}
+
+ void fetchOperations(emptyCursor(), false)}>Retry + history.push("/metrics/users")}> + Back to users + + router.push("/metrics/select", "back")}> + Change Source + +
+
+ )} + + {!error && ( + <> + + + {trimId(userId)} + + + {userFromState ? ( +
+
+ Balance + {formatSats(userFromState.balance)} +
+ {userFromState.locked && ( +
+ Status + Locked +
+ )} + {userFromState.owner_of_app_id && ( +
+ App owner + {trimId(userFromState.owner_of_app_id)} +
+ )} + {userFromState.app_users.length > 0 && ( +
+ Linked app users + {userFromState.app_users.length} +
+ )} +
+ ) : ( + User summary unavailable — open from the users list for details. + )} +
+
+ + + + + Operations + {operations.length > 0 && ( + + ({operations.length}) + + )} + + + + {!loading && operations.length > 0 && ( + + {operations.length} operation{operations.length === 1 ? "" : "s"} loaded + + )} + + {!loading && } + + {hasMore && ( + + {loadingMore ? "Loading..." : "Load more operations"} + + )} + + + + )} +
+
+ ); +} diff --git a/src/Pages/Metrics/UsersAdmin.tsx b/src/Pages/Metrics/UsersAdmin.tsx new file mode 100644 index 00000000..d8d15ce2 --- /dev/null +++ b/src/Pages/Metrics/UsersAdmin.tsx @@ -0,0 +1,194 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { RouteComponentProps } from "react-router-dom"; +import { + IonBadge, + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCardTitle, + IonContent, + IonHeader, + IonPage, + IonText, + useIonLoading, + useIonRouter, +} from "@ionic/react"; +import { toast } from "react-toastify"; +import { UserAdminInfo, UsersAdminInfo } from "@/Api/pub/autogenerated/ts/types"; +import MetricsSubPageToolbar from "@/Layout2/Metrics/MetricsSubPageToolbar"; +import { useAppSelector } from "@/State/store/hooks"; +import { selectAdminNprofileViews } from "@/State/scoped/backups/sources/selectors"; +import { selectSelectedMetricsAdminSourceId } from "@/State/runtime/slice"; +import { fetcher } from "./fetcher"; +import { formatTableAmount, MetricsDataTable, MetricsTableEmpty, MetricsTableRow } from "./metricsDataTable"; + +const PAGE_SIZE = 50; +const USERS_GRID = "minmax(100px, 1.2fr) minmax(90px, 1fr) minmax(120px, 1.5fr) 68px"; + +function trimId(id: string): string { + return id.length <= 12 ? id : `${id.slice(0, 6)}…${id.slice(-4)}`; +} + +export default function UsersAdmin({ history }: RouteComponentProps) { + const router = useIonRouter(); + const admins = useAppSelector(selectAdminNprofileViews); + const selectedId = useAppSelector(selectSelectedMetricsAdminSourceId); + const adminSource = useMemo( + () => admins.find((a) => a.sourceId === selectedId), + [admins, selectedId] + ); + + const [page, setPage] = useState(0); + const [data, setData] = useState({ total: 0, users: [] }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [presentLoading, dismissLoading] = useIonLoading(); + + const skip = page * PAGE_SIZE; + const rangeStart = data.total === 0 ? 0 : skip + 1; + const rangeEnd = Math.min(skip + data.users.length, data.total); + const hasPrev = page > 0; + const hasNext = skip + data.users.length < data.total; + + const fetchUsers = useCallback(async () => { + if (!adminSource) return; + + const res = await fetcher( + { pubkey: adminSource.lpk, relays: adminSource.relays }, + adminSource.keys, + { + onStart: async () => { + setError(null); + setLoading(true); + await dismissLoading(); + await presentLoading("Fetching users..."); + }, + onEnd: async () => { + setLoading(false); + await dismissLoading(); + }, + onFail: (err) => { + setError(err); + toast.error(err); + }, + }, + [(client) => client.GetUsersAdminInfo({ skip, take: PAGE_SIZE })] + ); + + if (res) { + setData(res[0]); + } + }, [adminSource, dismissLoading, presentLoading, skip]); + + useEffect(() => { + void fetchUsers(); + }, [fetchUsers]); + + const openUser = (user: UserAdminInfo) => { + history.push(`/metrics/users/${user.user_id}`, { user }); + }; + + return ( + + + + + + + {error && ( +
+
Something went wrong
+
{error}
+
+ void fetchUsers()}>Retry + router.push("/metrics/select", "back")}> + Change Source + +
+
+ )} + + {!error && ( + <> + + + + Users + {data.total > 0 && ( + + ({data.total.toLocaleString()}) + + )} + + + + + {data.total === 0 && !loading + ? "No users" + : `Showing ${rangeStart}–${rangeEnd} of ${data.total.toLocaleString()}`} + + + {data.users.length === 0 && !loading ? ( + + ) : ( + + User + Balance + Details + Status + + } + > + {data.users.map((user, i) => ( + openUser(user)} + > + + {trimId(user.user_id)} + + + {formatTableAmount(user.balance)} + + + {user.app_users.length} app user{user.app_users.length === 1 ? "" : "s"} + {user.owner_of_app_id ? ` · owns ${trimId(user.owner_of_app_id)}` : ""} + +
+ {user.locked ? ( + Locked + ) : ( + + )} +
+
+ ))} +
+ )} + +
+ setPage((p) => p - 1)}> + Previous + + setPage((p) => p + 1)}> + Next + + void fetchUsers()}> + Refresh + +
+
+
+ + )} +
+
+ ); +} diff --git a/src/Pages/Metrics/adminOperationDisplay.tsx b/src/Pages/Metrics/adminOperationDisplay.tsx new file mode 100644 index 00000000..712e71ac --- /dev/null +++ b/src/Pages/Metrics/adminOperationDisplay.tsx @@ -0,0 +1,104 @@ +import { IonIcon } from "@ionic/react"; +import { flashOutline, linkOutline, personOutline } from "ionicons/icons"; +import { UserOperation, UserOperationType } from "@/Api/pub/autogenerated/ts/types"; +import { + formatTableAmount, + formatTableTs, + MetricsDataTable, + MetricsTableEmpty, + MetricsTableRow, +} from "./metricsDataTable"; + +const OPERATIONS_GRID = "minmax(100px, 1.2fr) minmax(110px, 1.2fr) minmax(72px, 90px) minmax(72px, 90px)"; + +export function operationIconType(t: UserOperationType) { + switch (t) { + case UserOperationType.INCOMING_INVOICE: + case UserOperationType.OUTGOING_INVOICE: + return flashOutline; + case UserOperationType.INCOMING_USER_TO_USER: + case UserOperationType.OUTGOING_USER_TO_USER: + return personOutline; + case UserOperationType.INCOMING_TX: + case UserOperationType.OUTGOING_TX: + return linkOutline; + } +} + +export function operationIconColor(t: UserOperationType) { + switch (t) { + case UserOperationType.INCOMING_INVOICE: + case UserOperationType.INCOMING_USER_TO_USER: + case UserOperationType.INCOMING_TX: + return "var(--ion-color-success)"; + case UserOperationType.OUTGOING_INVOICE: + case UserOperationType.OUTGOING_USER_TO_USER: + case UserOperationType.OUTGOING_TX: + return "var(--ion-color-danger)"; + default: + return "var(--ion-color-medium)"; + } +} + +function operationTypeLabel(t: UserOperationType): string { + switch (t) { + case UserOperationType.INCOMING_INVOICE: + return "Incoming invoice"; + case UserOperationType.OUTGOING_INVOICE: + return "Outgoing invoice"; + case UserOperationType.INCOMING_USER_TO_USER: + return "Incoming U2U"; + case UserOperationType.OUTGOING_USER_TO_USER: + return "Outgoing U2U"; + case UserOperationType.INCOMING_TX: + return "Incoming tx"; + case UserOperationType.OUTGOING_TX: + return "Outgoing tx"; + } +} + +export function AdminOperationsList({ operations }: { operations: UserOperation[] }) { + const sorted = [...operations].sort((a, b) => b.paidAtUnix - a.paidAtUnix); + + if (sorted.length === 0) { + return ; + } + + return ( + + Op type + Date + Amount + Service fee + + } + > + {sorted.map((op, i) => ( + +
+ + + {operationTypeLabel(op.type)} + +
+ + {formatTableTs(op.paidAtUnix)} + + + {formatTableAmount(op.amount)} + + + {formatTableAmount(op.service_fee)} + +
+ ))} +
+ ); +} diff --git a/src/Pages/Metrics/earnings.tsx b/src/Pages/Metrics/earnings.tsx index 9cf1c768..3c632333 100644 --- a/src/Pages/Metrics/earnings.tsx +++ b/src/Pages/Metrics/earnings.tsx @@ -3,13 +3,14 @@ import { Period } from "../../Components/Dropdowns/LVDropdown" import { getNostrClient } from "@/Api/nostr" import { toast } from "react-toastify"; import * as Types from '../../Api/pub/autogenerated/ts/types'; -import { IonButton, IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonContent, IonLabel, IonItem, IonList, IonListHeader, IonIcon, IonPage, IonProgressBar, IonSkeletonText, IonHeader, useIonLoading, useIonRouter } from "@ionic/react"; -import { flashOutline, linkOutline, personOutline } from "ionicons/icons"; +import { IonButton, IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonContent, IonPage, IonText, IonHeader, useIonLoading, useIonRouter } from "@ionic/react"; import PeriodSelector from "@/Components/Dropdowns/PeriodDropdown/PeriodSelector"; import MetricsSubPageToolbar from "@/Layout2/Metrics/MetricsSubPageToolbar"; import { useAppSelector } from "@/State/store/hooks"; import { selectAdminNprofileViews } from "@/State/scoped/backups/sources/selectors"; import { selectSelectedMetricsAdminSourceId } from "@/State/runtime/slice"; +import { AdminOperationsList } from "./adminOperationDisplay"; +import { formatTableAmount } from "./metricsDataTable"; export default function Earnings() { const router = useIonRouter(); @@ -124,50 +125,65 @@ export default function Earnings() { /> {metrics.apps.map((app, i) => ( - + {app.app.name} - - {!loading && app.operations.length > 0 && ( + {!loading && ( -
Moved {app.received + app.spent} sats in {app.operations.length} operations
-
Earned {app.fees} sats
- - {showingOps !== app.app.name && ( - setShowingOps(app.app.name)}>Show operations - )} - {showingOps === app.app.name && ( - setShowingOps("")}>Hide operations +
+
+ Moved + + {formatTableAmount(app.received + app.spent)} sats + +
+
+ Operations + + {app.operations.length} + +
+
+ Earned + + {formatTableAmount(app.fees)} sats + +
+
+ + {app.operations.length > 0 && ( + <> + {showingOps !== app.app.name ? ( + setShowingOps(app.app.name)}> + Show operations + + ) : ( + <> + setShowingOps("")}> + Hide operations + +
+ +
+ + )} + )} - {showingOps === app.app.name && ( - - - Op type - Amount - Service fee - - {app.operations.map((op, j) => ( - - - - - {op.amount} - {op.service_fee} - - ))} - + {app.operations.length === 0 && ( + No operations )}
)} - - {!loading && app.operations.length === 0 && ( - -
No operations
-
- )}
))} @@ -217,31 +233,3 @@ export const getUnixTimeRange = (period: Period, offset: number) => { return { from_unix, to_unix }; } -const iconType = (t: Types.UserOperationType) => { - switch (t) { - case Types.UserOperationType.INCOMING_INVOICE: - case Types.UserOperationType.OUTGOING_INVOICE: - return flashOutline - case Types.UserOperationType.INCOMING_USER_TO_USER: - case Types.UserOperationType.OUTGOING_USER_TO_USER: - return personOutline - case Types.UserOperationType.INCOMING_TX: - case Types.UserOperationType.OUTGOING_TX: - return linkOutline - } -} - -const iconColor = (t: Types.UserOperationType) => { - switch (t) { - case Types.UserOperationType.INCOMING_INVOICE: - case Types.UserOperationType.INCOMING_USER_TO_USER: - case Types.UserOperationType.INCOMING_TX: - return 'green' - case Types.UserOperationType.OUTGOING_INVOICE: - case Types.UserOperationType.OUTGOING_USER_TO_USER: - case Types.UserOperationType.OUTGOING_TX: - return 'red' - default: - return 'white' - } -} diff --git a/src/Pages/Metrics/index.tsx b/src/Pages/Metrics/index.tsx index a3af29d8..8ae34232 100644 --- a/src/Pages/Metrics/index.tsx +++ b/src/Pages/Metrics/index.tsx @@ -17,6 +17,8 @@ import Manage from "../Manage"; import Channels from "../Channels"; import AdminSwaps from "./adminSwaps/AdminSwaps"; import { AssetsAndLiab } from "./AssetsAndLiab"; +import UsersAdmin from "./UsersAdmin"; +import UserOperationsAdmin from "./UserOperationsAdmin"; const Metrics = ({ match, location, history }: RouteComponentProps) => { @@ -56,6 +58,8 @@ const Metrics = ({ match, location, history }: RouteComponentProps) => { + + ); diff --git a/src/Pages/Metrics/metricsDataTable.tsx b/src/Pages/Metrics/metricsDataTable.tsx new file mode 100644 index 00000000..ec8eab9f --- /dev/null +++ b/src/Pages/Metrics/metricsDataTable.tsx @@ -0,0 +1,109 @@ +import { IonText } from "@ionic/react"; +import type { CSSProperties, ReactNode } from "react"; + +export function formatTableTs(unix: number): string { + if (!unix) return "—"; + const d = new Date(unix * 1000); + const pad = (n: number) => String(n).padStart(2, "0"); + const day = pad(d.getDate()); + const month = pad(d.getMonth() + 1); + const year = String(d.getFullYear()).slice(-2); + const hours = pad(d.getHours()); + const minutes = pad(d.getMinutes()); + const seconds = pad(d.getSeconds()); + return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`; +} + +export function formatTableAmount(n: number): string { + return n.toLocaleString("fr-FR"); +} + +const TABLE_SHELL: CSSProperties = { + borderRadius: "8px", + border: "1px solid var(--ion-color-medium-tint)", + overflow: "hidden", +}; + +const TABLE_HEADER: CSSProperties = { + display: "grid", + gap: "6px", + padding: "10px 12px", + fontSize: "0.75rem", + color: "var(--ion-color-medium)", + borderBottom: "1px solid var(--ion-color-medium-tint)", +}; + +const TABLE_ROW_BASE: CSSProperties = { + display: "grid", + gap: "6px", + padding: "10px 12px", + alignItems: "center", + fontSize: "0.85rem", +}; + +export function MetricsTableEmpty({ message }: { message: string }) { + return ( + + {message} + + ); +} + +export function MetricsDataTable({ + grid, + minWidth = "480px", + header, + children, +}: { + grid: string; + minWidth?: string; + header: ReactNode; + children: ReactNode; +}) { + return ( +
+
+
{header}
+ {children} +
+
+ ); +} + +export function MetricsTableRow({ + grid, + isLast, + onClick, + children, +}: { + grid: string; + isLast: boolean; + onClick?: () => void; + children: ReactNode; +}) { + return ( +
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(); + } + } + : undefined + } + style={{ + ...TABLE_ROW_BASE, + gridTemplateColumns: grid, + borderBottom: isLast ? undefined : "1px solid var(--ion-color-medium-tint)", + cursor: onClick ? "pointer" : undefined, + }} + > + {children} +
+ ); +} diff --git a/src/Pages/Metrics/metricsMain.tsx b/src/Pages/Metrics/metricsMain.tsx index 76c49128..adc6730f 100644 --- a/src/Pages/Metrics/metricsMain.tsx +++ b/src/Pages/Metrics/metricsMain.tsx @@ -497,6 +497,11 @@ const Dashboard = () => { Assets & Liabilities
+
+ + Users + +
From d87bf10009020e960e41fa04b0314a966abaabf3 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Thu, 2 Jul 2026 16:16:52 +0000 Subject: [PATCH 2/4] fix contrast --- src/Pages/Metrics/AssetsAndLiab.tsx | 7 ++++--- src/Pages/Metrics/UsersAdmin.tsx | 2 +- src/Pages/Metrics/adminOperationDisplay.tsx | 2 +- src/Pages/Metrics/metricsDataTable.tsx | 7 +++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Pages/Metrics/AssetsAndLiab.tsx b/src/Pages/Metrics/AssetsAndLiab.tsx index 87c0dad7..5b507d85 100644 --- a/src/Pages/Metrics/AssetsAndLiab.tsx +++ b/src/Pages/Metrics/AssetsAndLiab.tsx @@ -270,7 +270,7 @@ function AssetOperationsTable({ operations }: { operations: UnifiedAssetOperatio
- {formatTableTs(op.ts)} + {formatTableTs(op.ts)}
{formatTableAmount(user.balance)} - + {user.app_users.length} app user{user.app_users.length === 1 ? "" : "s"} {user.owner_of_app_id ? ` · owns ${trimId(user.owner_of_app_id)}` : ""} diff --git a/src/Pages/Metrics/adminOperationDisplay.tsx b/src/Pages/Metrics/adminOperationDisplay.tsx index 712e71ac..032e8fff 100644 --- a/src/Pages/Metrics/adminOperationDisplay.tsx +++ b/src/Pages/Metrics/adminOperationDisplay.tsx @@ -88,7 +88,7 @@ export function AdminOperationsList({ operations }: { operations: UserOperation[ {operationTypeLabel(op.type)}
- + {formatTableTs(op.paidAtUnix)} diff --git a/src/Pages/Metrics/metricsDataTable.tsx b/src/Pages/Metrics/metricsDataTable.tsx index ec8eab9f..cc302aa0 100644 --- a/src/Pages/Metrics/metricsDataTable.tsx +++ b/src/Pages/Metrics/metricsDataTable.tsx @@ -20,7 +20,7 @@ export function formatTableAmount(n: number): string { const TABLE_SHELL: CSSProperties = { borderRadius: "8px", - border: "1px solid var(--ion-color-medium-tint)", + border: "1px solid rgba(var(--ion-color-medium-rgb), 0.18)", overflow: "hidden", }; @@ -39,8 +39,11 @@ const TABLE_ROW_BASE: CSSProperties = { padding: "10px 12px", alignItems: "center", fontSize: "0.85rem", + color: "var(--ion-text-color)", }; +const TABLE_ROW_BORDER = "1px solid rgba(var(--ion-color-medium-rgb), 0.1)"; + export function MetricsTableEmpty({ message }: { message: string }) { return ( @@ -99,7 +102,7 @@ export function MetricsTableRow({ style={{ ...TABLE_ROW_BASE, gridTemplateColumns: grid, - borderBottom: isLast ? undefined : "1px solid var(--ion-color-medium-tint)", + borderBottom: isLast ? undefined : TABLE_ROW_BORDER, cursor: onClick ? "pointer" : undefined, }} > From 1091fb55795e7bd3d38dae3b4e6ed4c445bdbc7a Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 6 Jul 2026 18:23:13 +0000 Subject: [PATCH 3/4] fixed pagination --- src/Api/pub/autogenerated/ts/types.ts | 187 +++++++++++++++++--------- src/Pages/Metrics/AssetsAndLiab.tsx | 157 +++++++++++---------- 2 files changed, 207 insertions(+), 137 deletions(-) diff --git a/src/Api/pub/autogenerated/ts/types.ts b/src/Api/pub/autogenerated/ts/types.ts index fa224979..1302c69c 100644 --- a/src/Api/pub/autogenerated/ts/types.ts +++ b/src/Api/pub/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 + + + const paymentsErr = LiquidityAssetOperationsPageValidate(o.payments, opts.payments_Options, `${path}.payments`) + if (paymentsErr !== null) return paymentsErr - 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 } @@ -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`) - 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 invoicesErr = LndAssetOperationsPageValidate(o.invoices, opts.invoices_Options, `${path}.invoices`) + if (invoicesErr !== null) return invoicesErr + + + const outgoing_txErr = LndAssetOperationsPageValidate(o.outgoing_tx, opts.outgoing_tx_Options, `${path}.outgoing_tx`) + if (outgoing_txErr !== null) return outgoing_txErr + + + const paymentsErr = LndAssetOperationsPageValidate(o.payments, opts.payments_Options, `${path}.payments`) + if (paymentsErr !== null) return paymentsErr - 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`) diff --git a/src/Pages/Metrics/AssetsAndLiab.tsx b/src/Pages/Metrics/AssetsAndLiab.tsx index 5b507d85..813daec2 100644 --- a/src/Pages/Metrics/AssetsAndLiab.tsx +++ b/src/Pages/Metrics/AssetsAndLiab.tsx @@ -22,11 +22,12 @@ import { AssetsAndLiabilities, AssetsAndLiabilitiesReq, AssetOperation, + LiquidityAssetOperationsPage, + LndAssetOperationsPage, LndAssetProvider, LndProviderFilter, LiquidityAssetProvider, LiquidityProviderFilter, - OperationsCursor, TrackedLiquidityProvider, TrackedLndProvider, TrackedOperationType, @@ -151,17 +152,17 @@ function collectAllOperations(data: AssetsAndLiabilities): UnifiedAssetOperation for (const lnd of data.lnds) { if (!lnd.tracked) continue; const { tracked } = lnd; - for (const op of tracked.invoices) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "invoice" }); - for (const op of tracked.payments) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "payment" }); - for (const op of tracked.incoming_tx) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "incoming_tx" }); - for (const op of tracked.outgoing_tx) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "outgoing_tx" }); + for (const op of tracked.invoices.operations) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "invoice" }); + for (const op of tracked.payments.operations) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "payment" }); + for (const op of tracked.incoming_tx.operations) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "incoming_tx" }); + for (const op of tracked.outgoing_tx.operations) rows.push({ op, providerKind: "lnd", providerPubkey: lnd.pubkey, opKind: "outgoing_tx" }); } for (const lp of data.liquidity_providers) { if (!lp.tracked) continue; const { tracked } = lp; - for (const op of tracked.invoices) rows.push({ op, providerKind: "lp", providerPubkey: lp.pubkey, opKind: "invoice" }); - for (const op of tracked.payments) rows.push({ op, providerKind: "lp", providerPubkey: lp.pubkey, opKind: "payment" }); + for (const op of tracked.invoices.operations) rows.push({ op, providerKind: "lp", providerPubkey: lp.pubkey, opKind: "invoice" }); + for (const op of tracked.payments.operations) rows.push({ op, providerKind: "lp", providerPubkey: lp.pubkey, opKind: "payment" }); } return rows.sort((a, b) => b.op.ts - a.op.ts); @@ -241,17 +242,41 @@ function mergeOperations(existing: AssetOperation[], incoming: AssetOperation[]) return merged.sort((a, b) => b.ts - a.ts); } -function oldestCursor(ops: AssetOperation[]): OperationsCursor | undefined { - if (ops.length === 0) return undefined; - const oldest = ops.reduce((a, b) => (a.ts <= b.ts ? a : b)); - return { ts: oldest.ts, id: 0 }; +function mergeLndPage(existing: LndAssetOperationsPage, incoming: LndAssetOperationsPage): LndAssetOperationsPage { + return { + has_more: incoming.has_more, + next_index_offset: incoming.next_index_offset, + operations: mergeOperations(existing.operations, incoming.operations), + }; +} + +function mergeLpPage(existing: LiquidityAssetOperationsPage, incoming: LiquidityAssetOperationsPage): LiquidityAssetOperationsPage { + return { + has_more: incoming.has_more, + next_cursor: incoming.next_cursor, + operations: mergeOperations(existing.operations, incoming.operations), + }; +} + +function lndProviderHasMore(tracked: TrackedLndProvider): boolean { + return tracked.invoices.has_more + || tracked.payments.has_more + || tracked.incoming_tx.has_more + || tracked.outgoing_tx.has_more; +} + +function lpProviderHasMore(tracked: TrackedLiquidityProvider): boolean { + return tracked.invoices.has_more || tracked.payments.has_more; } function countProviderOperations(tracked: TrackedLndProvider | TrackedLiquidityProvider): number { if ("incoming_tx" in tracked) { - return tracked.invoices.length + tracked.payments.length + tracked.incoming_tx.length + tracked.outgoing_tx.length; + return tracked.invoices.operations.length + + tracked.payments.operations.length + + tracked.incoming_tx.operations.length + + tracked.outgoing_tx.operations.length; } - return tracked.invoices.length + tracked.payments.length; + return tracked.invoices.operations.length + tracked.payments.operations.length; } const TABLE_GRID = "minmax(80px, 1.1fr) 100px 52px 36px minmax(68px, 80px) minmax(68px, 80px)"; @@ -558,15 +583,13 @@ export const AssetsAndLiab = () => { const [presentLoading, dismissLoading] = useIonLoading(); const fetchInFlightRef = useRef(false); const autoFetchedForSourceIdRef = useRef(null); - const lpCursorRef = useRef>({}); - const initProviderHasMore = useCallback((data: AssetsAndLiabilities) => { const next: Record = {}; for (const lnd of data.lnds) { - if (lnd.tracked) next[lnd.pubkey] = true; + if (lnd.tracked) next[lnd.pubkey] = lndProviderHasMore(lnd.tracked); } for (const lp of data.liquidity_providers) { - if (lp.tracked) next[lp.pubkey] = true; + if (lp.tracked) next[lp.pubkey] = lpProviderHasMore(lp.tracked); } setProviderHasMore(next); }, []); @@ -616,7 +639,6 @@ export const AssetsAndLiab = () => { const data = await runFetch({ lnd_providers: [], liquidity_providers: [] }, true); if (!data) return; setAssetsAndLiabilities(data); - lpCursorRef.current = {}; initProviderHasMore(data); }, [initProviderHasMore, runFetch]); @@ -624,12 +646,22 @@ export const AssetsAndLiab = () => { const provider = assetsAndLiabilities.lnds.find((lnd) => lnd.pubkey === pubkey); if (!provider?.tracked || fetchInFlightRef.current) return; + const { tracked } = provider; const filter: LndProviderFilter = { pubkey, limit_invoices: OPERATIONS_PAGE_SIZE, limit_payments: OPERATIONS_PAGE_SIZE, - skip_invoices: provider.tracked.invoices.length, - skip_payments: provider.tracked.payments.length, + limit_transactions: OPERATIONS_PAGE_SIZE, + ...(tracked.invoices.has_more && tracked.invoices.next_index_offset != null + ? { invoice_index_offset: tracked.invoices.next_index_offset } + : {}), + ...(tracked.payments.has_more && tracked.payments.next_index_offset != null + ? { payment_index_offset: tracked.payments.next_index_offset } + : {}), + ...((tracked.incoming_tx.has_more || tracked.outgoing_tx.has_more) + && (tracked.incoming_tx.next_index_offset ?? tracked.outgoing_tx.next_index_offset) != null + ? { tx_index_offset: tracked.incoming_tx.next_index_offset ?? tracked.outgoing_tx.next_index_offset } + : {}), }; setLoadingMorePubkey(pubkey); @@ -643,33 +675,27 @@ export const AssetsAndLiab = () => { return; } - const newInvoices = incoming.tracked.invoices; - const newPayments = incoming.tracked.payments; - const gotMore = newInvoices.length > 0 || newPayments.length > 0; + const incomingTracked = incoming.tracked; + const existingTracked = provider.tracked; + const mergedTracked: TrackedLndProvider = { + confirmed_balance: incomingTracked.confirmed_balance, + unconfirmed_balance: incomingTracked.unconfirmed_balance, + channels_balance: incomingTracked.channels_balance, + invoices: mergeLndPage(existingTracked.invoices, incomingTracked.invoices), + payments: mergeLndPage(existingTracked.payments, incomingTracked.payments), + incoming_tx: mergeLndPage(existingTracked.incoming_tx, incomingTracked.incoming_tx), + outgoing_tx: mergeLndPage(existingTracked.outgoing_tx, incomingTracked.outgoing_tx), + }; setAssetsAndLiabilities((prev) => ({ ...prev, - lnds: prev.lnds.map((lnd) => { - if (lnd.pubkey !== pubkey || !lnd.tracked) return lnd; - return { - ...lnd, - tracked: { - ...lnd.tracked, - confirmed_balance: incoming.tracked!.confirmed_balance, - unconfirmed_balance: incoming.tracked!.unconfirmed_balance, - channels_balance: incoming.tracked!.channels_balance, - invoices: mergeOperations(lnd.tracked.invoices, newInvoices), - payments: mergeOperations(lnd.tracked.payments, newPayments), - incoming_tx: mergeOperations(lnd.tracked.incoming_tx, incoming.tracked!.incoming_tx), - outgoing_tx: mergeOperations(lnd.tracked.outgoing_tx, incoming.tracked!.outgoing_tx), - }, - }; - }), + lnds: prev.lnds.map((lnd) => ( + lnd.pubkey === pubkey ? { ...lnd, tracked: mergedTracked } : lnd + )), })); - setProviderHasMore((prev) => ({ ...prev, - [pubkey]: gotMore && (newInvoices.length >= OPERATIONS_PAGE_SIZE || newPayments.length >= OPERATIONS_PAGE_SIZE), + [pubkey]: lndProviderHasMore(mergedTracked), })); } finally { setLoadingMorePubkey(null); @@ -680,16 +706,16 @@ export const AssetsAndLiab = () => { const provider = assetsAndLiabilities.liquidity_providers.find((lp) => lp.pubkey === pubkey); if (!provider?.tracked || fetchInFlightRef.current) return; - const cursors = lpCursorRef.current[pubkey] ?? { - incoming: oldestCursor(provider.tracked.invoices), - outgoing: oldestCursor(provider.tracked.payments), - }; - + const { tracked } = provider; const filter: LiquidityProviderFilter = { pubkey, limit: OPERATIONS_PAGE_SIZE, - ...(cursors.incoming ? { latestIncomingInvoice: cursors.incoming } : {}), - ...(cursors.outgoing ? { latestOutgoingInvoice: cursors.outgoing } : {}), + ...(tracked.invoices.has_more && tracked.invoices.next_cursor + ? { latestIncomingInvoice: tracked.invoices.next_cursor } + : {}), + ...(tracked.payments.has_more && tracked.payments.next_cursor + ? { latestOutgoingInvoice: tracked.payments.next_cursor } + : {}), }; setLoadingMorePubkey(pubkey); @@ -703,35 +729,23 @@ export const AssetsAndLiab = () => { return; } - const newInvoices = incoming.tracked.invoices; - const newPayments = incoming.tracked.payments; - const gotMore = newInvoices.length > 0 || newPayments.length > 0; + const incomingTracked = incoming.tracked; + const existingTracked = provider.tracked; + const mergedTracked: TrackedLiquidityProvider = { + balance: incomingTracked.balance, + invoices: mergeLpPage(existingTracked.invoices, incomingTracked.invoices), + payments: mergeLpPage(existingTracked.payments, incomingTracked.payments), + }; setAssetsAndLiabilities((prev) => ({ ...prev, - liquidity_providers: prev.liquidity_providers.map((lp) => { - if (lp.pubkey !== pubkey || !lp.tracked) return lp; - const mergedInvoices = mergeOperations(lp.tracked.invoices, newInvoices); - const mergedPayments = mergeOperations(lp.tracked.payments, newPayments); - lpCursorRef.current[pubkey] = { - incoming: oldestCursor(mergedInvoices), - outgoing: oldestCursor(mergedPayments), - }; - return { - ...lp, - tracked: { - ...lp.tracked, - balance: incoming.tracked!.balance, - invoices: mergedInvoices, - payments: mergedPayments, - }, - }; - }), + liquidity_providers: prev.liquidity_providers.map((lp) => ( + lp.pubkey === pubkey ? { ...lp, tracked: mergedTracked } : lp + )), })); - setProviderHasMore((prev) => ({ ...prev, - [pubkey]: gotMore && (newInvoices.length + newPayments.length) >= OPERATIONS_PAGE_SIZE, + [pubkey]: lpProviderHasMore(mergedTracked), })); } finally { setLoadingMorePubkey(null); @@ -745,7 +759,6 @@ export const AssetsAndLiab = () => { useEffect(() => { autoFetchedForSourceIdRef.current = null; - lpCursorRef.current = {}; setProviderHasMore({}); }, [selectedId]); From 9590a32eac2ace03fd655c70b65cdaa8083f9730 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 13 Jul 2026 17:10:03 +0000 Subject: [PATCH 4/4] split v2 from v1 --- src/Api/pub/autogenerated/ts/nostr_client.ts | 15 ++ .../pub/autogenerated/ts/nostr_transport.ts | 16 ++ src/Api/pub/autogenerated/ts/types.ts | 253 +++++++++++++++++- src/Pages/Metrics/AssetsAndLiab.tsx | 40 +-- 4 files changed, 293 insertions(+), 31 deletions(-) diff --git a/src/Api/pub/autogenerated/ts/nostr_client.ts b/src/Api/pub/autogenerated/ts/nostr_client.ts index 8c68ddfd..b0fc0d05 100644 --- a/src/Api/pub/autogenerated/ts/nostr_client.ts +++ b/src/Api/pub/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/src/Api/pub/autogenerated/ts/nostr_transport.ts b/src/Api/pub/autogenerated/ts/nostr_transport.ts index c4c474fb..bf786513 100644 --- a/src/Api/pub/autogenerated/ts/nostr_transport.ts +++ b/src/Api/pub/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/src/Api/pub/autogenerated/ts/types.ts b/src/Api/pub/autogenerated/ts/types.ts index 1302c69c..484c7f63 100644 --- a/src/Api/pub/autogenerated/ts/types.ts +++ b/src/Api/pub/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/src/Pages/Metrics/AssetsAndLiab.tsx b/src/Pages/Metrics/AssetsAndLiab.tsx index 813daec2..94d0d2b9 100644 --- a/src/Pages/Metrics/AssetsAndLiab.tsx +++ b/src/Pages/Metrics/AssetsAndLiab.tsx @@ -19,17 +19,17 @@ import { } from "ionicons/icons"; import { toast } from "react-toastify"; import { - AssetsAndLiabilities, - AssetsAndLiabilitiesReq, + AssetsAndLiabilitiesV2, + AssetsAndLiabilitiesReqV2, AssetOperation, LiquidityAssetOperationsPage, LndAssetOperationsPage, - LndAssetProvider, + LndAssetProviderV2, LndProviderFilter, - LiquidityAssetProvider, + LiquidityAssetProviderV2, LiquidityProviderFilter, - TrackedLiquidityProvider, - TrackedLndProvider, + TrackedLiquidityProviderV2, + TrackedLndProviderV2, TrackedOperationType, } from "@/Api/pub/autogenerated/ts/types"; import MetricsSubPageToolbar from "@/Layout2/Metrics/MetricsSubPageToolbar"; @@ -39,7 +39,7 @@ const anHour = 3600; const aDay = 86400; /** Test data to preview the full UI; replaced when real fetch completes. */ -/* const MOCK_ASSETS_AND_LIABILITIES: AssetsAndLiabilities = { +/* const MOCK_ASSETS_AND_LIABILITIES: AssetsAndLiabilitiesV2 = { users_balance: 2_847_500, lnds: [ { @@ -146,7 +146,7 @@ type UnifiedAssetOperation = { opKind: AssetOpKind; }; -function collectAllOperations(data: AssetsAndLiabilities): UnifiedAssetOperation[] { +function collectAllOperations(data: AssetsAndLiabilitiesV2): UnifiedAssetOperation[] { const rows: UnifiedAssetOperation[] = []; for (const lnd of data.lnds) { @@ -258,18 +258,18 @@ function mergeLpPage(existing: LiquidityAssetOperationsPage, incoming: Liquidity }; } -function lndProviderHasMore(tracked: TrackedLndProvider): boolean { +function lndProviderHasMore(tracked: TrackedLndProviderV2): boolean { return tracked.invoices.has_more || tracked.payments.has_more || tracked.incoming_tx.has_more || tracked.outgoing_tx.has_more; } -function lpProviderHasMore(tracked: TrackedLiquidityProvider): boolean { +function lpProviderHasMore(tracked: TrackedLiquidityProviderV2): boolean { return tracked.invoices.has_more || tracked.payments.has_more; } -function countProviderOperations(tracked: TrackedLndProvider | TrackedLiquidityProvider): number { +function countProviderOperations(tracked: TrackedLndProviderV2 | TrackedLiquidityProviderV2): number { if ("incoming_tx" in tracked) { return tracked.invoices.operations.length + tracked.payments.operations.length @@ -406,7 +406,7 @@ function LndAssetCard({ loadingMore, onLoadMore, }: { - provider: LndAssetProvider; + provider: LndAssetProviderV2; operationCount: number; hasMore: boolean; loadingMore: boolean; @@ -502,7 +502,7 @@ function LiquidityAssetCard({ loadingMore, onLoadMore, }: { - provider: LiquidityAssetProvider; + provider: LiquidityAssetProviderV2; operationCount: number; hasMore: boolean; loadingMore: boolean; @@ -577,13 +577,13 @@ export const AssetsAndLiab = () => { ); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [assetsAndLiabilities, setAssetsAndLiabilities] = useState({ liquidity_providers: [], lnds: [], users_balance: 0 }); + const [assetsAndLiabilities, setAssetsAndLiabilities] = useState({ liquidity_providers: [], lnds: [], users_balance: 0 }); const [providerHasMore, setProviderHasMore] = useState>({}); const [loadingMorePubkey, setLoadingMorePubkey] = useState(null); const [presentLoading, dismissLoading] = useIonLoading(); const fetchInFlightRef = useRef(false); const autoFetchedForSourceIdRef = useRef(null); - const initProviderHasMore = useCallback((data: AssetsAndLiabilities) => { + const initProviderHasMore = useCallback((data: AssetsAndLiabilitiesV2) => { const next: Record = {}; for (const lnd of data.lnds) { if (lnd.tracked) next[lnd.pubkey] = lndProviderHasMore(lnd.tracked); @@ -595,9 +595,9 @@ export const AssetsAndLiab = () => { }, []); const runFetch = useCallback(async ( - req: AssetsAndLiabilitiesReq, + req: AssetsAndLiabilitiesReqV2, showFullLoading: boolean, - ): Promise => { + ): Promise => { if (!adminSource || fetchInFlightRef.current) return null; fetchInFlightRef.current = true; @@ -625,7 +625,7 @@ export const AssetsAndLiab = () => { toast.error(err); }, }, - [(client) => client.GetAssetsAndLiabilities(req)] + [(client) => client.GetAssetsAndLiabilitiesV2(req)] ); if (!res) return null; const [data] = res; @@ -677,7 +677,7 @@ export const AssetsAndLiab = () => { const incomingTracked = incoming.tracked; const existingTracked = provider.tracked; - const mergedTracked: TrackedLndProvider = { + const mergedTracked: TrackedLndProviderV2 = { confirmed_balance: incomingTracked.confirmed_balance, unconfirmed_balance: incomingTracked.unconfirmed_balance, channels_balance: incomingTracked.channels_balance, @@ -731,7 +731,7 @@ export const AssetsAndLiab = () => { const incomingTracked = incoming.tracked; const existingTracked = provider.tracked; - const mergedTracked: TrackedLiquidityProvider = { + const mergedTracked: TrackedLiquidityProviderV2 = { balance: incomingTracked.balance, invoices: mergeLpPage(existingTracked.invoices, incomingTracked.invoices), payments: mergeLpPage(existingTracked.payments, incomingTracked.payments),