diff --git a/src/Api/pub/autogenerated/ts/nostr_client.ts b/src/Api/pub/autogenerated/ts/nostr_client.ts index 5bc489bf..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') @@ -696,6 +711,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..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') @@ -1162,6 +1178,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..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 | 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 | 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) @@ -219,6 +222,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 } @@ -383,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 @@ -413,6 +423,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 +872,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 @@ -1044,6 +1089,77 @@ export const AssetsAndLiabilitiesReqValidate = (o?: AssetsAndLiabilitiesReq, opt return null } +export type AssetsAndLiabilitiesReqV2 = { + liquidity_providers: LiquidityProviderFilter[] + lnd_providers: LndProviderFilter[] +} +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 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') + + 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 (!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 +} + +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 @@ -2096,10 +2212,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 +2225,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 +2258,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 +2271,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 +2313,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 } @@ -2516,6 +2644,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 @@ -2543,6 +2708,73 @@ 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 + 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 @@ -2625,6 +2857,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 @@ -2652,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[] } @@ -2947,6 +3240,55 @@ export const LndNodeMetricsValidate = (o?: LndNodeMetrics, opts: LndNodeMetricsO return null } +export type LndProviderFilter = { + invoice_index_offset?: number + limit_invoices?: number + limit_payments?: number + limit_transactions?: number + payment_index_offset?: number + pubkey: string + tx_index_offset?: number +} +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 + 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.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 +} + export type LndSeed = { seed: string[] } @@ -4595,6 +4937,36 @@ export const TrackedLiquidityProviderValidate = (o?: TrackedLiquidityProvider, o 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 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') + + 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`) + + 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 + + + return null +} + export type TrackedLndProvider = { channels_balance: number confirmed_balance: number @@ -4663,17 +5035,72 @@ export const TrackedLndProviderValidate = (o?: TrackedLndProvider, opts: Tracked return null } +export type TrackedLndProviderV2 = { + channels_balance: number + confirmed_balance: number + incoming_tx: LndAssetOperationsPage + invoices: LndAssetOperationsPage + outgoing_tx: LndAssetOperationsPage + payments: LndAssetOperationsPage + unconfirmed_balance: number +} +export const TrackedLndProviderV2OptionalFields: [] = [] +export type TrackedLndProviderV2Options = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + channels_balance_CustomCheck?: (v: number) => boolean + confirmed_balance_CustomCheck?: (v: number) => boolean + incoming_tx_Options?: LndAssetOperationsPageOptions + invoices_Options?: LndAssetOperationsPageOptions + outgoing_tx_Options?: LndAssetOperationsPageOptions + payments_Options?: LndAssetOperationsPageOptions + unconfirmed_balance_CustomCheck?: (v: number) => boolean +} +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') + + 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`) + + const incoming_txErr = LndAssetOperationsPageValidate(o.incoming_tx, opts.incoming_tx_Options, `${path}.incoming_tx`) + if (incoming_txErr !== null) return incoming_txErr + + + 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 (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 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 +5115,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 +5459,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 +5725,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..94d0d2b9 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, @@ -19,11 +19,17 @@ import { } from "ionicons/icons"; import { toast } from "react-toastify"; import { - AssetsAndLiabilities, - AssetsAndLiabilitiesReq, + AssetsAndLiabilitiesV2, + AssetsAndLiabilitiesReqV2, AssetOperation, - LndAssetProvider, - LiquidityAssetProvider, + LiquidityAssetOperationsPage, + LndAssetOperationsPage, + LndAssetProviderV2, + LndProviderFilter, + LiquidityAssetProviderV2, + LiquidityProviderFilter, + TrackedLiquidityProviderV2, + TrackedLndProviderV2, TrackedOperationType, } from "@/Api/pub/autogenerated/ts/types"; import MetricsSubPageToolbar from "@/Layout2/Metrics/MetricsSubPageToolbar"; @@ -33,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: [ { @@ -88,28 +94,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: AssetsAndLiabilitiesV2): UnifiedAssetOperation[] { + const rows: UnifiedAssetOperation[] = []; + + for (const lnd of data.lnds) { + if (!lnd.tracked) continue; + const { tracked } = lnd; + 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.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); +} + +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 +207,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 +222,196 @@ 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 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: TrackedLndProviderV2): boolean { + return tracked.invoices.has_more + || tracked.payments.has_more + || tracked.incoming_tx.has_more + || tracked.outgoing_tx.has_more; +} + +function lpProviderHasMore(tracked: TrackedLiquidityProviderV2): boolean { + return tracked.invoices.has_more || tracked.payments.has_more; +} + +function countProviderOperations(tracked: TrackedLndProviderV2 | TrackedLiquidityProviderV2): number { + if ("incoming_tx" in tracked) { + return tracked.invoices.operations.length + + tracked.payments.operations.length + + tracked.incoming_tx.operations.length + + tracked.outgoing_tx.operations.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)"; + +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: LndAssetProviderV2; + operationCount: number; + hasMore: boolean; + loadingMore: boolean; + onLoadMore: () => void; +}) { const tracked = provider.tracked; if (!tracked) { return ( @@ -161,12 +432,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 +442,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: LiquidityAssetProviderV2; + operationCount: number; + hasMore: boolean; + loadingMore: boolean; + onLoadMore: () => void; +}) { const tracked = provider.tracked; if (!tracked) { return ( @@ -301,10 +524,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 +534,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 +577,198 @@ 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 [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: AssetsAndLiabilitiesV2) => { + const next: Record = {}; + for (const lnd of data.lnds) { + if (lnd.tracked) next[lnd.pubkey] = lndProviderHasMore(lnd.tracked); + } + for (const lp of data.liquidity_providers) { + if (lp.tracked) next[lp.pubkey] = lpProviderHasMore(lp.tracked); + } + setProviderHasMore(next); + }, []); - 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 runFetch = useCallback(async ( + req: AssetsAndLiabilitiesReqV2, + 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.GetAssetsAndLiabilitiesV2(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); + initProviderHasMore(data); + }, [initProviderHasMore, runFetch]); - 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 { tracked } = provider; + const filter: LndProviderFilter = { + pubkey, + limit_invoices: OPERATIONS_PAGE_SIZE, + limit_payments: OPERATIONS_PAGE_SIZE, + 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 } + : {}), }; - 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 incomingTracked = incoming.tracked; + const existingTracked = provider.tracked; + const mergedTracked: TrackedLndProviderV2 = { + 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) => ( + lnd.pubkey === pubkey ? { ...lnd, tracked: mergedTracked } : lnd + )), + })); + setProviderHasMore((prev) => ({ + ...prev, + [pubkey]: lndProviderHasMore(mergedTracked), + })); + } 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 { tracked } = provider; + const filter: LiquidityProviderFilter = { + pubkey, + limit: OPERATIONS_PAGE_SIZE, + ...(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); + 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 incomingTracked = incoming.tracked; + const existingTracked = provider.tracked; + const mergedTracked: TrackedLiquidityProviderV2 = { + 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) => ( + lp.pubkey === pubkey ? { ...lp, tracked: mergedTracked } : lp + )), + })); + setProviderHasMore((prev) => ({ + ...prev, + [pubkey]: lpProviderHasMore(mergedTracked), + })); + } finally { + setLoadingMorePubkey(null); + } + }, [assetsAndLiabilities.liquidity_providers, runFetch]); + + const fetchDiscoveryRef = useRef(fetchDiscovery); + fetchDiscoveryRef.current = fetchDiscovery; + + const sourceId = adminSource?.sourceId; + + useEffect(() => { + autoFetchedForSourceIdRef.current = null; + 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 +786,11 @@ export const AssetsAndLiab = () => { return sum; }, [assetsAndLiabilities]); + const allOperations = useMemo( + () => collectAllOperations(assetsAndLiabilities), + [assetsAndLiabilities] + ); + return ( @@ -482,7 +811,7 @@ export const AssetsAndLiab = () => {
Something went wrong
{error}
- void fetchAssetsAndLiabilities()}>Retry + void fetchDiscovery()}>Retry router.push("/metrics/select", "back")}> Change Source @@ -496,7 +825,12 @@ export const AssetsAndLiab = () => {
Summary - void fetchAssetsAndLiabilities()}> + void fetchDiscovery()} + > Refresh
@@ -590,36 +924,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..5e5515be --- /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..032e8fff --- /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..cc302aa0 --- /dev/null +++ b/src/Pages/Metrics/metricsDataTable.tsx @@ -0,0 +1,112 @@ +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 rgba(var(--ion-color-medium-rgb), 0.18)", + 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", + 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 ( + + {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 : TABLE_ROW_BORDER, + 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 + +