diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index a69ff3075..d97294cef 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -118,6 +118,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq) - output: [AssetsAndLiabilities](#AssetsAndLiabilities) +- GetAssetsAndLiabilitiesV2 + - auth type: __Admin__ + - input: [AssetsAndLiabilitiesReqV2](#AssetsAndLiabilitiesReqV2) + - output: [AssetsAndLiabilitiesV2](#AssetsAndLiabilitiesV2) + - GetBundleMetrics - auth type: __Metrics__ - input: [LatestBundleMetricReq](#LatestBundleMetricReq) @@ -253,6 +258,16 @@ The nostr server will send back a message response, and inside the body there wi - input: [GetUserOperationsRequest](#GetUserOperationsRequest) - output: [GetUserOperationsResponse](#GetUserOperationsResponse) +- GetUserOperationsFromAdmin + - auth type: __Admin__ + - input: [GetUserOperationsRequest](#GetUserOperationsRequest) + - output: [GetUserOperationsResponse](#GetUserOperationsResponse) + +- GetUsersAdminInfo + - auth type: __Admin__ + - input: [UsersAdminInfoRequest](#UsersAdminInfoRequest) + - output: [UsersAdminInfo](#UsersAdminInfo) + - LinkNPubThroughToken - auth type: __GuestWithPub__ - input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest) @@ -626,6 +641,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq) - output: [AssetsAndLiabilities](#AssetsAndLiabilities) +- GetAssetsAndLiabilitiesV2 + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/assets/liabilities/v2__ + - input: [AssetsAndLiabilitiesReqV2](#AssetsAndLiabilitiesReqV2) + - output: [AssetsAndLiabilitiesV2](#AssetsAndLiabilitiesV2) + - GetBundleMetrics - auth type: __Metrics__ - http method: __post__ @@ -840,6 +862,20 @@ The nostr server will send back a message response, and inside the body there wi - input: [GetUserOperationsRequest](#GetUserOperationsRequest) - output: [GetUserOperationsResponse](#GetUserOperationsResponse) +- GetUserOperationsFromAdmin + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/user/operations__ + - input: [GetUserOperationsRequest](#GetUserOperationsRequest) + - output: [GetUserOperationsResponse](#GetUserOperationsResponse) + +- GetUsersAdminInfo + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/users/info__ + - input: [UsersAdminInfoRequest](#UsersAdminInfoRequest) + - output: [UsersAdminInfo](#UsersAdminInfo) + - HandleLnurlAddress - auth type: __Guest__ - http method: __get__ @@ -1196,6 +1232,12 @@ The nostr server will send back a message response, and inside the body there wi - __info__: _[UserInfo](#UserInfo)_ - __max_withdrawable__: _number_ +### AppUserAdminInfo + - __app_user_id__: _string_ + - __has_callback_url__: _boolean_ + - __has_topic_id__: _boolean_ + - __npub__: _string_ + ### Application - __balance__: _number_ - __id__: _string_ @@ -1225,6 +1267,15 @@ The nostr server will send back a message response, and inside the body there wi - __limit_payments__: _number_ *this field is optional - __limit_providers__: _number_ *this field is optional +### AssetsAndLiabilitiesReqV2 + - __liquidity_providers__: ARRAY of: _[LiquidityProviderFilter](#LiquidityProviderFilter)_ + - __lnd_providers__: ARRAY of: _[LndProviderFilter](#LndProviderFilter)_ + +### AssetsAndLiabilitiesV2 + - __liquidity_providers__: ARRAY of: _[LiquidityAssetProviderV2](#LiquidityAssetProviderV2)_ + - __lnds__: ARRAY of: _[LndAssetProviderV2](#LndAssetProviderV2)_ + - __users_balance__: _number_ + ### AuthApp - __app__: _[Application](#Application)_ - __auth_token__: _string_ @@ -1405,6 +1456,7 @@ The nostr server will send back a message response, and inside the body there wi - __latestOutgoingTx__: _[OperationsCursor](#OperationsCursor)_ - __latestOutgoingUserToUserPayment__: _[OperationsCursor](#OperationsCursor)_ - __max_size__: _number_ + - __user_id__: _string_ *this field is optional ### GetUserOperationsResponse - __latestIncomingInvoiceOperations__: _[UserOperations](#UserOperations)_ @@ -1413,6 +1465,7 @@ The nostr server will send back a message response, and inside the body there wi - __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_ - __latestOutgoingTxOperations__: _[UserOperations](#UserOperations)_ - __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_ + - __user_id__: _string_ *this field is optional ### GraphPoint - __x__: _number_ @@ -1468,10 +1521,25 @@ The nostr server will send back a message response, and inside the body there wi ### LinkNPubThroughTokenRequest - __token__: _string_ +### LiquidityAssetOperationsPage + - __has_more__: _boolean_ + - __next_cursor__: _[OperationsCursor](#OperationsCursor)_ *this field is optional + - __operations__: ARRAY of: _[AssetOperation](#AssetOperation)_ + ### LiquidityAssetProvider - __pubkey__: _string_ - __tracked__: _[TrackedLiquidityProvider](#TrackedLiquidityProvider)_ *this field is optional +### LiquidityAssetProviderV2 + - __pubkey__: _string_ + - __tracked__: _[TrackedLiquidityProviderV2](#TrackedLiquidityProviderV2)_ *this field is optional + +### LiquidityProviderFilter + - __latestIncomingInvoice__: _[OperationsCursor](#OperationsCursor)_ *this field is optional + - __latestOutgoingInvoice__: _[OperationsCursor](#OperationsCursor)_ *this field is optional + - __limit__: _number_ *this field is optional + - __pubkey__: _string_ + ### LiveDebitRequest - __debit__: _[LiveDebitRequest_debit](#LiveDebitRequest_debit)_ - __k1__: _string_ *this field is optional @@ -1486,10 +1554,19 @@ The nostr server will send back a message response, and inside the body there wi - __latest_balance__: _number_ - __operation__: _[UserOperation](#UserOperation)_ +### LndAssetOperationsPage + - __has_more__: _boolean_ + - __next_index_offset__: _number_ *this field is optional + - __operations__: ARRAY of: _[AssetOperation](#AssetOperation)_ + ### LndAssetProvider - __pubkey__: _string_ - __tracked__: _[TrackedLndProvider](#TrackedLndProvider)_ *this field is optional +### LndAssetProviderV2 + - __pubkey__: _string_ + - __tracked__: _[TrackedLndProviderV2](#TrackedLndProviderV2)_ *this field is optional + ### LndChannels - __open_channels__: ARRAY of: _[OpenChannel](#OpenChannel)_ @@ -1535,6 +1612,15 @@ The nostr server will send back a message response, and inside the body there wi - __pending_channels__: _number_ - __root_ops__: ARRAY of: _[RootOperation](#RootOperation)_ +### LndProviderFilter + - __invoice_index_offset__: _number_ *this field is optional + - __limit_invoices__: _number_ *this field is optional + - __limit_payments__: _number_ *this field is optional + - __limit_transactions__: _number_ *this field is optional + - __payment_index_offset__: _number_ *this field is optional + - __pubkey__: _string_ + - __tx_index_offset__: _number_ *this field is optional + ### LndSeed - __seed__: ARRAY of: _string_ @@ -1818,6 +1904,11 @@ The nostr server will send back a message response, and inside the body there wi - __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_ - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_ +### TrackedLiquidityProviderV2 + - __balance__: _number_ + - __invoices__: _[LiquidityAssetOperationsPage](#LiquidityAssetOperationsPage)_ + - __payments__: _[LiquidityAssetOperationsPage](#LiquidityAssetOperationsPage)_ + ### TrackedLndProvider - __channels_balance__: _number_ - __confirmed_balance__: _number_ @@ -1827,10 +1918,20 @@ The nostr server will send back a message response, and inside the body there wi - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_ - __unconfirmed_balance__: _number_ +### TrackedLndProviderV2 + - __channels_balance__: _number_ + - __confirmed_balance__: _number_ + - __incoming_tx__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ + - __invoices__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ + - __outgoing_tx__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ + - __payments__: _[LndAssetOperationsPage](#LndAssetOperationsPage)_ + - __unconfirmed_balance__: _number_ + ### TrackedOperation - __amount__: _number_ - __ts__: _number_ - __type__: _[TrackedOperationType](#TrackedOperationType)_ + - __user_id__: _string_ *this field is optional ### TransactionSwapQuote - __chain_fee_sats__: _number_ @@ -1888,6 +1989,13 @@ The nostr server will send back a message response, and inside the body there wi ### UseInviteLinkRequest - __invite_token__: _string_ +### UserAdminInfo + - __app_users__: ARRAY of: _[AppUserAdminInfo](#AppUserAdminInfo)_ + - __balance__: _number_ + - __locked__: _boolean_ + - __owner_of_app_id__: _string_ *this field is optional + - __user_id__: _string_ + ### UserHealthState - __downtime_reason__: _string_ @@ -1927,6 +2035,14 @@ The nostr server will send back a message response, and inside the body there wi - __operations__: ARRAY of: _[UserOperation](#UserOperation)_ - __toIndex__: _[OperationsCursor](#OperationsCursor)_ +### UsersAdminInfo + - __total__: _number_ + - __users__: ARRAY of: _[UserAdminInfo](#UserAdminInfo)_ + +### UsersAdminInfoRequest + - __skip__: _number_ *this field is optional + - __take__: _number_ *this field is optional + ### UsersInfo - __always_been_inactive__: _number_ - __balance_avg__: _number_ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index ca3ccfe92..88d833048 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -82,6 +82,7 @@ type Client struct { GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) GetAssetsAndLiabilities func(req AssetsAndLiabilitiesReq) (*AssetsAndLiabilities, error) + GetAssetsAndLiabilitiesV2 func(req AssetsAndLiabilitiesReqV2) (*AssetsAndLiabilitiesV2, error) GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) GetDebitAuthorizations func() (*DebitAuthorizations, error) GetErrorStats func() (*ErrorStats, error) @@ -112,6 +113,8 @@ type Client struct { GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error) GetUserOffers func() (*UserOffers, error) GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) + GetUserOperationsFromAdmin func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) + GetUsersAdminInfo func(req UsersAdminInfoRequest) (*UsersAdminInfo, error) HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error @@ -897,6 +900,35 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + GetAssetsAndLiabilitiesV2: func(req AssetsAndLiabilitiesReqV2) (*AssetsAndLiabilitiesV2, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/assets/liabilities/v2" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := AssetsAndLiabilitiesV2{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, GetBundleMetrics: func(req LatestBundleMetricReq) (*BundleMetrics, error) { auth, err := params.RetrieveMetricsAuth() if err != nil { @@ -1600,6 +1632,64 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + GetUserOperationsFromAdmin: func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/user/operations" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := GetUserOperationsResponse{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, + GetUsersAdminInfo: func(req UsersAdminInfoRequest) (*UsersAdminInfo, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/users/info" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := UsersAdminInfo{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, HandleLnurlAddress: func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) { auth, err := params.RetrieveGuestAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 76a6898ea..b9b872419 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -156,6 +156,12 @@ type AppUser struct { Info *UserInfo `json:"info"` Max_withdrawable int64 `json:"max_withdrawable"` } +type AppUserAdminInfo struct { + App_user_id string `json:"app_user_id"` + Has_callback_url bool `json:"has_callback_url"` + Has_topic_id bool `json:"has_topic_id"` + Npub string `json:"npub"` +} type Application struct { Balance int64 `json:"balance"` Id string `json:"id"` @@ -185,6 +191,15 @@ type AssetsAndLiabilitiesReq struct { Limit_payments int64 `json:"limit_payments"` Limit_providers int64 `json:"limit_providers"` } +type AssetsAndLiabilitiesReqV2 struct { + Liquidity_providers []LiquidityProviderFilter `json:"liquidity_providers"` + Lnd_providers []LndProviderFilter `json:"lnd_providers"` +} +type AssetsAndLiabilitiesV2 struct { + Liquidity_providers []LiquidityAssetProviderV2 `json:"liquidity_providers"` + Lnds []LndAssetProviderV2 `json:"lnds"` + Users_balance int64 `json:"users_balance"` +} type AuthApp struct { App *Application `json:"app"` Auth_token string `json:"auth_token"` @@ -365,6 +380,7 @@ type GetUserOperationsRequest struct { Latestoutgoingtx *OperationsCursor `json:"latestOutgoingTx"` Latestoutgoingusertouserpayment *OperationsCursor `json:"latestOutgoingUserToUserPayment"` Max_size int64 `json:"max_size"` + User_id string `json:"user_id"` } type GetUserOperationsResponse struct { Latestincominginvoiceoperations *UserOperations `json:"latestIncomingInvoiceOperations"` @@ -373,6 +389,7 @@ type GetUserOperationsResponse struct { Latestoutgoinginvoiceoperations *UserOperations `json:"latestOutgoingInvoiceOperations"` Latestoutgoingtxoperations *UserOperations `json:"latestOutgoingTxOperations"` Latestoutgoingusertouserpayemnts *UserOperations `json:"latestOutgoingUserToUserPayemnts"` + User_id string `json:"user_id"` } type GraphPoint struct { X int64 `json:"x"` @@ -428,10 +445,25 @@ type LatestUsageMetricReq struct { type LinkNPubThroughTokenRequest struct { Token string `json:"token"` } +type LiquidityAssetOperationsPage struct { + Has_more bool `json:"has_more"` + Next_cursor *OperationsCursor `json:"next_cursor"` + Operations []AssetOperation `json:"operations"` +} type LiquidityAssetProvider struct { Pubkey string `json:"pubkey"` Tracked *TrackedLiquidityProvider `json:"tracked"` } +type LiquidityAssetProviderV2 struct { + Pubkey string `json:"pubkey"` + Tracked *TrackedLiquidityProviderV2 `json:"tracked"` +} +type LiquidityProviderFilter struct { + Latestincominginvoice *OperationsCursor `json:"latestIncomingInvoice"` + Latestoutgoinginvoice *OperationsCursor `json:"latestOutgoingInvoice"` + Limit int64 `json:"limit"` + Pubkey string `json:"pubkey"` +} type LiveDebitRequest struct { Debit *LiveDebitRequest_debit `json:"debit"` K1 string `json:"k1"` @@ -446,10 +478,19 @@ type LiveUserOperation struct { Latest_balance int64 `json:"latest_balance"` Operation *UserOperation `json:"operation"` } +type LndAssetOperationsPage struct { + Has_more bool `json:"has_more"` + Next_index_offset int64 `json:"next_index_offset"` + Operations []AssetOperation `json:"operations"` +} type LndAssetProvider struct { Pubkey string `json:"pubkey"` Tracked *TrackedLndProvider `json:"tracked"` } +type LndAssetProviderV2 struct { + Pubkey string `json:"pubkey"` + Tracked *TrackedLndProviderV2 `json:"tracked"` +} type LndChannels struct { Open_channels []OpenChannel `json:"open_channels"` } @@ -495,6 +536,15 @@ type LndNodeMetrics struct { Pending_channels int64 `json:"pending_channels"` Root_ops []RootOperation `json:"root_ops"` } +type LndProviderFilter struct { + Invoice_index_offset int64 `json:"invoice_index_offset"` + Limit_invoices int64 `json:"limit_invoices"` + Limit_payments int64 `json:"limit_payments"` + Limit_transactions int64 `json:"limit_transactions"` + Payment_index_offset int64 `json:"payment_index_offset"` + Pubkey string `json:"pubkey"` + Tx_index_offset int64 `json:"tx_index_offset"` +} type LndSeed struct { Seed []string `json:"seed"` } @@ -778,6 +828,11 @@ type TrackedLiquidityProvider struct { Invoices []AssetOperation `json:"invoices"` Payments []AssetOperation `json:"payments"` } +type TrackedLiquidityProviderV2 struct { + Balance int64 `json:"balance"` + Invoices *LiquidityAssetOperationsPage `json:"invoices"` + Payments *LiquidityAssetOperationsPage `json:"payments"` +} type TrackedLndProvider struct { Channels_balance int64 `json:"channels_balance"` Confirmed_balance int64 `json:"confirmed_balance"` @@ -787,10 +842,20 @@ type TrackedLndProvider struct { Payments []AssetOperation `json:"payments"` Unconfirmed_balance int64 `json:"unconfirmed_balance"` } +type TrackedLndProviderV2 struct { + Channels_balance int64 `json:"channels_balance"` + Confirmed_balance int64 `json:"confirmed_balance"` + Incoming_tx *LndAssetOperationsPage `json:"incoming_tx"` + Invoices *LndAssetOperationsPage `json:"invoices"` + Outgoing_tx *LndAssetOperationsPage `json:"outgoing_tx"` + Payments *LndAssetOperationsPage `json:"payments"` + Unconfirmed_balance int64 `json:"unconfirmed_balance"` +} type TrackedOperation struct { - Amount int64 `json:"amount"` - Ts int64 `json:"ts"` - Type TrackedOperationType `json:"type"` + Amount int64 `json:"amount"` + Ts int64 `json:"ts"` + Type TrackedOperationType `json:"type"` + User_id string `json:"user_id"` } type TransactionSwapQuote struct { Chain_fee_sats int64 `json:"chain_fee_sats"` @@ -848,6 +913,13 @@ type UsageMetrics struct { type UseInviteLinkRequest struct { Invite_token string `json:"invite_token"` } +type UserAdminInfo struct { + App_users []AppUserAdminInfo `json:"app_users"` + Balance int64 `json:"balance"` + Locked bool `json:"locked"` + Owner_of_app_id string `json:"owner_of_app_id"` + User_id string `json:"user_id"` +} type UserHealthState struct { Downtime_reason string `json:"downtime_reason"` } @@ -887,6 +959,14 @@ type UserOperations struct { Operations []UserOperation `json:"operations"` Toindex *OperationsCursor `json:"toIndex"` } +type UsersAdminInfo struct { + Total int64 `json:"total"` + Users []UserAdminInfo `json:"users"` +} +type UsersAdminInfoRequest struct { + Skip int64 `json:"skip"` + Take int64 `json:"take"` +} type UsersInfo struct { Always_been_inactive int64 `json:"always_been_inactive"` Balance_avg int64 `json:"balance_avg"` diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index 6f04506ad..2e8315eb3 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -1042,6 +1042,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.GetAssetsAndLiabilitiesV2) throw new Error('method: GetAssetsAndLiabilitiesV2 is not implemented') + app.post('/api/admin/assets/liabilities/v2', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetAssetsAndLiabilitiesV2', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetAssetsAndLiabilitiesV2) throw new Error('method: GetAssetsAndLiabilitiesV2 is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.AssetsAndLiabilitiesReqV2Validate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.GetAssetsAndLiabilitiesV2({rpcName:'GetAssetsAndLiabilitiesV2', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented') app.post('/api/reports/bundle', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'GetBundleMetrics', batch: false, nostr: false, batchSize: 0} @@ -1575,6 +1597,50 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.GetUserOperationsFromAdmin) throw new Error('method: GetUserOperationsFromAdmin is not implemented') + app.post('/api/admin/user/operations', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetUserOperationsFromAdmin', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetUserOperationsFromAdmin) throw new Error('method: GetUserOperationsFromAdmin is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.GetUserOperationsRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.GetUserOperationsFromAdmin({rpcName:'GetUserOperationsFromAdmin', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) + if (!opts.allowNotImplementedMethods && !methods.GetUsersAdminInfo) throw new Error('method: GetUsersAdminInfo is not implemented') + app.post('/api/admin/users/info', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetUsersAdminInfo', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetUsersAdminInfo) throw new Error('method: GetUsersAdminInfo is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.UsersAdminInfoRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.GetUsersAdminInfo({rpcName:'GetUsersAdminInfo', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.HandleLnurlAddress) throw new Error('method: HandleLnurlAddress is not implemented') app.get('/.well-known/lnurlp/:address_name', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'HandleLnurlAddress', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 1e96081d9..bf0453c83 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -382,6 +382,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAssetsAndLiabilitiesV2: async (request: Types.AssetsAndLiabilitiesReqV2): Promise => { + const auth = await params.retrieveAdminAuth() + if (auth === null) throw new Error('retrieveAdminAuth() returned null') + let finalRoute = '/api/admin/assets/liabilities/v2' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.AssetsAndLiabilitiesV2Validate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetBundleMetrics: async (request: Types.LatestBundleMetricReq): Promise => { const auth = await params.retrieveMetricsAuth() if (auth === null) throw new Error('retrieveMetricsAuth() returned null') @@ -754,6 +768,34 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetUserOperationsFromAdmin: async (request: Types.GetUserOperationsRequest): Promise => { + const auth = await params.retrieveAdminAuth() + if (auth === null) throw new Error('retrieveAdminAuth() returned null') + let finalRoute = '/api/admin/user/operations' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.GetUserOperationsResponseValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, + GetUsersAdminInfo: async (request: Types.UsersAdminInfoRequest): Promise => { + const auth = await params.retrieveAdminAuth() + if (auth === null) throw new Error('retrieveAdminAuth() returned null') + let finalRoute = '/api/admin/users/info' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.UsersAdminInfoValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, HandleLnurlAddress: async (routeParams: Types.HandleLnurlAddress_RouteParams): Promise => { const auth = await params.retrieveGuestAuth() if (auth === null) throw new Error('retrieveGuestAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 8a07f5bf3..a14681b4c 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -302,6 +302,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAssetsAndLiabilitiesV2: async (request: Types.AssetsAndLiabilitiesReqV2): Promise => { + const auth = await params.retrieveNostrAdminAuth() + if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + const data = await send(params.pubDestination, {rpcName:'GetAssetsAndLiabilitiesV2',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.AssetsAndLiabilitiesV2Validate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetBundleMetrics: async (request: Types.LatestBundleMetricReq): Promise => { const auth = await params.retrieveNostrMetricsAuth() if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') @@ -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/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index c92fc4239..1d1fd20e7 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -767,6 +767,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'GetAssetsAndLiabilitiesV2': + try { + if (!methods.GetAssetsAndLiabilitiesV2) throw new Error('method: GetAssetsAndLiabilitiesV2 is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.AssetsAndLiabilitiesReqV2Validate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.GetAssetsAndLiabilitiesV2({rpcName:'GetAssetsAndLiabilitiesV2', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'GetBundleMetrics': try { if (!methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented') @@ -1154,6 +1170,38 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'GetUserOperationsFromAdmin': + try { + if (!methods.GetUserOperationsFromAdmin) throw new Error('method: GetUserOperationsFromAdmin is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.GetUserOperationsRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.GetUserOperationsFromAdmin({rpcName:'GetUserOperationsFromAdmin', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break + case 'GetUsersAdminInfo': + try { + if (!methods.GetUsersAdminInfo) throw new Error('method: GetUsersAdminInfo is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.UsersAdminInfoRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.GetUsersAdminInfo({rpcName:'GetUsersAdminInfo', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'LinkNPubThroughToken': try { if (!methods.LinkNPubThroughToken) throw new Error('method: LinkNPubThroughToken is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index bf813a088..191642c6b 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: export type AdminContext = { admin_id: string } -export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | BumpTx_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminInvoiceSwapQuotes_Input | GetAdminTransactionSwapQuotes_Input | GetAssetsAndLiabilities_Input | GetInviteLinkState_Input | GetSeed_Input | 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/proto/service/methods.proto b/proto/service/methods.proto index cfdd7b347..121689794 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -119,6 +119,13 @@ service LightningPub { option (nostr) = true; }; + rpc GetAssetsAndLiabilitiesV2(structs.AssetsAndLiabilitiesReqV2) returns (structs.AssetsAndLiabilitiesV2) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/assets/liabilities/v2"; + option (nostr) = true; + }; + rpc BumpTx(structs.BumpTx) returns (structs.Empty) { option (auth_type) = "Admin"; option (http_method) = "post"; @@ -238,6 +245,37 @@ service LightningPub { option (nostr) = true; } + rpc CreateOneTimeInviteLink(structs.CreateOneTimeInviteLinkRequest) returns (structs.CreateOneTimeInviteLinkResponse) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/app/invite/create"; + option (nostr) = true; + } + + rpc GetInviteLinkState(structs.GetInviteTokenStateRequest) returns (structs.GetInviteTokenStateResponse) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/app/invite/get"; + option (nostr) = true; + } + + rpc GetUsersAdminInfo(structs.UsersAdminInfoRequest) returns (structs.UsersAdminInfo) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/users/info"; + option (nostr) = true; + } + + rpc GetUserOperationsFromAdmin(structs.GetUserOperationsRequest) returns (structs.GetUserOperationsResponse) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/user/operations"; + option (nostr) = true; + } + + // + // + rpc GetUsageMetrics(structs.LatestUsageMetricReq) returns (structs.UsageMetrics) { option (auth_type) = "Metrics"; option (http_method) = "post"; @@ -333,22 +371,11 @@ service LightningPub { option (nostr) = true; } - rpc CreateOneTimeInviteLink(structs.CreateOneTimeInviteLinkRequest) returns (structs.CreateOneTimeInviteLinkResponse) { - option (auth_type) = "Admin"; - option (http_method) = "post"; - option (http_route) = "/api/admin/app/invite/create"; - option (nostr) = true; - } - rpc GetInviteLinkState(structs.GetInviteTokenStateRequest) returns (structs.GetInviteTokenStateResponse) { - option (auth_type) = "Admin"; - option (http_method) = "post"; - option (http_route) = "/api/admin/app/invite/get"; - option (nostr) = true; - } - // + // + // rpc Health(structs.Empty) returns (structs.Empty){ diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 33a4ebf1f..e4470fed4 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -19,18 +19,65 @@ message EncryptionExchangeRequest { string deviceId = 2; } +message LndProviderFilter { + string pubkey = 1; + optional int64 limit_invoices = 2; + optional int64 invoice_index_offset = 3; + optional int64 limit_payments = 4; + optional int64 payment_index_offset = 5; + optional int64 limit_transactions = 6; + optional int64 tx_index_offset = 7; +} + +message LiquidityProviderFilter { + string pubkey = 1; + optional OperationsCursor latestIncomingInvoice = 2; + optional OperationsCursor latestOutgoingInvoice = 3; + optional int64 limit = 4; +} + message AssetsAndLiabilitiesReq { optional int64 limit_invoices = 1; optional int64 limit_payments = 2; optional int64 limit_providers = 4; } +message AssetsAndLiabilitiesReqV2 { + repeated LndProviderFilter lnd_providers = 1; + repeated LiquidityProviderFilter liquidity_providers = 2; +} + message BumpTx { string txid = 1; int64 output_index = 2; int64 sat_per_vbyte = 3; } +message AppUserAdminInfo { + string app_user_id = 1; + string npub = 2; + bool has_callback_url = 3; + bool has_topic_id = 4; +} + +message UserAdminInfo { + string user_id = 1; + int64 balance = 2; + bool locked = 3; + repeated AppUserAdminInfo app_users = 4; + optional string owner_of_app_id = 5; +} + +message UsersAdminInfoRequest { + optional int64 skip = 1; + optional int64 take = 2; +} + +message UsersAdminInfo { + repeated UserAdminInfo users = 1; + int64 total = 2; +} + enum TrackedOperationType { USER = 0; @@ -41,6 +88,7 @@ message TrackedOperation { int64 ts = 1; int64 amount = 2; TrackedOperationType type = 3; + optional string user_id = 4; } message AssetOperation { @@ -49,6 +97,18 @@ message AssetOperation { optional TrackedOperation tracked = 3; } +message LndAssetOperationsPage { + repeated AssetOperation operations = 1; + bool has_more = 2; + optional int64 next_index_offset = 3; +} + +message LiquidityAssetOperationsPage { + repeated AssetOperation operations = 1; + bool has_more = 2; + optional OperationsCursor next_cursor = 3; +} + message TrackedLndProvider { int64 confirmed_balance = 1; int64 unconfirmed_balance = 2; @@ -65,6 +125,22 @@ message TrackedLiquidityProvider { repeated AssetOperation invoices = 3; } +message TrackedLndProviderV2 { + int64 confirmed_balance = 1; + int64 unconfirmed_balance = 2; + int64 channels_balance = 3; + LndAssetOperationsPage payments = 4; + LndAssetOperationsPage invoices = 5; + LndAssetOperationsPage incoming_tx = 6; + LndAssetOperationsPage outgoing_tx = 7; +} + +message TrackedLiquidityProviderV2 { + int64 balance = 1; + LiquidityAssetOperationsPage payments = 2; + LiquidityAssetOperationsPage invoices = 3; +} + message LndAssetProvider { string pubkey = 1; optional TrackedLndProvider tracked = 2; @@ -75,12 +151,28 @@ message LiquidityAssetProvider { optional TrackedLiquidityProvider tracked = 2; } +message LndAssetProviderV2 { + string pubkey = 1; + optional TrackedLndProviderV2 tracked = 2; +} + +message LiquidityAssetProviderV2 { + string pubkey = 1; + optional TrackedLiquidityProviderV2 tracked = 2; +} + message AssetsAndLiabilities { int64 users_balance = 1; repeated LndAssetProvider lnds = 2; repeated LiquidityAssetProvider liquidity_providers = 3; } +message AssetsAndLiabilitiesV2 { + int64 users_balance = 1; + repeated LndAssetProviderV2 lnds = 2; + repeated LiquidityAssetProviderV2 liquidity_providers = 3; +} + message UserHealthState { string downtime_reason = 1; } @@ -621,6 +713,7 @@ message GetUserOperationsRequest{ OperationsCursor latestIncomingUserToUserPayment = 5; OperationsCursor latestOutgoingUserToUserPayment = 6; int64 max_size = 7; + optional string user_id = 8; } enum UserOperationType { INCOMING_TX =0; @@ -656,6 +749,7 @@ message GetUserOperationsResponse{ UserOperations latestIncomingTxOperations=4; UserOperations latestOutgoingUserToUserPayemnts=5; UserOperations latestIncomingUserToUserPayemnts=6; + optional string user_id = 7; } message AddProductRequest { diff --git a/src/services/helpers/pageLimit.ts b/src/services/helpers/pageLimit.ts new file mode 100644 index 000000000..c0a86b121 --- /dev/null +++ b/src/services/helpers/pageLimit.ts @@ -0,0 +1,8 @@ +export const DEFAULT_PAGE_SIZE = 20 +export const DEFAULT_LND_PAGE_SIZE = 20 +export const MAX_PAGE_SIZE = 50 +export const MAX_LIQUIDITY_PAGE_SIZE = 40 + +export const clampPageLimit = (value: number | undefined, defaultLimit: number, maxLimit: number): number => + Math.min(Math.max(value ?? defaultLimit, 1), maxLimit) + diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 4ac9245bc..8685518c2 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -9,7 +9,7 @@ import { LightningClient } from '../../../proto/lnd/lightning.client.js' import { InvoicesClient } from '../../../proto/lnd/invoices.client.js' import { RouterClient } from '../../../proto/lnd/router.client.js' import { ChainNotifierClient } from '../../../proto/lnd/chainnotifier.client.js' -import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse, CoinSelectionStrategy, OpenStatusUpdate, CloseStatusUpdate, PendingUpdate, ChannelEventUpdate_UpdateType } from '../../../proto/lnd/lightning.js' +import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse, CoinSelectionStrategy, OpenStatusUpdate, CloseStatusUpdate, PendingUpdate, ChannelEventUpdate_UpdateType, ListPaymentsResponse, ListInvoiceResponse } from '../../../proto/lnd/lightning.js' import { OpenChannelReq } from './openChannelReq.js'; import { AddInvoiceReq } from './addInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js'; @@ -607,9 +607,9 @@ export default class { } } - async GetTransactions(startHeight: number): Promise { + async GetTransactions(startHeight: number, indexOffset = 0, maxTransactions = 0): Promise { this.log(DEBUG, "Getting transactions") - const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "", indexOffset: 0, maxTransactions: 0 }, DeadLineMetadata()) + const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "", indexOffset, maxTransactions }, DeadLineMetadata()) return res.response } @@ -693,20 +693,20 @@ export default class { return response } - async GetAllInvoices(max: number) { + async GetAllInvoices(max: number, startOffset = 0): Promise { this.log(DEBUG, "Getting all paid invoices") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { - return { invoices: [] } + return { invoices: [], firstIndexOffset: 0n, lastIndexOffset: 0n } } - const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) + const res = await this.lightning.listInvoices({ indexOffset: BigInt(startOffset), numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response } - async GetAllPayments(max: number) { + async GetAllPayments(max: number, startOffset = 0): Promise { this.log(DEBUG, "Getting all payments") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { - return { payments: [] } + return { payments: [], firstIndexOffset: 0n, lastIndexOffset: 0n, totalNumPayments: 0n } } - const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }) + const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: BigInt(startOffset), maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response } diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index dbde290dd..0e3ecf840 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -7,10 +7,16 @@ import LND from "../lnd/lnd.js"; import SettingsManager from "./settingsManager.js"; import { Swaps } from "../lnd/swaps/swaps.js"; import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"; +import { AddressReceivingTransaction } from "../storage/entity/AddressReceivingTransaction.js"; +import { RootOperation } from "../storage/entity/RootOperation.js"; +import { UserInvoicePayment } from "../storage/entity/UserInvoicePayment.js"; +import { UserReceivingInvoice } from "../storage/entity/UserReceivingInvoice.js"; +import { UserTransactionPayment } from "../storage/entity/UserTransactionPayment.js"; import { TrackedProvider } from "../storage/entity/TrackedProvider.js"; import { NodeInfo } from "../lnd/settings.js"; import { Invoice, Payment, OutputDetail, Transaction, Payment_PaymentStatus, Invoice_InvoiceState } from "../../../proto/lnd/lightning.js"; import { LiquidityProvider } from "./liquidityProvider.js"; +import { clampPageLimit, DEFAULT_LND_PAGE_SIZE, DEFAULT_PAGE_SIZE, MAX_LIQUIDITY_PAGE_SIZE, MAX_PAGE_SIZE } from "../helpers/pageLimit.js"; /* type TrackedOperation = { ts: number amount: number @@ -378,10 +384,15 @@ export class AdminManager { } async GetAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq): Promise { - const providers = await this.storage.liquidityStorage.GetTrackedProviders() + const v2Req = await this.BuildV2ReqFromV1(req) + const v2Res = await this.GetAssetsAndLiabilitiesV2(v2Req) + return this.V2ToV1Response(v2Res) + } - const lnds: Types.LndAssetProvider[] = [] - const liquidityProviders: Types.LiquidityAssetProvider[] = [] + async GetAssetsAndLiabilitiesV2(req: Types.AssetsAndLiabilitiesReqV2): Promise { + const providers = await this.storage.liquidityStorage.GetTrackedProviders() + const lnds: Types.LndAssetProviderV2[] = [] + const liquidityProviders: Types.LiquidityAssetProviderV2[] = [] for (const provider of providers) { if (provider.provider_type === 'lnd') { const lndEntry = await this.GetLndAssetsAndLiabilities(req, provider) @@ -399,25 +410,77 @@ export class AdminManager { } } - async GetProviderAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq, provider: TrackedProvider): Promise { + private async BuildV2ReqFromV1(req: Types.AssetsAndLiabilitiesReq): Promise { + const providers = await this.storage.liquidityStorage.GetTrackedProviders() + const limitPayments = req.limit_payments ?? DEFAULT_LND_PAGE_SIZE + const limitInvoices = req.limit_invoices ?? 100 + const limitProviders = req.limit_providers ?? 100 + return { + lnd_providers: providers + .filter(p => p.provider_type === 'lnd') + .map(p => ({ + pubkey: p.provider_pubkey, + limit_payments: limitPayments, + limit_invoices: limitInvoices, + limit_transactions: MAX_PAGE_SIZE, + })), + liquidity_providers: providers + .filter(p => p.provider_type === 'lnPub') + .map(p => ({ + pubkey: p.provider_pubkey, + limit: limitProviders, + })), + } + } + + private V2ToV1Response(res: Types.AssetsAndLiabilitiesV2): Types.AssetsAndLiabilities { + return { + users_balance: res.users_balance, + lnds: res.lnds.map(lnd => ({ + pubkey: lnd.pubkey, + tracked: lnd.tracked ? { + confirmed_balance: lnd.tracked.confirmed_balance, + unconfirmed_balance: lnd.tracked.unconfirmed_balance, + channels_balance: lnd.tracked.channels_balance, + payments: lnd.tracked.payments.operations, + invoices: lnd.tracked.invoices.operations, + incoming_tx: lnd.tracked.incoming_tx.operations, + outgoing_tx: lnd.tracked.outgoing_tx.operations, + } : undefined, + })), + liquidity_providers: res.liquidity_providers.map(provider => ({ + pubkey: provider.pubkey, + tracked: provider.tracked ? { + balance: provider.tracked.balance, + payments: provider.tracked.payments.operations, + invoices: provider.tracked.invoices.operations, + } : undefined, + })), + } + } + + async GetProviderAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReqV2, provider: TrackedProvider): Promise { if (!this.liquidityProvider) { throw new Error("liquidity provider not attached") } if (this.liquidityProvider.GetProviderPubkey() !== provider.provider_pubkey) { return { pubkey: provider.provider_pubkey, tracked: undefined } } - const providerOps = await this.liquidityProvider.GetOperations(req.limit_providers || 100) - // we only care about invoices cuz they are the only ops we can generate with a provider - const invoices: Types.AssetOperation[] = [] - const payments: Types.AssetOperation[] = [] - for (const op of providerOps.latestIncomingInvoiceOperations.operations) { - const assetOp = await this.GetProviderInvoiceAssetOperation(op) - invoices.push(assetOp) - } - for (const op of providerOps.latestOutgoingInvoiceOperations.operations) { - const assetOp = await this.GetProviderPaymentAssetOperation(op) - payments.push(assetOp) - } + const filter = req.liquidity_providers.find(p => p.pubkey === provider.provider_pubkey) + const incoming = filter?.latestIncomingInvoice + const outgoing = filter?.latestOutgoingInvoice + const limit = clampPageLimit(filter?.limit, DEFAULT_PAGE_SIZE, MAX_LIQUIDITY_PAGE_SIZE) + const providerOps = await this.liquidityProvider.GetOperations(incoming, outgoing, limit) + const invoices = await this.BuildLiquidityAssetOperationsPage( + providerOps.latestIncomingInvoiceOperations, + limit, + 'receiving_invoice', + ) + const payments = await this.BuildLiquidityAssetOperationsPage( + providerOps.latestOutgoingInvoiceOperations, + limit, + 'payment', + ) const balance = await this.liquidityProvider.GetUserState() return { pubkey: provider.provider_pubkey, @@ -429,169 +492,325 @@ export class AdminManager { } } - async GetProviderInvoiceAssetOperation(op: Types.UserOperation): Promise { - const ts = Number(op.paidAtUnix) - const amount = Number(op.amount) - const invoice = op.identifier - const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(invoice) - if (userInvoice) { - const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP } - return { ts, amount, tracked } - } - const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice", invoice) - if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } - } - return { ts, amount, tracked: undefined } - } - - async GetProviderPaymentAssetOperation(op: Types.UserOperation): Promise { - const ts = Number(op.paidAtUnix) - const amount = Number(op.amount) - const invoice = op.identifier - const userInvoice = await this.storage.paymentStorage.GetPaymentOwner(invoice) - if (userInvoice) { - const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP } - return { ts, amount, tracked } - } - const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice_payment", invoice) - if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } - } - return { ts, amount, tracked: undefined } - } - - async GetLndAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq, provider: TrackedProvider): Promise { + private async BuildLiquidityAssetOperationsPage( + userOps: Types.UserOperations, + limit: number, + lookupType: 'payment' | 'receiving_invoice', + ): Promise { + const tracker = await this.BuildAssetOperationTracker({ + paymentInvoices: lookupType === 'payment' ? userOps.operations.map(op => op.identifier) : [], + receivingInvoices: lookupType === 'receiving_invoice' ? userOps.operations.map(op => op.identifier) : [], + txHashes: [], + addressTxKeys: [], + }) + const operations = userOps.operations.map(op => { + const ts = Number(op.paidAtUnix) + const amount = Number(op.amount) + return lookupType === 'payment' + ? tracker.fromPayment(op.identifier, ts, amount) + : tracker.fromReceivingInvoice(op.identifier, ts, amount) + }) + const hasMore = userOps.operations.length >= limit + return { + operations, + has_more: hasMore, + next_cursor: hasMore ? userOps.toIndex : undefined, + } + } + + private async BuildAssetOperationTracker(keys: { + paymentInvoices: string[] + receivingInvoices: string[] + txHashes: string[] + addressTxKeys: { address: string, txHash: string, outputIndex: number }[] + }): Promise { + const paymentInvoices = [...new Set(keys.paymentInvoices)] + const receivingInvoices = [...new Set(keys.receivingInvoices)] + const txHashes = [...new Set(keys.txHashes)] + const chainIds = keys.addressTxKeys.map(k => `${k.address}:${k.txHash}:${k.outputIndex}`) + const [ + paymentOwners, + invoiceOwners, + txHashOwners, + addressTxs, + rootInvoicePayments, + rootInvoices, + rootChainPayments, + rootChainOps, + ] = await Promise.all([ + this.storage.paymentStorage.GetPaymentOwners(paymentInvoices), + this.storage.paymentStorage.GetInvoiceOwners(receivingInvoices), + this.storage.paymentStorage.GetTxHashPaymentOwners(txHashes), + this.storage.paymentStorage.GetAddressReceivingTransactionsByTxHashes(txHashes), + this.storage.metricsStorage.GetRootOperationsByIdentifiers('invoice_payment', paymentInvoices), + this.storage.metricsStorage.GetRootOperationsByIdentifiers('invoice', receivingInvoices), + this.storage.metricsStorage.GetRootOperationsByIdentifiers('chain_payment', txHashes), + this.storage.metricsStorage.GetRootOperationsByIdentifiers('chain', chainIds), + ]) + return new AssetOperationTracker( + paymentOwners, + invoiceOwners, + txHashOwners, + addressTxs, + rootInvoicePayments, + rootInvoices, + rootChainPayments, + rootChainOps, + ) + } + + async GetLndAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReqV2, provider: TrackedProvider): Promise { const info = await this.lnd.GetInfo() if (provider.provider_pubkey !== info.identityPubkey) { return { pubkey: provider.provider_pubkey, tracked: undefined } } - const latestLndPayments = await this.lnd.GetAllPayments(req.limit_payments || 50) - const payments: Types.AssetOperation[] = [] - for (const payment of latestLndPayments.payments) { - if (payment.status !== Payment_PaymentStatus.SUCCEEDED) { - continue - } - const assetOp = await this.GetPaymentAssetOperation(payment) - payments.push(assetOp) - } - const invoices: Types.AssetOperation[] = [] - const paidInvoices = await this.lnd.GetAllInvoices(req.limit_invoices || 100) - for (const invoiceEntry of paidInvoices.invoices) { - if (invoiceEntry.state !== Invoice_InvoiceState.SETTLED) { - continue + const filter = req.lnd_providers.find(p => p.pubkey === provider.provider_pubkey) + const paymentsLimit = clampPageLimit(filter?.limit_payments, DEFAULT_LND_PAGE_SIZE, MAX_PAGE_SIZE) + const paymentsOffset = filter?.payment_index_offset || 0 + const invoicesLimit = clampPageLimit(filter?.limit_invoices, DEFAULT_LND_PAGE_SIZE, MAX_PAGE_SIZE) + const invoicesOffset = filter?.invoice_index_offset || 0 + const txLimit = clampPageLimit(filter?.limit_transactions, DEFAULT_LND_PAGE_SIZE, MAX_PAGE_SIZE) + const txOffset = filter?.tx_index_offset || 0 + + const [payments, invoices, txPages, balance] = await Promise.all([ + this.FetchLndPaymentPage(paymentsLimit, paymentsOffset), + this.FetchLndInvoicePage(invoicesLimit, invoicesOffset), + this.FetchLndTransactionPages(info.blockHeight, txLimit, txOffset), + this.lnd.GetBalance(), + ]) + const channelsBalance = balance.channelsBalance.reduce((acc, c) => acc + Number(c.localBalanceSats), 0) + return { + pubkey: provider.provider_pubkey, + tracked: { + confirmed_balance: Number(balance.confirmedBalance), + unconfirmed_balance: Number(balance.unconfirmedBalance), + channels_balance: channelsBalance, + payments, + invoices, + incoming_tx: txPages.incoming_tx, + outgoing_tx: txPages.outgoing_tx, } - const assetOp = await this.GetInvoiceAssetOperation(invoiceEntry) - invoices.push(assetOp) } - const latestLndTransactions = await this.lnd.GetTransactions(info.blockHeight) - const txOuts: Types.AssetOperation[] = [] - const txIns: Types.AssetOperation[] = [] - for (const transaction of latestLndTransactions.transactions) { + } + + private async BuildLndAssetOperationsPage( + limit: number, + batch: T[], + firstIndexOffset: number | undefined, + include: (item: T) => boolean, + extractKeys: (items: T[]) => { + paymentInvoices: string[] + receivingInvoices: string[] + }, + toAssetOp: (item: T, tracker: AssetOperationTracker) => Types.AssetOperation, + ): Promise { + const included = batch.filter(include) + const keys = extractKeys(included) + const tracker = await this.BuildAssetOperationTracker({ + ...keys, + txHashes: [], + addressTxKeys: [], + }) + const operations = included.map(item => toAssetOp(item, tracker)) + const hasMore = batch.length >= limit + return { + operations, + has_more: hasMore, + next_index_offset: hasMore ? firstIndexOffset : undefined, + } + } + + private async FetchLndPaymentPage(limit: number, indexOffset: number): Promise { + const res = await this.lnd.GetAllPayments(limit, indexOffset) + const firstIndexOffset = Number(res.firstIndexOffset) + return this.BuildLndAssetOperationsPage( + limit, + res.payments, + firstIndexOffset, + payment => payment.status === Payment_PaymentStatus.SUCCEEDED, + payments => ({ paymentInvoices: payments.map(p => p.paymentRequest), receivingInvoices: [] }), + (payment, tracker) => tracker.fromPayment( + payment.paymentRequest, + Number(payment.creationTimeNs / (BigInt(1000_000_000))), + Number(payment.valueSat), + ), + ) + } + + private async FetchLndInvoicePage(limit: number, indexOffset: number): Promise { + const res = await this.lnd.GetAllInvoices(limit, indexOffset) + const firstIndexOffset = Number(res.firstIndexOffset) + return this.BuildLndAssetOperationsPage( + limit, + res.invoices, + firstIndexOffset, + invoice => invoice.state === Invoice_InvoiceState.SETTLED, + invoices => ({ paymentInvoices: [], receivingInvoices: invoices.map(i => i.paymentRequest) }), + (invoice, tracker) => tracker.fromReceivingInvoice( + invoice.paymentRequest, + Number(invoice.settleDate), + Number(invoice.amtPaidSat), + ), + ) + } + + private async FetchLndTransactionPages( + blockHeight: number, + limit: number, + indexOffset: number + ): Promise<{ incoming_tx: Types.LndAssetOperationsPage, outgoing_tx: Types.LndAssetOperationsPage }> { + const res = await this.lnd.GetTransactions(blockHeight, indexOffset, limit) + const incomingItems: { transaction: Transaction, output: OutputDetail }[] = [] + const outgoingTxs: Transaction[] = [] + for (const transaction of res.transactions) { for (const output of transaction.outputDetails) { if (output.isOurAddress) { - const assetOp = await this.GetTxOutAssetOperation(transaction, output) - txOuts.push(assetOp) + incomingItems.push({ transaction, output }) } } - // we only produce TXs with a single output const input = transaction.previousOutpoints.find(p => p.isOurOutput) if (input) { - const assetOp = await this.GetTxInAssetOperation(transaction) - txIns.push(assetOp) + outgoingTxs.push(transaction) } } - const balance = await this.lnd.GetBalance() - const channelsBalance = balance.channelsBalance.reduce((acc, c) => acc + Number(c.localBalanceSats), 0) + const txHashes = [...new Set([ + ...incomingItems.map(({ transaction }) => transaction.txHash), + ...outgoingTxs.map(tx => tx.txHash), + ])] + const tracker = await this.BuildAssetOperationTracker({ + paymentInvoices: [], + receivingInvoices: [], + txHashes, + addressTxKeys: incomingItems.map(({ transaction, output }) => ({ + address: output.address, + txHash: transaction.txHash, + outputIndex: Number(output.outputIndex), + })), + }) + const incoming = incomingItems.map(({ transaction, output }) => tracker.fromTxOut(transaction, output)) + const outgoing = outgoingTxs.map(tx => tracker.fromTxIn(tx)) + const hasMore = res.transactions.length >= limit + const nextIndexOffset = hasMore && res.transactions.length > 0 ? Number(res.firstIndex) : undefined + const toPage = (operations: Types.AssetOperation[]): Types.LndAssetOperationsPage => ({ + operations, + has_more: hasMore, + next_index_offset: nextIndexOffset, + }) return { - pubkey: provider.provider_pubkey, - tracked: { - confirmed_balance: Number(balance.confirmedBalance), - unconfirmed_balance: Number(balance.unconfirmedBalance), - channels_balance: channelsBalance, - payments, - invoices, - incoming_tx: txOuts, // tx outputs, are incoming sats - outgoing_tx: txIns, // tx inputs, are outgoing sats - } + incoming_tx: toPage(incoming), + outgoing_tx: toPage(outgoing), } } - async GetPaymentAssetOperation(payment: Payment): Promise { - const invoice = payment.paymentRequest - const userInvoice = await this.storage.paymentStorage.GetPaymentOwner(invoice) - const ts = Number(payment.creationTimeNs / (BigInt(1000_000_000))) - const amount = Number(payment.valueSat) - if (userInvoice) { - const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP } - return { ts, amount, tracked } + async BumpTx(req: Types.BumpTx): Promise { + await this.lnd.BumpFee(req.txid, req.output_index, req.sat_per_vbyte) + } + +} + +class AssetOperationTracker { + private paymentOwnerMap: Map + private invoiceOwnerMap: Map + private txHashOwnerMap: Map + private addressTxMap: Map + private rootInvoicePaymentMap: Map + private rootInvoiceMap: Map + private rootChainPaymentMap: Map + private rootChainMap: Map + + constructor( + paymentOwners: UserInvoicePayment[], + invoiceOwners: UserReceivingInvoice[], + txHashOwners: UserTransactionPayment[], + addressTxs: AddressReceivingTransaction[], + rootInvoicePayments: RootOperation[], + rootInvoices: RootOperation[], + rootChainPayments: RootOperation[], + rootChainOps: RootOperation[], + ) { + this.paymentOwnerMap = new Map(paymentOwners.map(p => [p.invoice, p])) + this.invoiceOwnerMap = new Map(invoiceOwners.map(p => [p.invoice, p])) + this.txHashOwnerMap = new Map(txHashOwners.map(p => [p.tx_hash, p])) + this.addressTxMap = new Map(addressTxs.map(p => [`${p.user_address.address}:${p.tx_hash}`, p])) + this.rootInvoicePaymentMap = new Map(rootInvoicePayments.map(o => [o.operation_identifier, o])) + this.rootInvoiceMap = new Map(rootInvoices.map(o => [o.operation_identifier, o])) + this.rootChainPaymentMap = new Map(rootChainPayments.map(o => [o.operation_identifier, o])) + this.rootChainMap = new Map(rootChainOps.map(o => [o.operation_identifier, o])) + } + + fromPayment(invoice: string, ts: number, amount: number): Types.AssetOperation { + const userPayment = this.paymentOwnerMap.get(invoice) + if (userPayment) { + return { + ts, amount, tracked: { + ts: userPayment.paid_at_unix, amount: userPayment.paid_amount, + type: USER_OP, user_id: userPayment.user.user_id, + }, + } } - const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice_payment", invoice) + const rootOp = this.rootInvoicePaymentMap.get(invoice) if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } + return { ts, amount, tracked: { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } } } return { ts, amount, tracked: undefined } } - async GetInvoiceAssetOperation(invoiceEntry: Invoice): Promise { - const invoice = invoiceEntry.paymentRequest - const ts = Number(invoiceEntry.settleDate) - const amount = Number(invoiceEntry.amtPaidSat) - const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(invoice) + fromReceivingInvoice(invoice: string, ts: number, amount: number): Types.AssetOperation { + const userInvoice = this.invoiceOwnerMap.get(invoice) if (userInvoice) { - const tracked: Types.TrackedOperation = { ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, type: USER_OP } - return { ts, amount, tracked } + return { + ts, amount, tracked: { + ts: userInvoice.paid_at_unix, amount: userInvoice.paid_amount, + type: USER_OP, user_id: userInvoice.user.user_id, + }, + } } - const rootOp = await this.storage.metricsStorage.GetRootOperation("invoice", invoice) + const rootOp = this.rootInvoiceMap.get(invoice) if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } + return { ts, amount, tracked: { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } } } return { ts, amount, tracked: undefined } } - async GetTxInAssetOperation(tx: Transaction): Promise { + fromTxIn(tx: Transaction): Types.AssetOperation { const ts = Number(tx.timeStamp) const amount = Number(tx.amount) - const userOp = await this.storage.paymentStorage.GetTxHashPaymentOwner(tx.txHash) + const userOp = this.txHashOwnerMap.get(tx.txHash) if (userOp) { - // user transaction payments are actually deprecated from lnd, but we keep this for consstency - const tracked: Types.TrackedOperation = { ts: userOp.paid_at_unix, amount: userOp.paid_amount, type: USER_OP } - return { ts, amount, tracked } + return { + ts, amount, tracked: { + ts: userOp.paid_at_unix, amount: userOp.paid_amount, + type: USER_OP, user_id: userOp.user.user_id, + }, + } } - const rootOp = await this.storage.metricsStorage.GetRootOperation("chain_payment", tx.txHash) + const rootOp = this.rootChainPaymentMap.get(tx.txHash) if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } + return { ts, amount, tracked: { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } } } return { ts, amount, tracked: undefined } } - async GetTxOutAssetOperation(tx: Transaction, output: OutputDetail): Promise { + fromTxOut(tx: Transaction, output: OutputDetail): Types.AssetOperation { const ts = Number(tx.timeStamp) const amount = Number(output.amount) const outputIndex = Number(output.outputIndex) - const userOp = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner(output.address, tx.txHash) + const userOp = this.addressTxMap.get(`${output.address}:${tx.txHash}`) if (userOp) { - const tracked: Types.TrackedOperation = { ts: userOp.paid_at_unix, amount: userOp.paid_amount, type: USER_OP } - return { ts, amount, tracked } + return { + ts, amount, tracked: { + ts: userOp.paid_at_unix, amount: userOp.paid_amount, + type: USER_OP, user_id: userOp.user_address.user.user_id, + }, + } } - const rootOp = await this.storage.metricsStorage.GetRootAddressTransaction(output.address, tx.txHash, outputIndex) + const rootOp = this.rootChainMap.get(`${output.address}:${tx.txHash}:${outputIndex}`) if (rootOp) { - const tracked: Types.TrackedOperation = { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } - return { ts, amount, tracked } + return { ts, amount, tracked: { ts: rootOp.at_unix, amount: rootOp.operation_amount, type: ROOT_OP } } } return { ts, amount, tracked: undefined } } - - async BumpTx(req: Types.BumpTx): Promise { - await this.lnd.BumpFee(req.txid, req.output_index, req.sat_per_vbyte) - } - } const getDataPath = (dataDir: string, dataPath: string) => { diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 4a7848fe1..e9a02718f 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -7,6 +7,7 @@ import { OfferPriceType, ndebitEncode, nmanageEncode, nofferEncode } from '@shoc import { getLogger } from '../helpers/logger.js' import SettingsManager from './settingsManager.js' import { assertCallbackUrlAllowed } from '../helpers/safeOutboundFetch.js' +import { clampPageLimit, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '../helpers/pageLimit.js' export default class { storage: Storage @@ -120,6 +121,44 @@ export default class { await this.storage.applicationStorage.UpdateAppUserMessagingToken(user.identifier, req.device_id, req.firebase_messaging_token); } + async GetUsersAdminInfo(req: Types.UsersAdminInfoRequest): Promise { + const { users, total } = await this.storage.userStorage.GetUsers({ + skip: req.skip, + take: clampPageLimit(req.take, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE), + }) + const applications = await this.storage.applicationStorage.GetApplications() + + const apps: Record = {} + applications.forEach(a => apps[a.owner.user_id] = a.app_id) + + const appUsers = await this.storage.applicationStorage.GetAppUsersForUsers(users.map(u => u.user_id)) + const appUsersByUserId = new Map() + for (const a of appUsers) { + const userId = a.user.user_id + let appUsersInfo = appUsersByUserId.get(userId) + if (!appUsersInfo) { + appUsersInfo = [] + appUsersByUserId.set(userId, appUsersInfo) + } + appUsersInfo.push({ + app_user_id: a.identifier, + npub: a.nostr_public_key || "", + has_callback_url: a.callback_url !== "", + has_topic_id: a.topic_id !== "", + }) + } + + const usersInfo: Types.UserAdminInfo[] = users.map(user => ({ + user_id: user.user_id, + balance: user.balance_sats, + locked: user.locked, + app_users: appUsersByUserId.get(user.user_id) ?? [], + owner_of_app_id: apps[user.user_id] + })) + + return { users: usersInfo, total } + } + async CleanupInactiveUsers() { this.log("Cleaning up inactive users") const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(365) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index f10d63bf2..948b92a5f 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -317,15 +317,29 @@ export class LiquidityProvider { return res } - GetOperations = async (max = 200) => { + newGetUserOperationsRequest = (incoming: Types.OperationsCursor, outgoing: Types.OperationsCursor, limit: number) => { + return { + latestIncomingInvoice: incoming, + latestOutgoingInvoice: outgoing, + latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { ts: 0, id: 0 }, + latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: limit + } + } + + GetOperations = async (incoming: Types.OperationsCursor | undefined, outgoing: Types.OperationsCursor | undefined, limit: number | undefined) => { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") } - const res = await this.client.GetUserOperations({ - latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, - latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { ts: 0, id: 0 }, - latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: max - }) + const latestIncomingInvoice = incoming || { ts: 0, id: 0 } + const latestOutgoingInvoice = outgoing || { ts: 0, id: 0 } + const maxSize = limit || 20 + const res = await Promise.race([ + this.client.GetUserOperations(this.newGetUserOperationsRequest(latestIncomingInvoice, latestOutgoingInvoice, maxSize)), + new Promise(res => setTimeout(() => res(false), 10 * 1000)) + ]) + if (res === false) { + throw new Error("timeout getting operations") + } if (res.status === 'ERROR') { this.log("error getting operations", res.reason) throw new Error(res.reason) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 4d3b6f7ca..531fbedcd 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -23,6 +23,7 @@ import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js' import { LndAddress } from '../lnd/lnd.js' import Metrics from '../metrics/index.js' import { TxPointSettings } from '../storage/tlv/stateBundler.js' +import { clampPageLimit, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '../helpers/pageLimit.js' import { PaymentSideEffects } from './paymentSideEffects.js' interface UserOperationInfo { serial_id: number @@ -1074,18 +1075,19 @@ export default class { } } - async GetUserOperations(userId: string, req: Types.GetUserOperationsRequest): Promise { + async GetUserOperations(userId: string, req: Types.GetUserOperationsRequest, admin: boolean = false): Promise { const user = await this.storage.userStorage.GetUser(userId) - if (user.locked) { + if (user.locked && !admin) { throw new Error("user is banned, cannot retrieve operations") } + const maxSize = admin ? clampPageLimit(req.max_size, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE) : req.max_size const [outgoingInvoices, outgoingTransactions, incomingInvoices, incomingTransactions, incomingUserToUser, outgoingUserToUser] = await Promise.all([ - this.storage.paymentStorage.GetUserInvoicePayments(userId, req.latestOutgoingInvoice.id, req.max_size), // - this.storage.paymentStorage.GetUserTransactionPayments(userId, req.latestOutgoingTx.id, req.max_size), - this.storage.paymentStorage.GetUserInvoicesFlaggedAsPaid(user.serial_id, req.latestIncomingInvoice.id, req.latestIncomingInvoice.ts, req.max_size), - this.storage.paymentStorage.GetUserReceivingTransactions(userId, req.latestIncomingTx.id, req.max_size), - this.storage.paymentStorage.GetUserToUserReceivedPayments(userId, req.latestIncomingUserToUserPayment.id, req.max_size), - this.storage.paymentStorage.GetUserToUserSentPayments(userId, req.latestOutgoingUserToUserPayment.id, req.max_size) + this.storage.paymentStorage.GetUserInvoicePayments(userId, req.latestOutgoingInvoice.id, maxSize), // + this.storage.paymentStorage.GetUserTransactionPayments(userId, req.latestOutgoingTx.id, maxSize), + this.storage.paymentStorage.GetUserInvoicesFlaggedAsPaid(user.serial_id, req.latestIncomingInvoice.id, req.latestIncomingInvoice.ts, maxSize), + this.storage.paymentStorage.GetUserReceivingTransactions(userId, req.latestIncomingTx.id, maxSize), + this.storage.paymentStorage.GetUserToUserReceivedPayments(userId, req.latestIncomingUserToUserPayment.id, maxSize), + this.storage.paymentStorage.GetUserToUserSentPayments(userId, req.latestOutgoingUserToUserPayment.id, maxSize) ]) return { latestIncomingInvoiceOperations: this.mapOperations(incomingInvoices, Types.UserOperationType.INCOMING_INVOICE, true), @@ -1093,7 +1095,8 @@ export default class { latestOutgoingInvoiceOperations: this.mapOperations(outgoingInvoices, Types.UserOperationType.OUTGOING_INVOICE, false), latestOutgoingTxOperations: this.mapOperations(outgoingTransactions, Types.UserOperationType.OUTGOING_TX, false), latestIncomingUserToUserPayemnts: this.mapOperations(incomingUserToUser, Types.UserOperationType.INCOMING_USER_TO_USER, true), - latestOutgoingUserToUserPayemnts: this.mapOperations(outgoingUserToUser, Types.UserOperationType.OUTGOING_USER_TO_USER, false) + latestOutgoingUserToUserPayemnts: this.mapOperations(outgoingUserToUser, Types.UserOperationType.OUTGOING_USER_TO_USER, false), + user_id: userId } } diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index e893073dd..f1fc15f93 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -144,6 +144,9 @@ export default (mainHandler: Main): Types.ServerMethods => { GetAssetsAndLiabilities: async ({ ctx, req }) => { return mainHandler.adminManager.GetAssetsAndLiabilities(req) }, + GetAssetsAndLiabilitiesV2: async ({ ctx, req }) => { + return mainHandler.adminManager.GetAssetsAndLiabilitiesV2(req) + }, GetProvidersDisruption: async () => { return mainHandler.metricsManager.GetProvidersDisruption() }, @@ -162,6 +165,13 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.appUserManager.BanUser(req.user_id) }, + GetUsersAdminInfo: async ({ ctx, req }) => { + return mainHandler.appUserManager.GetUsersAdminInfo(req) + }, + GetUserOperationsFromAdmin: async ({ req }) => { + if (!req.user_id) throw new Error("user_id is required") + return mainHandler.paymentManager.GetUserOperations(req.user_id, req, true) + }, GetSeed: async ({ ctx }) => { return mainHandler.unlocker.GetSeed() }, @@ -181,7 +191,9 @@ export default (mainHandler: Main): Types.ServerMethods => { return mainHandler.appUserManager.UpdateCallbackUrl(ctx, req) }, GetUserOperations: async ({ ctx, req }) => { - return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req) + const sanitizedReq = { ...req } + delete sanitizedReq.user_id + return mainHandler.paymentManager.GetUserOperations(ctx.user_id, sanitizedReq) }, GetPaymentState: async ({ ctx, req }) => { diff --git a/src/services/storage/applicationStorage.ts b/src/services/storage/applicationStorage.ts index 402e7773a..d2670e5cf 100644 --- a/src/services/storage/applicationStorage.ts +++ b/src/services/storage/applicationStorage.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { Between, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm" +import { Between, FindOperator, In, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm" import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { Application } from "./entity/Application.js" import UserStorage from './userStorage.js'; @@ -142,6 +142,13 @@ export default class { return this.dbs.Find('ApplicationUser', { where: { user: { user_id: userId } } }, txId) } + async GetAppUsersForUsers(userIds: string[], txId?: string): Promise { + if (userIds.length === 0) { + return [] + } + return this.dbs.Find('ApplicationUser', { where: { user: { user_id: In(userIds) } } }, txId) + } + async IsApplicationOwner(userId: string, txId?: string) { return this.dbs.FindOne('Application', { where: { owner: { user_id: userId } } }, txId) } diff --git a/src/services/storage/db/serializationHelpers.ts b/src/services/storage/db/serializationHelpers.ts index ee19201f0..ab6a1b0c7 100644 --- a/src/services/storage/db/serializationHelpers.ts +++ b/src/services/storage/db/serializationHelpers.ts @@ -10,7 +10,9 @@ type SerializedFindOperator = { export function serializeFindOperator(operator: FindOperator): SerializedFindOperator { let value: any; if (Array.isArray(operator['value']) && operator['type'] !== 'between') { - value = operator['value'].map(serializeFindOperator); + value = operator['value'].map(item => + item instanceof FindOperator ? serializeFindOperator(item) : item + ); } else if ((operator as any).child !== undefined) { // Not(IsNull()) etc.: TypeORM's .value getter unwraps nested FindOperators, so we'd lose the inner operator. Use .child to serialize the nested operator. value = serializeFindOperator((operator as any).child); diff --git a/src/services/storage/db/storageInterface.ts b/src/services/storage/db/storageInterface.ts index 976ec20d6..b901455e6 100644 --- a/src/services/storage/db/storageInterface.ts +++ b/src/services/storage/db/storageInterface.ts @@ -4,7 +4,7 @@ import { DbSettings, MainDbNames } from './db.js'; import { DeepPartial, FindOptionsWhere } from 'typeorm'; import { ConnectOperation, DeleteOperation, RemoveOperation, FindOneOperation, - FindOperation, UpdateOperation, CreateAndSaveOperation, StartTxOperation, + FindOperation, FindAndCountOperation, UpdateOperation, CreateAndSaveOperation, StartTxOperation, EndTxOperation, QueryOptions, OperationResponse, IStorageOperation, IncrementOperation, @@ -111,6 +111,12 @@ export class StorageInterface extends EventEmitter { return this.handleOp(findOp) } + FindAndCount(entity: DBNames, q: QueryOptions, txId?: string, debug = false): Promise<[T[], number]> { + const opId = Math.random().toString() + const findAndCountOp: FindAndCountOperation = { type: 'findAndCount', entity, opId, q, txId, debug } + return this.handleOp<[T[], number]>(findAndCountOp) + } + Sum(entity: DBNames, columnName: PickKeysByType, q: WhereCondition, txId?: string): Promise { const opId = Math.random().toString() const sumOp: SumOperation = { type: 'sum', entity, opId, columnName, q, txId } diff --git a/src/services/storage/db/storageProcessor.ts b/src/services/storage/db/storageProcessor.ts index e6f657401..b145304fa 100644 --- a/src/services/storage/db/storageProcessor.ts +++ b/src/services/storage/db/storageProcessor.ts @@ -110,6 +110,15 @@ export type FindOperation = { debug?: boolean } +export type FindAndCountOperation = { + type: 'findAndCount' + entity: DBNames + opId: string + q: QueryOptions + txId?: string + debug?: boolean +} + export type SumOperation = { type: 'sum' entity: DBNames @@ -139,7 +148,7 @@ export interface IStorageOperation { } export type StorageOperation = ConnectOperation | StartTxOperation | EndTxOperation | DeleteOperation | RemoveOperation | UpdateOperation | - FindOneOperation | FindOperation | CreateAndSaveOperation | IncrementOperation | DecrementOperation | SumOperation | PingOperation + FindOneOperation | FindOperation | FindAndCountOperation | CreateAndSaveOperation | IncrementOperation | DecrementOperation | SumOperation | PingOperation export type SuccessOperationResponse = { success: true, type: string, data: T, opId: string } export type OperationResponse = SuccessOperationResponse | ErrorOperationResponse @@ -222,6 +231,9 @@ class StorageProcessor { case 'find': await this.handleFind(operation); break; + case 'findAndCount': + await this.handleFindAndCount(operation); + break; case 'sum': await this.handleSum(operation); break; @@ -460,6 +472,18 @@ class StorageProcessor { }); } + private async handleFindAndCount(operation: FindAndCountOperation) { + const res = await this.handleRead(operation.txId, eM => { + return eM.getRepository(this.getEntity(operation.entity)).findAndCount(operation.q) + }) + this.sendResponse({ + success: true, + type: 'findAndCount', + data: res, + opId: operation.opId + }); + } + private async handleSum(operation: SumOperation) { const res = await this.handleRead(operation.txId, eM => { return eM.getRepository(this.getEntity(operation.entity)).sum(operation.columnName, operation.q) diff --git a/src/services/storage/metricsStorage.ts b/src/services/storage/metricsStorage.ts index d80d11c95..d85c0e994 100644 --- a/src/services/storage/metricsStorage.ts +++ b/src/services/storage/metricsStorage.ts @@ -1,4 +1,4 @@ -import { Between, DataSource, EntityManager, FindManyOptions, FindOperator, LessThanOrEqual, MoreThanOrEqual } from "typeorm" +import { Between, DataSource, EntityManager, FindManyOptions, FindOperator, In, LessThanOrEqual, MoreThanOrEqual } from "typeorm" import { BalanceEvent } from "./entity/BalanceEvent.js" import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js" import TransactionsQueue from "./db/transactionsQueue.js"; @@ -156,6 +156,12 @@ export default class { async GetRootOperation(opType: RootOperationType, id: string, txId?: string) { return this.dbs.FindOne('RootOperation', { where: { operation_type: opType, operation_identifier: id } }, txId) } + async GetRootOperationsByIdentifiers(opType: RootOperationType, ids: string[], txId?: string) { + if (ids.length === 0) { + return [] + } + return this.dbs.Find('RootOperation', { where: { operation_type: opType, operation_identifier: In(ids) } }, txId) + } async GetRootAddressTransaction(address: string, txHash: string, index: number) { return this.GetRootOperation("chain", `${address}:${txHash}:${index}`) } diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index fc21f1578..5f6f29672 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm" +import { And, Between, Equal, FindOperator, In, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm" import { User } from './entity/User.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; @@ -171,13 +171,37 @@ export default class { async GetTxHashPaymentOwner(txHash: string, txId?: string): Promise { return this.dbs.FindOne('UserTransactionPayment', { where: { tx_hash: txHash } }, txId) } + async GetTxHashPaymentOwners(txHashes: string[], txId?: string): Promise { + if (txHashes.length === 0) { + return [] + } + return this.dbs.Find('UserTransactionPayment', { where: { tx_hash: In(txHashes) } }, txId) + } + async GetAddressReceivingTransactionsByTxHashes(txHashes: string[], txId?: string): Promise { + if (txHashes.length === 0) { + return [] + } + return this.dbs.Find('AddressReceivingTransaction', { where: { tx_hash: In(txHashes) } }, txId) + } async GetInvoiceOwner(paymentRequest: string, txId?: string): Promise { return this.dbs.FindOne('UserReceivingInvoice', { where: { invoice: paymentRequest } }, txId) } + async GetInvoiceOwners(paymentRequests: string[], txId?: string): Promise { + if (paymentRequests.length === 0) { + return [] + } + return this.dbs.Find('UserReceivingInvoice', { where: { invoice: In(paymentRequests) } }, txId) + } async GetPaymentOwner(paymentRequest: string, txId?: string): Promise { return this.dbs.FindOne('UserInvoicePayment', { where: { invoice: paymentRequest } }, txId) } + async GetPaymentOwners(paymentRequests: string[], txId?: string): Promise { + if (paymentRequests.length === 0) { + return [] + } + return this.dbs.Find('UserInvoicePayment', { where: { invoice: In(paymentRequests) } }, txId) + } async GetUser2UserPayment(serialId: number, txId?: string): Promise { return this.dbs.FindOne('UserToUserPayment', { where: { serial_id: serialId } }, txId) } diff --git a/src/services/storage/userStorage.ts b/src/services/storage/userStorage.ts index 632faa783..3711f8322 100644 --- a/src/services/storage/userStorage.ts +++ b/src/services/storage/userStorage.ts @@ -25,6 +25,11 @@ export default class { }, txId) } + async GetUsers(filter: { skip?: number, take?: number }): Promise<{ users: User[], total: number }> { + const [users, total] = await this.dbs.FindAndCount('User', { skip: filter.skip, take: filter.take || 20, order: { updated_at: 'DESC' } }) + return { users, total } + } + /* async AddBasicUser(name: string, secret: string): Promise { return this.DB.transaction(async tx => { const user = await this.AddUser(0, tx) diff --git a/src/tests/adminUsers.spec.ts b/src/tests/adminUsers.spec.ts new file mode 100644 index 000000000..a7adc3031 --- /dev/null +++ b/src/tests/adminUsers.spec.ts @@ -0,0 +1,134 @@ +import * as Types from "../../proto/autogenerated/ts/types.js" +import { defaultInvoiceExpiry } from "../services/storage/paymentStorage.js" +import { expectThrowsAsync, runSanityCheck, safelySetUserBalance, TestBase } from "./testBase.js" + +export const ignore = false +export const dev = false + +const USER1_FUNDING_SATS = 2000 +const USER2_FUNDING_SATS = 500 +const OUTGOING_PAYMENT_SATS = 500 + +const emptyCursor = (): Types.OperationsCursor => ({ id: 0, ts: 0 }) + +const outgoingInvoiceServiceFee = (T: TestBase, amount: number) => + T.main.paymentManager.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, amount, true) + +const getUsersAdminInfo = (T: TestBase, req: Types.UsersAdminInfoRequest = {}) => + T.main.appUserManager.GetUsersAdminInfo(req) + +const getUserOperationsFromAdmin = (T: TestBase, userId: string, maxSize = 50) => + T.main.paymentManager.GetUserOperations(userId, { + user_id: userId, + max_size: maxSize, + latestIncomingInvoice: emptyCursor(), + latestIncomingTx: emptyCursor(), + latestIncomingUserToUserPayment: emptyCursor(), + latestOutgoingInvoice: emptyCursor(), + latestOutgoingTx: emptyCursor(), + latestOutgoingUserToUserPayment: emptyCursor(), + }, true) + +export default async (T: TestBase) => { + await safelySetUserBalance(T, T.user1, USER1_FUNDING_SATS) + await safelySetUserBalance(T, T.user2, USER2_FUNDING_SATS) + await testGetUsersAdminInfo(T) + await testGetUsersAdminInfoPagination(T) + await testGetUserOperationsFromAdminOutgoingPayment(T) + await testGetUserOperationsFromAdminWorksForBannedUser(T) + await runSanityCheck(T) +} + +const testGetUsersAdminInfo = async (T: TestBase) => { + T.d("starting testGetUsersAdminInfo") + + const res = await getUsersAdminInfo(T) + T.expect(Types.UsersAdminInfoValidate(res)).to.equal(null) + T.expect(res.total).to.be.at.least(3) + + const user1 = res.users.find(u => u.user_id === T.user1.userId) + const user2 = res.users.find(u => u.user_id === T.user2.userId) + T.expect(user1).to.not.equal(undefined) + T.expect(user2).to.not.equal(undefined) + T.expect(user1!.balance).to.equal(USER1_FUNDING_SATS) + T.expect(user2!.balance).to.equal(USER2_FUNDING_SATS) + T.expect(user1!.locked).to.equal(false) + + const user1AppUser = user1!.app_users.find(a => a.app_user_id === T.user1.appUserIdentifier) + T.expect(user1AppUser).to.not.equal(undefined) + T.expect(user1AppUser!.has_topic_id).to.equal(true) + + const app = await T.main.storage.applicationStorage.GetApplication(T.app.appId) + const owner = res.users.find(u => u.user_id === app.owner.user_id) + T.expect(owner).to.not.equal(undefined) + T.expect(owner!.owner_of_app_id).to.equal(T.app.appId) + T.d("GetUsersAdminInfo returns balances, app users, and app ownership") +} + +const testGetUsersAdminInfoPagination = async (T: TestBase) => { + T.d("starting testGetUsersAdminInfoPagination") + const full = await getUsersAdminInfo(T) + const page = await getUsersAdminInfo(T, { skip: 0, take: 1 }) + + T.expect(Types.UsersAdminInfoValidate(page)).to.equal(null) + T.expect(page.users.length).to.equal(1) + T.expect(page.total).to.equal(full.total) + T.d("GetUsersAdminInfo pagination respects skip and take") +} + +const testGetUserOperationsFromAdminOutgoingPayment = async (T: TestBase) => { + T.d("starting testGetUserOperationsFromAdminOutgoingPayment") + const serviceFee = outgoingInvoiceServiceFee(T, OUTGOING_PAYMENT_SATS) + + const invoice = await T.externalAccessToOtherLnd.NewInvoice( + OUTGOING_PAYMENT_SATS, "admin-user-ops", defaultInvoiceExpiry, { from: "system", useProvider: false } + ) + await T.main.appUserManager.PayInvoice( + { app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, + { invoice: invoice.payRequest, amount: 0 } + ) + + const user1 = await T.main.storage.userStorage.GetUser(T.user1.userId) + T.expect(user1.balance_sats).to.equal(USER1_FUNDING_SATS - OUTGOING_PAYMENT_SATS - serviceFee) + + const res = await getUserOperationsFromAdmin(T, T.user1.userId) + T.expect(Types.GetUserOperationsResponseValidate(res)).to.equal(null) + T.expect(res.user_id).to.equal(T.user1.userId) + + const incomingFunding = res.latestIncomingInvoiceOperations.operations.find(op => + op.type === Types.UserOperationType.INCOMING_INVOICE && op.amount === USER1_FUNDING_SATS + ) + T.expect(incomingFunding).to.not.equal(undefined) + + const outgoingPayment = res.latestOutgoingInvoiceOperations.operations.find(op => + op.type === Types.UserOperationType.OUTGOING_INVOICE && op.amount === OUTGOING_PAYMENT_SATS + ) + T.expect(outgoingPayment).to.not.equal(undefined) + T.expect(outgoingPayment!.service_fee).to.equal(serviceFee) + T.d("GetUserOperationsFromAdmin returns outgoing invoice payments") +} + +const testGetUserOperationsFromAdminWorksForBannedUser = async (T: TestBase) => { + T.d("starting testGetUserOperationsFromAdminWorksForBannedUser") + const opsReq = { + max_size: 50, + latestIncomingInvoice: emptyCursor(), + latestIncomingTx: emptyCursor(), + latestIncomingUserToUserPayment: emptyCursor(), + latestOutgoingInvoice: emptyCursor(), + latestOutgoingTx: emptyCursor(), + latestOutgoingUserToUserPayment: emptyCursor(), + } + + await T.main.appUserManager.BanUser(T.user1.userId) + await expectThrowsAsync( + T.main.paymentManager.GetUserOperations(T.user1.userId, opsReq, false), + "user is banned, cannot retrieve operations" + ) + + const res = await getUserOperationsFromAdmin(T, T.user1.userId) + T.expect(Types.GetUserOperationsResponseValidate(res)).to.equal(null) + T.expect(res.latestOutgoingInvoiceOperations.operations).to.have.lengthOf(1) + T.expect(res.latestOutgoingInvoiceOperations.operations[0].amount).to.equal(OUTGOING_PAYMENT_SATS) + T.d("GetUserOperationsFromAdmin still works for banned users") +} diff --git a/src/tests/assetsAndLiabilities.spec.ts b/src/tests/assetsAndLiabilities.spec.ts new file mode 100644 index 000000000..8af4a27ec --- /dev/null +++ b/src/tests/assetsAndLiabilities.spec.ts @@ -0,0 +1,116 @@ +import * as Types from "../../proto/autogenerated/ts/types.js" +import { defaultInvoiceExpiry } from "../services/storage/paymentStorage.js" +import Main from "../services/main/index.js" +import { runSanityCheck, safelySetUserBalance, TestBase, TestUserData } from "./testBase.js" +import { initBootstrappedInstance } from "./setupBootstrapped.js" + +export const ignore = false +export const dev = false + +const emptyReq = (): Types.AssetsAndLiabilitiesReqV2 => ({ + lnd_providers: [], + liquidity_providers: [], +}) + +const getAssets = (T: TestBase, req = emptyReq()) => + T.main.adminManager.GetAssetsAndLiabilitiesV2(req) + +export default async (T: TestBase) => { + await testUsersBalance(T) + await testLndProviderTracked(T) + await testLndTrackedUserPayment(T) + await testLndPagination(T) + await testV1Compatibility(T) + await runSanityCheck(T) +} + +const testUsersBalance = async (T: TestBase) => { + T.d("starting testUsersBalance") + await safelySetUserBalance(T, T.user1, 2000) + await safelySetUserBalance(T, T.user2, 500) + const res = await getAssets(T) + T.expect(Types.AssetsAndLiabilitiesV2Validate(res)).to.equal(null) + T.expect(res.users_balance).to.equal(2500) + T.d("users_balance matches total user balances") +} + +const testLndProviderTracked = async (T: TestBase) => { + T.d("starting testLndProviderTracked") + const lndInfo = await T.main.lnd.GetInfo() + const res = await getAssets(T, { + lnd_providers: [{ pubkey: lndInfo.identityPubkey }], + liquidity_providers: [], + }) + const lnd = res.lnds.find(p => p.pubkey === lndInfo.identityPubkey) + T.expect(lnd).to.not.equal(undefined) + T.expect(lnd!.tracked).to.not.equal(undefined) + T.expect(lnd!.tracked!.confirmed_balance).to.be.a("number") + T.expect(lnd!.tracked!.payments.operations).to.be.an("array") + T.expect(lnd!.tracked!.invoices.operations).to.be.an("array") + T.d("lnd provider returns tracked balances and operation pages") +} + +const testLndTrackedUserPayment = async (T: TestBase) => { + T.d("starting testLndTrackedUserPayment") + const invoice = await T.externalAccessToOtherLnd.NewInvoice( + 500, "assets-liabilities", defaultInvoiceExpiry, { from: "system", useProvider: false } + ) + await T.main.appUserManager.PayInvoice( + { app_id: T.user1.appId, user_id: T.user1.userId, app_user_id: T.user1.appUserIdentifier }, + { invoice: invoice.payRequest, amount: 0 } + ) + + const lndInfo = await T.main.lnd.GetInfo() + const res = await getAssets(T, { + lnd_providers: [{ pubkey: lndInfo.identityPubkey, limit_payments: 50 }], + liquidity_providers: [], + }) + const lnd = res.lnds.find(p => p.pubkey === lndInfo.identityPubkey) + const userPayment = lnd!.tracked!.payments.operations.find( + op => op.tracked?.type === Types.TrackedOperationType.USER + && op.tracked.user_id === T.user1.userId + && op.amount === 500 + ) + T.expect(userPayment).to.not.equal(undefined) + T.d("lnd payment is tracked as a user operation") +} + +const testLndPagination = async (T: TestBase) => { + T.d("starting testLndPagination") + const lndInfo = await T.main.lnd.GetInfo() + const res = await getAssets(T, { + lnd_providers: [{ pubkey: lndInfo.identityPubkey, limit_payments: 1 }], + liquidity_providers: [], + }) + const payments = res.lnds[0].tracked!.payments + T.expect(payments.operations.length).to.be.at.most(1) + if (payments.has_more) { + T.expect(payments.next_index_offset).to.be.a("number") + } + T.d("lnd payment pagination respects limit") +} + +const testV1Compatibility = async (T: TestBase) => { + T.d("starting testV1Compatibility") + const res = await T.main.adminManager.GetAssetsAndLiabilities({}) + T.expect(Types.AssetsAndLiabilitiesValidate(res)).to.equal(null) + T.expect(res.users_balance).to.be.a("number") + const lnd = res.lnds.find(p => p.tracked !== undefined) + if (lnd?.tracked) { + T.expect(lnd.tracked.payments).to.be.an("array") + T.expect(lnd.tracked.invoices).to.be.an("array") + T.expect(lnd.tracked.incoming_tx).to.be.an("array") + T.expect(lnd.tracked.outgoing_tx).to.be.an("array") + } + T.d("v1 endpoint returns flat operation arrays") +} + +const inboundPaymentOnBootstrapped = async (T: TestBase, bootstrapped: Main, user: TestUserData) => { + const ctx = { app_id: user.appId, user_id: user.userId, app_user_id: user.appUserIdentifier } + const invoiceRes = await bootstrapped.appUserManager.NewInvoice(ctx, { amountSats: 1000, memo: "assetsLiabilities" }) + await T.externalAccessToOtherLnd.PayInvoice( + invoiceRes.invoice, 0, { routingFeeLimit: 100, serviceFee: 100 }, 1000, + { from: "system", useProvider: false } + ) + await new Promise(resolve => setTimeout(resolve, 200)) +} diff --git a/src/tests/batchAssetLookups.spec.ts b/src/tests/batchAssetLookups.spec.ts new file mode 100644 index 000000000..3f09aa0ba --- /dev/null +++ b/src/tests/batchAssetLookups.spec.ts @@ -0,0 +1,311 @@ +import crypto from 'crypto' +import { User } from '../services/storage/entity/User.js' +import { UserReceivingAddress } from '../services/storage/entity/UserReceivingAddress.js' +import { UserInvoicePayment } from '../services/storage/entity/UserInvoicePayment.js' +import { UserReceivingInvoice } from '../services/storage/entity/UserReceivingInvoice.js' +import { UserTransactionPayment } from '../services/storage/entity/UserTransactionPayment.js' +import { AddressReceivingTransaction } from '../services/storage/entity/AddressReceivingTransaction.js' +import { RootOperation } from '../services/storage/entity/RootOperation.js' +import { StorageTestBase } from './testBase.js' + +export const ignore = false +export const dev = false +export const requires = 'storage' + +type SeedData = { + userA: User + userB: User + addressA: UserReceivingAddress + paymentA: UserInvoicePayment + paymentB: UserInvoicePayment + invoiceA: UserReceivingInvoice + invoiceB: UserReceivingInvoice + txPaymentA: UserTransactionPayment + txPaymentB: UserTransactionPayment + addressTxA: AddressReceivingTransaction + addressTxB: AddressReceivingTransaction + rootInvoicePayment: RootOperation + rootInvoice: RootOperation + rootChainPayment: RootOperation + rootChain: RootOperation +} + +const invoicesFrom = (payments: UserInvoicePayment[]) => + payments.map(p => p.invoice).sort() + +const invoicesFromReceiving = (invoices: UserReceivingInvoice[]) => + invoices.map(i => i.invoice).sort() + +const txHashesFrom = (txs: UserTransactionPayment[]) => + txs.map(t => t.tx_hash).sort() + +export default async (T: StorageTestBase) => { + const seed = await seedData(T) + await testEmptyBatchLookups(T) + await testGetPaymentOwners(T, seed) + await testGetInvoiceOwners(T, seed) + await testGetTxHashPaymentOwners(T, seed) + await testGetAddressReceivingTransactionsByTxHashes(T, seed) + await testGetRootOperationsByIdentifiers(T, seed) +} + +const seedData = async (T: StorageTestBase): Promise => { + const suffix = Date.now().toString() + const userA = await T.storage.dbs.CreateAndSave('User', { + user_id: `batch-asset-user-a-${suffix}`, + balance_sats: 0, + locked: false, + }) + const userB = await T.storage.dbs.CreateAndSave('User', { + user_id: `batch-asset-user-b-${suffix}`, + balance_sats: 0, + locked: false, + }) + const addressA = await T.storage.dbs.CreateAndSave('UserReceivingAddress', { + user: userA, + address: `bcrt1batch${suffix}a`, + callbackUrl: '', + }) + const paymentA = await T.storage.dbs.CreateAndSave('UserInvoicePayment', { + user: userA, + invoice: `lnbc-payment-a-${suffix}`, + paid_amount: 100, + routing_fees: 1, + service_fees: 2, + paid_at_unix: 1_700_000_000, + }) + const paymentB = await T.storage.dbs.CreateAndSave('UserInvoicePayment', { + user: userB, + invoice: `lnbc-payment-b-${suffix}`, + paid_amount: 200, + routing_fees: 2, + service_fees: 3, + paid_at_unix: 1_700_000_100, + }) + const invoiceA = await T.storage.dbs.CreateAndSave('UserReceivingInvoice', { + user: userA, + invoice: `lnbc-invoice-a-${suffix}`, + expires_at_unix: 1_800_000_000, + paid_at_unix: 1_700_000_200, + paid_amount: 300, + }) + const invoiceB = await T.storage.dbs.CreateAndSave('UserReceivingInvoice', { + user: userB, + invoice: `lnbc-invoice-b-${suffix}`, + expires_at_unix: 1_800_000_100, + paid_at_unix: 1_700_000_300, + paid_amount: 400, + }) + const txHashA = crypto.randomBytes(32).toString('hex') + const txHashB = crypto.randomBytes(32).toString('hex') + const txPaymentA = await T.storage.dbs.CreateAndSave('UserTransactionPayment', { + user: userA, + address: addressA.address, + tx_hash: txHashA, + output_index: 0, + paid_amount: 500, + chain_fees: 10, + service_fees: 5, + paid_at_unix: 1_700_000_400, + }) + const txPaymentB = await T.storage.dbs.CreateAndSave('UserTransactionPayment', { + user: userB, + address: `bcrt1batch${suffix}b`, + tx_hash: txHashB, + output_index: 1, + paid_amount: 600, + chain_fees: 11, + service_fees: 6, + paid_at_unix: 1_700_000_500, + }) + const addressTxA = await T.storage.dbs.CreateAndSave('AddressReceivingTransaction', { + user_address: addressA, + tx_hash: txHashA, + output_index: 0, + paid_amount: 700, + service_fee: 7, + paid_at_unix: 1_700_000_600, + }) + const addressTxB = await T.storage.dbs.CreateAndSave('AddressReceivingTransaction', { + user_address: addressA, + tx_hash: txHashB, + output_index: 2, + paid_amount: 800, + service_fee: 8, + paid_at_unix: 1_700_000_700, + }) + const rootInvoicePayment = await T.storage.metricsStorage.AddRootOperation( + 'invoice_payment', paymentA.invoice, 900, + ) + const rootInvoice = await T.storage.metricsStorage.AddRootOperation( + 'invoice', invoiceA.invoice, 1000, + ) + const rootChainPayment = await T.storage.metricsStorage.AddRootOperation( + 'chain_payment', txHashA, 1100, + ) + const rootChain = await T.storage.metricsStorage.AddRootOperation( + 'chain', `${addressA.address}:${txHashA}:0`, 1200, + ) + return { + userA, + userB, + addressA, + paymentA, + paymentB, + invoiceA, + invoiceB, + txPaymentA, + txPaymentB, + addressTxA, + addressTxB, + rootInvoicePayment, + rootInvoice, + rootChainPayment, + rootChain, + } +} + +const testEmptyBatchLookups = async (T: StorageTestBase) => { + T.d('Starting testEmptyBatchLookups') + const paymentStorage = T.storage.paymentStorage + const metricsStorage = T.storage.metricsStorage + + T.expect(await paymentStorage.GetPaymentOwners([])).to.deep.equal([]) + T.expect(await paymentStorage.GetInvoiceOwners([])).to.deep.equal([]) + T.expect(await paymentStorage.GetTxHashPaymentOwners([])).to.deep.equal([]) + T.expect(await paymentStorage.GetAddressReceivingTransactionsByTxHashes([])).to.deep.equal([]) + T.expect(await metricsStorage.GetRootOperationsByIdentifiers('invoice_payment', [])).to.deep.equal([]) + T.expect(await metricsStorage.GetRootOperationsByIdentifiers('invoice', [])).to.deep.equal([]) + T.expect(await metricsStorage.GetRootOperationsByIdentifiers('chain_payment', [])).to.deep.equal([]) + T.expect(await metricsStorage.GetRootOperationsByIdentifiers('chain', [])).to.deep.equal([]) + + T.d('Finished testEmptyBatchLookups') +} + +const testGetPaymentOwners = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetPaymentOwners') + const { paymentA, paymentB, userA, userB } = seed + + const found = await T.storage.paymentStorage.GetPaymentOwners([ + paymentA.invoice, + paymentB.invoice, + 'lnbc-missing-invoice', + ]) + T.expect(invoicesFrom(found)).to.deep.equal(invoicesFrom([paymentA, paymentB])) + + const byUser = new Map(found.map(p => [p.invoice, p.user.user_id])) + T.expect(byUser.get(paymentA.invoice)).to.equal(userA.user_id) + T.expect(byUser.get(paymentB.invoice)).to.equal(userB.user_id) + + const single = await T.storage.paymentStorage.GetPaymentOwners([paymentA.invoice]) + T.expect(single.length).to.equal(1) + T.expect(single[0].invoice).to.equal(paymentA.invoice) + + T.d('Finished testGetPaymentOwners') +} + +const testGetInvoiceOwners = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetInvoiceOwners') + const { invoiceA, invoiceB, userA, userB } = seed + + const found = await T.storage.paymentStorage.GetInvoiceOwners([ + invoiceB.invoice, + invoiceA.invoice, + 'lnbc-missing-invoice', + ]) + T.expect(invoicesFromReceiving(found)).to.deep.equal(invoicesFromReceiving([invoiceA, invoiceB])) + + const byUser = new Map(found.map(i => [i.invoice, i.user.user_id])) + T.expect(byUser.get(invoiceA.invoice)).to.equal(userA.user_id) + T.expect(byUser.get(invoiceB.invoice)).to.equal(userB.user_id) + + T.d('Finished testGetInvoiceOwners') +} + +const testGetTxHashPaymentOwners = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetTxHashPaymentOwners') + const { txPaymentA, txPaymentB, userA, userB } = seed + + const found = await T.storage.paymentStorage.GetTxHashPaymentOwners([ + txPaymentB.tx_hash, + txPaymentA.tx_hash, + crypto.randomBytes(32).toString('hex'), + ]) + T.expect(txHashesFrom(found)).to.deep.equal(txHashesFrom([txPaymentA, txPaymentB])) + + const byUser = new Map(found.map(t => [t.tx_hash, t.user.user_id])) + T.expect(byUser.get(txPaymentA.tx_hash)).to.equal(userA.user_id) + T.expect(byUser.get(txPaymentB.tx_hash)).to.equal(userB.user_id) + + T.d('Finished testGetTxHashPaymentOwners') +} + +const testGetAddressReceivingTransactionsByTxHashes = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetAddressReceivingTransactionsByTxHashes') + const { addressTxA, addressTxB, userA } = seed + + const found = await T.storage.paymentStorage.GetAddressReceivingTransactionsByTxHashes([ + addressTxB.tx_hash, + addressTxA.tx_hash, + crypto.randomBytes(32).toString('hex'), + ]) + T.expect(found.length).to.equal(2) + + const byTxHash = new Map(found.map(t => [t.tx_hash, t])) + T.expect(byTxHash.get(addressTxA.tx_hash)?.paid_amount).to.equal(addressTxA.paid_amount) + T.expect(byTxHash.get(addressTxB.tx_hash)?.paid_amount).to.equal(addressTxB.paid_amount) + T.expect(byTxHash.get(addressTxA.tx_hash)?.user_address.user.user_id).to.equal(userA.user_id) + + T.d('Finished testGetAddressReceivingTransactionsByTxHashes') +} + +const testGetRootOperationsByIdentifiers = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testGetRootOperationsByIdentifiers') + const { + rootInvoicePayment, + rootInvoice, + rootChainPayment, + rootChain, + paymentA, + invoiceA, + txPaymentA, + addressA, + } = seed + + const invoicePayments = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'invoice_payment', + [paymentA.invoice, 'missing-invoice-payment'], + ) + T.expect(invoicePayments.length).to.equal(1) + T.expect(invoicePayments[0].operation_identifier).to.equal(rootInvoicePayment.operation_identifier) + T.expect(invoicePayments[0].operation_amount).to.equal(rootInvoicePayment.operation_amount) + + const invoices = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'invoice', + [invoiceA.invoice], + ) + T.expect(invoices.length).to.equal(1) + T.expect(invoices[0].operation_identifier).to.equal(rootInvoice.operation_identifier) + + const chainPayments = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'chain_payment', + [txPaymentA.tx_hash], + ) + T.expect(chainPayments.length).to.equal(1) + T.expect(chainPayments[0].operation_identifier).to.equal(rootChainPayment.operation_identifier) + + const chainOps = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'chain', + [`${addressA.address}:${txPaymentA.tx_hash}:0`, 'addr:tx:99'], + ) + T.expect(chainOps.length).to.equal(1) + T.expect(chainOps[0].operation_identifier).to.equal(rootChain.operation_identifier) + + const wrongType = await T.storage.metricsStorage.GetRootOperationsByIdentifiers( + 'invoice', + [paymentA.invoice], + ) + T.expect(wrongType.length).to.equal(0) + + T.d('Finished testGetRootOperationsByIdentifiers') +} diff --git a/src/tests/storageOperators.spec.ts b/src/tests/storageOperators.spec.ts new file mode 100644 index 000000000..2c2f6fdc8 --- /dev/null +++ b/src/tests/storageOperators.spec.ts @@ -0,0 +1,428 @@ +import { + And, Between, Equal, ILike, In, IsNull, LessThan, LessThanOrEqual, + MoreThan, MoreThanOrEqual, Not, Like, +} from 'typeorm' +import { User } from '../services/storage/entity/User.js' +import { Application } from '../services/storage/entity/Application.js' +import { InviteToken } from '../services/storage/entity/InviteToken.js' +import { StorageTestBase } from './testBase.js' +import crypto from 'crypto' + +export const ignore = false +export const dev = false +export const requires = 'storage' + +type SeedData = { + low: User + mid: User + high: User + locked: User + app: Application + inviteWithSats: InviteToken + inviteWithoutSats: InviteToken +} + +const userIds = (users: User[]) => users.map(u => u.user_id).sort() + +export default async (T: StorageTestBase) => { + const seed = await seedData(T) + await testPing(T) + await testComparisonOperators(T, seed) + await testBetweenOperator(T, seed) + await testInOperator(T, seed) + await testEqualAndNotOperators(T, seed) + await testAndOperator(T, seed) + await testLikeOperators(T, seed) + await testIsNullOperator(T, seed) + await testOrConditions(T, seed) + await testFindAndCount(T, seed) + await testSum(T, seed) + await testIncrementDecrement(T, seed) + await testDeleteById(T) + await testRemove(T) + await testDateRoundTrip(T, seed) + await testQueryPagination(T, seed) + await testOperatorsInsideTransaction(T, seed) +} + +const seedData = async (T: StorageTestBase): Promise => { + const suffix = Date.now().toString() + const low = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-low-${suffix}`, + balance_sats: 100, + locked: false, + }) + const mid = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-mid-${suffix}`, + balance_sats: 500, + locked: false, + }) + const high = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-high-${suffix}`, + balance_sats: 1000, + locked: false, + }) + const locked = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-locked-${suffix}`, + balance_sats: 200, + locked: true, + }) + const app = await T.storage.dbs.CreateAndSave('Application', { + app_id: crypto.randomBytes(32).toString('hex'), + name: `op-test-app-${suffix}`, + owner: low, + allow_user_creation: false, + }) + const inviteWithSats = await T.storage.dbs.CreateAndSave('InviteToken', { + inviteToken: `token-with-sats-${suffix}`, + used: false, + sats: 42, + application: app, + }) + const inviteWithoutSats = await T.storage.dbs.CreateAndSave('InviteToken', { + inviteToken: `token-without-sats-${suffix}`, + used: false, + sats: undefined, + application: app, + }) + return { low, mid, high, locked, app, inviteWithSats, inviteWithoutSats } +} + +const testPing = async (T: StorageTestBase) => { + T.d('Starting testPing') + await T.storage.dbs.Ping() + T.d('Finished testPing') +} + +const testComparisonOperators = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testComparisonOperators') + const { low, mid, high, locked } = seed + + const lessThan = await T.storage.dbs.Find('User', { + where: { balance_sats: LessThan(500) }, + }) + T.expect(userIds(lessThan)).to.deep.equal(userIds([low, locked])) + + const moreThan = await T.storage.dbs.Find('User', { + where: { balance_sats: MoreThan(400) }, + }) + T.expect(userIds(moreThan)).to.deep.equal(userIds([mid, high])) + + const lessThanOrEqual = await T.storage.dbs.Find('User', { + where: { balance_sats: LessThanOrEqual(200) }, + }) + T.expect(userIds(lessThanOrEqual)).to.deep.equal(userIds([low, locked])) + + const moreThanOrEqual = await T.storage.dbs.Find('User', { + where: { balance_sats: MoreThanOrEqual(500) }, + }) + T.expect(userIds(moreThanOrEqual)).to.deep.equal(userIds([mid, high])) + + const one = await T.storage.dbs.FindOne('User', { + where: { user_id: low.user_id, balance_sats: LessThan(200) }, + }) + T.expect(one?.user_id).to.equal(low.user_id) + + T.d('Finished testComparisonOperators') +} + +const testBetweenOperator = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testBetweenOperator') + const { low, mid, locked } = seed + + const found = await T.storage.dbs.Find('User', { + where: { balance_sats: Between(100, 500) }, + }) + T.expect(userIds(found)).to.deep.equal(userIds([low, mid, locked])) + + T.d('Finished testBetweenOperator') +} + +const testInOperator = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testInOperator') + const { low, high } = seed + + const found = await T.storage.dbs.Find('User', { + where: { balance_sats: In([100, 1000]) }, + }) + T.expect(userIds(found)).to.deep.equal(userIds([low, high])) + + T.d('Finished testInOperator') +} + +const testEqualAndNotOperators = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testEqualAndNotOperators') + const { low, mid, high, locked } = seed + + const lockedUsers = await T.storage.dbs.Find('User', { + where: { locked: Equal(true) }, + }) + T.expect(userIds(lockedUsers)).to.deep.equal([locked.user_id]) + + const unlocked = await T.storage.dbs.Find('User', { + where: { locked: Not(Equal(true)) }, + }) + T.expect(userIds(unlocked)).to.deep.equal(userIds([low, mid, high])) + + T.d('Finished testEqualAndNotOperators') +} + +const testAndOperator = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testAndOperator') + const { mid } = seed + + const found = await T.storage.dbs.Find('User', { + where: { + balance_sats: And(MoreThan(400), LessThan(600)), + locked: Equal(false), + }, + }) + T.expect(found.length).to.equal(1) + T.expect(found[0].user_id).to.equal(mid.user_id) + + T.d('Finished testAndOperator') +} + +const testLikeOperators = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testLikeOperators') + const { low, mid, high, locked } = seed + + const likeFound = await T.storage.dbs.Find('User', { + where: { user_id: Like('op-test-%') }, + }) + T.expect(userIds(likeFound)).to.deep.equal(userIds([low, mid, high, locked])) + + const ilikeFound = await T.storage.dbs.Find('User', { + where: { user_id: ILike('OP-TEST-LOW-%') }, + }) + T.expect(ilikeFound.length).to.equal(1) + T.expect(ilikeFound[0].user_id).to.equal(low.user_id) + + T.d('Finished testLikeOperators') +} + +const testIsNullOperator = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testIsNullOperator') + const { inviteWithSats, inviteWithoutSats } = seed + + const nullSats = await T.storage.dbs.Find('InviteToken', { + where: { sats: IsNull() }, + }) + T.expect(nullSats.length).to.equal(1) + T.expect(nullSats[0].inviteToken).to.equal(inviteWithoutSats.inviteToken) + + const notNullSats = await T.storage.dbs.Find('InviteToken', { + where: { sats: Not(IsNull()) }, + }) + T.expect(notNullSats.length).to.equal(1) + T.expect(notNullSats[0].inviteToken).to.equal(inviteWithSats.inviteToken) + + T.d('Finished testIsNullOperator') +} + +const testOrConditions = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testOrConditions') + const { low, high } = seed + + const found = await T.storage.dbs.Find('User', { + where: [ + { balance_sats: LessThan(150) }, + { balance_sats: MoreThan(900) }, + ], + }) + T.expect(userIds(found)).to.deep.equal(userIds([low, high])) + + T.d('Finished testOrConditions') +} + +const testFindAndCount = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testFindAndCount') + const { low, mid, high, locked } = seed + + const [users, count] = await T.storage.dbs.FindAndCount('User', { + where: { locked: Equal(false) }, + order: { balance_sats: 'ASC' }, + }) + T.expect(count).to.equal(3) + T.expect(userIds(users)).to.deep.equal(userIds([low, mid, high])) + + T.d('Finished testFindAndCount') +} + +const testSum = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testSum') + const { low, mid, high, locked } = seed + const expected = low.balance_sats + mid.balance_sats + high.balance_sats + locked.balance_sats + + const total = await T.storage.dbs.Sum('User', 'balance_sats', { + user_id: Like('op-test-%'), + }) + T.expect(total).to.equal(expected) + + const unlockedTotal = await T.storage.dbs.Sum('User', 'balance_sats', { + locked: Equal(false), + user_id: Like('op-test-%'), + }) + T.expect(unlockedTotal).to.equal(low.balance_sats + mid.balance_sats + high.balance_sats) + + T.d('Finished testSum') +} + +const testIncrementDecrement = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testIncrementDecrement') + const { low } = seed + + const incAffected = await T.storage.dbs.Increment('User', + { user_id: low.user_id }, + 'balance_sats', + 50, + ) + T.expect(incAffected).to.equal(1) + + const afterInc = await T.storage.dbs.FindOne('User', { + where: { user_id: low.user_id }, + }) + T.expect(afterInc?.balance_sats).to.equal(150) + + const decAffected = await T.storage.dbs.Decrement('User', + { user_id: low.user_id }, + 'balance_sats', + 25, + ) + T.expect(decAffected).to.equal(1) + + const afterDec = await T.storage.dbs.FindOne('User', { + where: { user_id: low.user_id }, + }) + T.expect(afterDec?.balance_sats).to.equal(125) + + T.d('Finished testIncrementDecrement') +} + +const testDeleteById = async (T: StorageTestBase) => { + T.d('Starting testDeleteById') + const user = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-delete-${Date.now()}`, + balance_sats: 1, + locked: false, + }) + + const affected = await T.storage.dbs.Delete('User', user.serial_id) + T.expect(affected).to.equal(1) + + const found = await T.storage.dbs.FindOne('User', { + where: { user_id: user.user_id }, + }) + T.expect(found).to.equal(null) + + T.d('Finished testDeleteById') +} + +const testRemove = async (T: StorageTestBase) => { + T.d('Starting testRemove') + const user = await T.storage.dbs.CreateAndSave('User', { + user_id: `op-test-remove-${Date.now()}`, + balance_sats: 1, + locked: false, + }) + + const loaded = await T.storage.dbs.FindOne('User', { + where: { user_id: user.user_id }, + }) + T.expect(loaded).to.not.equal(null) + + const removed = await T.storage.dbs.Remove('User', loaded!) + T.expect(removed.user_id).to.equal(user.user_id) + + const found = await T.storage.dbs.FindOne('User', { + where: { user_id: user.user_id }, + }) + T.expect(found).to.equal(null) + + T.d('Finished testRemove') +} + +const testDateRoundTrip = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testDateRoundTrip') + const { low } = seed + + const found = await T.storage.dbs.FindOne('User', { + where: { + user_id: low.user_id, + created_at: MoreThan(new Date(0)), + }, + }) + T.expect(found).to.not.equal(null) + T.expect(found!.created_at).to.be.instanceOf(Date) + T.expect(found!.updated_at).to.be.instanceOf(Date) + + T.d('Finished testDateRoundTrip') +} + +const testQueryPagination = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testQueryPagination') + const { low, mid, high } = seed + + const page = await T.storage.dbs.Find('User', { + where: { locked: Equal(false), user_id: Like('op-test-%') }, + order: { balance_sats: 'ASC' }, + take: 2, + skip: 1, + }) + T.expect(page.length).to.equal(2) + T.expect(page[0].user_id).to.equal(mid.user_id) + T.expect(page[1].user_id).to.equal(high.user_id) + + T.d('Finished testQueryPagination') +} + +const testOperatorsInsideTransaction = async (T: StorageTestBase, seed: SeedData) => { + T.d('Starting testOperatorsInsideTransaction') + const { mid } = seed + const originalBalance = mid.balance_sats + + await T.storage.StartTransaction(async txId => { + const found = await T.storage.dbs.FindOne('User', { + where: { user_id: mid.user_id, balance_sats: Equal(originalBalance) }, + }, txId) + T.expect(found?.user_id).to.equal(mid.user_id) + + await T.storage.dbs.Update('User', + { user_id: mid.user_id, balance_sats: Equal(originalBalance) }, + { balance_sats: 777 }, + txId, + ) + return null + }, 'storage-operators-tx') + + const updated = await T.storage.dbs.FindOne('User', { + where: { user_id: mid.user_id }, + }) + T.expect(updated?.balance_sats).to.equal(777) + + await T.storage.dbs.Update('User', + { user_id: mid.user_id }, + { balance_sats: originalBalance }, + ) + + try { + await T.storage.StartTransaction(async txId => { + await T.storage.dbs.Update('User', + { user_id: mid.user_id }, + { invalid_column: 'test' } as any, + txId, + ) + return null + }, 'storage-operators-tx-error') + T.expect.fail('Should have thrown an error') + } catch (error) { + T.expect(error).to.not.equal(null) + } + + const afterFailedTx = await T.storage.dbs.FindOne('User', { + where: { user_id: mid.user_id }, + }) + T.expect(afterFailedTx?.balance_sats).to.equal(originalBalance) + + T.d('Finished testOperatorsInsideTransaction') +}