app metrics

This commit is contained in:
boufni95 2023-12-16 21:20:54 +01:00
parent f0dd9a08ac
commit 7e1157caa4
16 changed files with 3056 additions and 2462 deletions

View file

@ -141,13 +141,20 @@ The nostr server will send back a message response, and inside the body there wi
- input: [AuthAppRequest](#AuthAppRequest) - input: [AuthAppRequest](#AuthAppRequest)
- output: [AuthApp](#AuthApp) - output: [AuthApp](#AuthApp)
- GetMetrics - GetUsageMetrics
- auth type: __Admin__ - auth type: __Admin__
- http method: __post__ - http method: __post__
- http route: __/api/admin/metrics__ - http route: __/api/admin/metrics/usage__
- This methods has an __empty__ __request__ body - This methods has an __empty__ __request__ body
- output: [UsageMetrics](#UsageMetrics) - output: [UsageMetrics](#UsageMetrics)
- GetAppsMetrics
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/metrics/apps__
- input: [AppsMetricsRequest](#AppsMetricsRequest)
- output: [AppsMetrics](#AppsMetrics)
- Health - Health
- auth type: __Guest__ - auth type: __Guest__
- http method: __get__ - http method: __get__
@ -414,105 +421,7 @@ The nostr server will send back a message response, and inside the body there wi
## Messages ## Messages
### The content of requests and response from the methods ### The content of requests and response from the methods
### NewAddressRequest ### PayInvoiceRequest
- __addressType__: _[AddressType](#AddressType)_
### DecodeInvoiceResponse
- __amount__: _number_
### Product
- __id__: _string_
- __name__: _string_
- __price_sats__: _number_
### MigrationUpdate
- __closure__: _[ClosureMigration](#ClosureMigration)_ *this field is optional
- __relays__: _[RelaysMigration](#RelaysMigration)_ *this field is optional
### AuthAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_ *this field is optional
### LnurlWithdrawInfoResponse
- __tag__: _string_
- __callback__: _string_
- __k1__: _string_
- __defaultDescription__: _string_
- __minWithdrawable__: _number_
- __maxWithdrawable__: _number_
- __balanceCheck__: _string_
- __payLink__: _string_
### ClosureMigration
- __closes_at_unix__: _number_
### SetMockAppBalanceRequest
- __amount__: _number_
### SendAppUserToAppPaymentRequest
- __from_user_identifier__: _string_
- __amount__: _number_
### NewInvoiceResponse
- __invoice__: _string_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### AddProductRequest
- __name__: _string_
- __price_sats__: _number_
### AddAppInvoiceRequest
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### AddAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __satsPerVByte__: _number_
### PayInvoiceResponse
- __preimage__: _string_
- __amount_paid__: _number_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### LnurlPayInfoResponse
- __tag__: _string_
- __callback__: _string_
- __maxSendable__: _number_
- __minSendable__: _number_
- __metadata__: _string_
- __allowsNostr__: _boolean_
- __nostrPubkey__: _string_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### GetUserOperationsRequest
- __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
- __latestIncomingUserToUserPayment__: _number_
- __latestOutgoingUserToUserPayment__: _number_
### SetMockInvoiceAsPaidRequest
- __invoice__: _string_ - __invoice__: _string_
- __amount__: _number_ - __amount__: _number_
@ -527,49 +436,124 @@ The nostr server will send back a message response, and inside the body there wi
- __network_fee__: _number_ - __network_fee__: _number_
- __confirmed__: _boolean_ - __confirmed__: _boolean_
### OpenChannelResponse
- __channelId__: _string_
### UserOperations ### UserOperations
- __fromIndex__: _number_ - __fromIndex__: _number_
- __toIndex__: _number_ - __toIndex__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_ - __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### AuthApp ### RelaysMigration
- __app__: _[Application](#Application)_ - __relays__: ARRAY of: _string_
- __auth_token__: _string_
### UsageMetrics ### AddAppRequest
- __metrics__: ARRAY of: _[UsageMetric](#UsageMetric)_
### Application
- __name__: _string_ - __name__: _string_
- __id__: _string_ - __allow_user_creation__: _boolean_
- __balance__: _number_
- __npub__: _string_
### AddAppUserRequest ### AddAppUserRequest
- __identifier__: _string_ - __identifier__: _string_
- __fail_if_exists__: _boolean_ - __fail_if_exists__: _boolean_
- __balance__: _number_ - __balance__: _number_
### AddAppUserInvoiceRequest ### OpenChannelResponse
- __receiver_identifier__: _string_ - __channelId__: _string_
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### GetAppUserRequest ### SetMockInvoiceAsPaidRequest
- __invoice__: _string_
- __amount__: _number_
### AuthApp
- __app__: _[Application](#Application)_
- __auth_token__: _string_
### GetAppUserLNURLInfoRequest
- __user_identifier__: _string_ - __user_identifier__: _string_
- __base_url_override__: _string_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __satsPerVByte__: _number_
### NewAddressResponse
- __address__: _string_
### PayAddressResponse
- __txId__: _string_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### DecodeInvoiceRequest
- __invoice__: _string_
### LnurlWithdrawInfoResponse
- __tag__: _string_
- __callback__: _string_
- __k1__: _string_
- __defaultDescription__: _string_
- __minWithdrawable__: _number_
- __maxWithdrawable__: _number_
- __balanceCheck__: _string_
- __payLink__: _string_
### MigrationUpdate
- __closure__: _[ClosureMigration](#ClosureMigration)_ *this field is optional
- __relays__: _[RelaysMigration](#RelaysMigration)_ *this field is optional
### UsageMetrics
- __metrics__: ARRAY of: _[UsageMetric](#UsageMetric)_
### AppsMetricsRequest
- __from_unix__: _number_ *this field is optional
- __to_unix__: _number_ *this field is optional
- __big_user_sats__: _number_ *this field is optional
- __huge_user_sats__: _number_ *this field is optional
- __include_operations__: _boolean_ *this field is optional
### SendAppUserToAppUserPaymentRequest ### SendAppUserToAppUserPaymentRequest
- __from_user_identifier__: _string_ - __from_user_identifier__: _string_
- __to_user_identifier__: _string_ - __to_user_identifier__: _string_
- __amount__: _number_ - __amount__: _number_
### GetAppUserLNURLInfoRequest ### LnurlPayInfoResponse
- __tag__: _string_
- __callback__: _string_
- __maxSendable__: _number_
- __minSendable__: _number_
- __metadata__: _string_
- __allowsNostr__: _boolean_
- __nostrPubkey__: _string_
### AppMetrics
- __app_name__: _string_
- __app_id__: _string_
- __app_npub__: _string_
- __app_balance__: _number_
- __total_received__: _number_
- __total_spent__: _number_
- __total_available__: _number_
- __total_users__: _number_
- __total_big_users__: _number_
- __total_huge_users__: _number_
- __unpaid_invoices__: _number_
- __unpaid_addresses__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### PayAppUserInvoiceRequest
- __user_identifier__: _string_ - __user_identifier__: _string_
- __base_url_override__: _string_ - __invoice__: _string_
- __amount__: _number_
### LiveUserOperation
- __operation__: _[UserOperation](#UserOperation)_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
### Product
- __id__: _string_
- __name__: _string_
- __price_sats__: _number_
### UsageMetric ### UsageMetric
- __processed_at_nano__: _string_ - __processed_at_nano__: _string_
@ -581,17 +565,97 @@ The nostr server will send back a message response, and inside the body there wi
- __batch__: _boolean_ - __batch__: _boolean_
- __nostr__: _boolean_ - __nostr__: _boolean_
- __batch_size__: _number_ - __batch_size__: _number_
- __success__: _boolean_
- __app_id__: _string_
### PayAddressResponse ### AppsMetrics
- __txId__: _string_ - __apps__: ARRAY of: _[AppMetrics](#AppMetrics)_
### Application
- __name__: _string_
- __id__: _string_
- __balance__: _number_
- __npub__: _string_
### LndGetInfoRequest
- __nodeId__: _number_
### SendAppUserToAppPaymentRequest
- __from_user_identifier__: _string_
- __amount__: _number_
### NewInvoiceResponse
- __invoice__: _string_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### LndGetInfoResponse
- __alias__: _string_
### Empty
### AuthAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_ *this field is optional
### AddProductRequest
- __name__: _string_
- __price_sats__: _number_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### UserInfo
- __userId__: _string_
- __balance__: _number_
- __max_withdrawable__: _number_
### ClosureMigration
- __closes_at_unix__: _number_
### GetAppUserRequest
- __user_identifier__: _string_
### DecodeInvoiceResponse
- __amount__: _number_
### SetMockAppBalanceRequest
- __amount__: _number_
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
### AddAppUserInvoiceRequest
- __receiver_identifier__: _string_
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### PayInvoiceResponse
- __preimage__: _string_
- __amount_paid__: _number_
- __operation_id__: _string_ - __operation_id__: _string_
- __service_fee__: _number_ - __service_fee__: _number_
- __network_fee__: _number_ - __network_fee__: _number_
### DecodeInvoiceRequest ### GetUserOperationsRequest
- __invoice__: _string_ - __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
- __latestIncomingUserToUserPayment__: _number_
- __latestOutgoingUserToUserPayment__: _number_
### NewAddressRequest
- __addressType__: _[AddressType](#AddressType)_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### GetUserOperationsResponse ### GetUserOperationsResponse
- __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_ - __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_
@ -601,56 +665,22 @@ The nostr server will send back a message response, and inside the body there wi
- __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_ - __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
- __latestIncomingUserToUserPayemnts__: _[UserOperations](#UserOperations)_ - __latestIncomingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
### NewAddressResponse
- __address__: _string_
### PayAppUserInvoiceRequest
- __user_identifier__: _string_
- __invoice__: _string_
- __amount__: _number_
### SetMockAppUserBalanceRequest
- __user_identifier__: _string_
- __amount__: _number_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
### GetProductBuyLinkResponse ### GetProductBuyLinkResponse
- __link__: _string_ - __link__: _string_
### Empty
### LndGetInfoRequest
- __nodeId__: _number_
### LndGetInfoResponse
- __alias__: _string_
### AppUser ### AppUser
- __identifier__: _string_ - __identifier__: _string_
- __info__: _[UserInfo](#UserInfo)_ - __info__: _[UserInfo](#UserInfo)_
- __max_withdrawable__: _number_ - __max_withdrawable__: _number_
### PayInvoiceRequest ### AddAppInvoiceRequest
- __invoice__: _string_ - __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### SetMockAppUserBalanceRequest
- __user_identifier__: _string_
- __amount__: _number_ - __amount__: _number_
### UserInfo
- __userId__: _string_
- __balance__: _number_
- __max_withdrawable__: _number_
### LiveUserOperation
- __operation__: _[UserOperation](#UserOperation)_
### RelaysMigration
- __relays__: ARRAY of: _string_
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
## Enums ## Enums
### The enumerators used in the messages ### The enumerators used in the messages

File diff suppressed because it is too large Load diff

View file

@ -98,20 +98,42 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } } 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.GetMetrics) throw new Error('method: GetMetrics is not implemented') if (!opts.allowNotImplementedMethods && !methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented')
app.post('/api/admin/metrics', async (req, res) => { app.post('/api/admin/metrics/usage', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetMetrics', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'GetUsageMetrics', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } const stats: Types.RequestStats = { start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {} let authCtx: Types.AuthContext = {}
try { try {
if (!methods.GetMetrics) throw new Error('method: GetMetrics is not implemented') if (!methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization']) const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext authCtx = authContext
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
stats.validate = stats.guard stats.validate = stats.guard
const query = req.query const query = req.query
const params = req.params const params = req.params
const response = await methods.GetMetrics({rpcName:'GetMetrics', ctx:authContext }) const response = await methods.GetUsageMetrics({rpcName:'GetUsageMetrics', ctx:authContext })
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.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented')
app.post('/api/admin/metrics/apps', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAppsMetrics', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics 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.AppsMetricsRequestValidate(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.GetAppsMetrics({rpcName:'GetAppsMetrics', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint() stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response}) res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])

View file

@ -57,10 +57,10 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetMetrics: async (): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => { GetUsageMetrics: async (): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => {
const auth = await params.retrieveAdminAuth() const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/metrics' let finalRoute = '/api/admin/metrics/usage'
const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } }) const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
@ -71,6 +71,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/metrics/apps'
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.AppsMetricsValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
Health: async (): Promise<ResultError | ({ status: 'OK' })> => { Health: async (): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveGuestAuth() const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null') if (auth === null) throw new Error('retrieveGuestAuth() returned null')

File diff suppressed because it is too large Load diff

View file

@ -97,10 +97,16 @@ service LightningPub {
option (http_route) = "/api/admin/app/auth"; option (http_route) = "/api/admin/app/auth";
} }
rpc GetMetrics(structs.Empty) returns (structs.UsageMetrics) { rpc GetUsageMetrics(structs.Empty) returns (structs.UsageMetrics) {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/admin/metrics"; option (http_route) = "/api/admin/metrics/usage";
}
rpc GetAppsMetrics(structs.AppsMetricsRequest) returns (structs.AppsMetrics) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/metrics/apps";
} }
// </Admin> // </Admin>

View file

@ -22,14 +22,44 @@ message UsageMetric {
bool batch = 7; bool batch = 7;
bool nostr = 8; bool nostr = 8;
int64 batch_size = 9; int64 batch_size = 9;
bool success = 10;
string app_id = 11;
} }
message UsageMetrics { message UsageMetrics {
repeated UsageMetric metrics = 1; repeated UsageMetric metrics = 1;
} }
message AppsMetricsRequest {
optional int64 from_unix = 1;
optional int64 to_unix = 2;
optional int64 big_user_sats = 3;
optional int64 huge_user_sats = 4;
optional bool include_operations = 5;
}
message AppMetrics {
string app_name = 1;
string app_id = 2;
string app_npub = 3;
int64 app_balance = 4;
int64 total_received = 5;
int64 total_spent = 6;
int64 total_available = 7;
int64 total_users = 8;
int64 total_big_users = 9;
int64 total_huge_users = 10;
int64 unpaid_invoices = 11;
int64 unpaid_addresses = 12;
repeated UserOperation operations = 100;
}
message AppsMetrics {
repeated AppMetrics apps = 1;
}
message LndGetInfoRequest { message LndGetInfoRequest {
int64 nodeId = 1; int64 nodeId = 1;
} }

View file

@ -58,7 +58,7 @@ export default class {
constructor(settings: MainSettings) { constructor(settings: MainSettings) {
this.settings = settings this.settings = settings
this.storage = new Storage(settings.storageSettings) this.storage = new Storage(settings.storageSettings)
this.metricsManager = new MetricsManager() this.metricsManager = new MetricsManager(this.storage)
this.lnd = NewLightningHandler(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb) this.lnd = NewLightningHandler(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb)
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb)

View file

@ -90,11 +90,13 @@ export default class {
async NewAddress(ctx: Types.UserContext, req: Types.NewAddressRequest): Promise<Types.NewAddressResponse> { async NewAddress(ctx: Types.UserContext, req: Types.NewAddressRequest): Promise<Types.NewAddressResponse> {
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const existingAddress = await this.storage.paymentStorage.GetExistingUserAddress(ctx.user_id, app)
if (existingAddress) {
return { address: existingAddress.address }
}
const res = await this.lnd.NewAddress(req.addressType) const res = await this.lnd.NewAddress(req.addressType)
const userAddress = await this.storage.paymentStorage.AddUserAddress(ctx.user_id, res.address, { linkedApplication: app }) const userAddress = await this.storage.paymentStorage.AddUserAddress(ctx.user_id, res.address, { linkedApplication: app })
return { return { address: userAddress.address }
address: userAddress.address
}
} }
async NewInvoice(userId: string, req: Types.NewInvoiceRequest, options: InboundOptionals = { expiry: defaultInvoiceExpiry }): Promise<Types.NewInvoiceResponse> { async NewInvoice(userId: string, req: Types.NewInvoiceRequest, options: InboundOptionals = { expiry: defaultInvoiceExpiry }): Promise<Types.NewInvoiceResponse> {
@ -172,7 +174,7 @@ export default class {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee) await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee)
} }
const routingFees = payment ? payment.feeSat : 0 const routingFees = payment ? payment.feeSat : 0
const newPayment = await this.storage.paymentStorage.AddUserInvoicePayment(userId, req.invoice, payAmount, routingFees, serviceFee, !!internalInvoice) const newPayment = await this.storage.paymentStorage.AddUserInvoicePayment(userId, req.invoice, payAmount, routingFees, serviceFee, !!internalInvoice, linkedApplication)
return { return {
preimage: payment ? payment.paymentPreimage : "", preimage: payment ? payment.paymentPreimage : "",
amount_paid: payment ? Number(payment.valueSat) : payAmount, amount_paid: payment ? Number(payment.valueSat) : payAmount,
@ -215,7 +217,7 @@ export default class {
await this.storage.userStorage.IncrementUserBalance(app.owner.user_id, serviceFee) await this.storage.userStorage.IncrementUserBalance(app.owner.user_id, serviceFee)
} }
const newTx = await this.storage.paymentStorage.AddUserTransactionPayment(ctx.user_id, req.address, txId, 0, req.amoutSats, chainFees, serviceFee, !!internalAddress, blockHeight) const newTx = await this.storage.paymentStorage.AddUserTransactionPayment(ctx.user_id, req.address, txId, 0, req.amoutSats, chainFees, serviceFee, !!internalAddress, blockHeight, app)
return { return {
txId: txId, txId: txId,
operation_id: `${Types.UserOperationType.OUTGOING_TX}-${newTx.serial_id}`, operation_id: `${Types.UserOperationType.OUTGOING_TX}-${newTx.serial_id}`,
@ -471,7 +473,7 @@ export default class {
const toIncrement = amount - fee const toIncrement = amount - fee
await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, amount, tx) await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, amount, tx)
await this.storage.userStorage.IncrementUserBalance(toUser.user_id, toIncrement, tx) await this.storage.userStorage.IncrementUserBalance(toUser.user_id, toIncrement, tx)
await this.storage.paymentStorage.AddUserToUserPayment(fromUserId, toUserId, amount, fee) await this.storage.paymentStorage.AddUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication)
if (isAppUserPayment && fee > 0) { if (isAppUserPayment && fee > 0) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee) await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee)
} }

View file

@ -1,7 +1,13 @@
import * as Types from '../../../proto/autogenerated/ts/types' import Storage from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { Application } from '../storage/entity/Application.js'
const maxEvents = 100_000 const maxEvents = 100_000
export default class Handler { export default class Handler {
storage: Storage
metrics: Types.UsageMetric[] = [] metrics: Types.UsageMetric[] = []
constructor(storage: Storage) {
this.storage = storage
}
AddMetrics(newMetrics: (Types.RequestMetric & { app_id?: string })[]) { AddMetrics(newMetrics: (Types.RequestMetric & { app_id?: string })[]) {
const parsed: Types.UsageMetric[] = newMetrics.map(m => ({ const parsed: Types.UsageMetric[] = newMetrics.map(m => ({
rpc_name: m.rpcName, rpc_name: m.rpcName,
@ -21,9 +27,95 @@ export default class Handler {
this.metrics.splice(0, len - maxEvents) this.metrics.splice(0, len - maxEvents)
} }
} }
async GetMetrics(): Promise<Types.UsageMetrics> { async GetUsageMetrics(): Promise<Types.UsageMetrics> {
return { return {
metrics: this.metrics metrics: this.metrics
} }
} }
async GetAppsMetrics(req: Types.AppsMetricsRequest): Promise<Types.AppsMetrics> {
const dbApps = await this.storage.applicationStorage.GetApplications()
const apps = await Promise.all(dbApps.map(app => this.GetAppMetrics(req, app)))
const unlinked = await this.GetAppMetrics(req, null)
apps.push(unlinked)
return {
apps
}
}
async GetAppMetrics(req: Types.AppsMetricsRequest, app: Application | null): Promise<Types.AppMetrics> {
const { receivingInvoices, receivingTransactions, outgoingInvoices, outgoingTransactions, receivingAddresses, userToUser } = await this.storage.paymentStorage.GetAppOperations(app, { from: req.from_unix, to: req.to_unix })
const bigUser = req.big_user_sats ? req.big_user_sats : 10_000
const hugeUser = req.big_user_sats ? req.big_user_sats : 500_000
let totalReceived = 0
let totalSpent = 0
let totalAvailable = 0
let totalBigUsers = 0
let totalHugeUsers = 0
let unpaidInvoices = 0
let paidAddresses = 0
const operations: Types.UserOperation[] = []
receivingInvoices.forEach(i => {
if (i.paid_at_unix > 0) {
totalReceived += i.paid_amount
operations.push({ type: Types.UserOperationType.INCOMING_INVOICE, amount: i.paid_amount, inbound: true, paidAtUnix: i.paid_at_unix, confirmed: true, service_fee: i.service_fee, network_fee: 0, identifier: "", operationId: "" })
} else {
unpaidInvoices++
}
})
receivingTransactions.forEach(txs => {
if (txs.length > 0) {
paidAddresses++
} else {
txs.forEach(tx => {
operations.push({ type: Types.UserOperationType.INCOMING_TX, amount: tx.paid_amount, inbound: true, paidAtUnix: tx.paid_at_unix, confirmed: tx.confs > 1, service_fee: tx.service_fee, network_fee: 0, identifier: "", operationId: "" })
if (tx.confs > 1) {
totalReceived += tx.paid_amount
}
})
}
})
outgoingInvoices.forEach(i => {
operations.push({ type: Types.UserOperationType.OUTGOING_INVOICE, amount: i.paid_amount, inbound: false, paidAtUnix: i.paid_at_unix, confirmed: true, service_fee: i.service_fees, network_fee: i.routing_fees, identifier: "", operationId: "" })
totalSpent += i.paid_amount
})
outgoingTransactions.forEach(tx => {
operations.push({ type: Types.UserOperationType.OUTGOING_TX, amount: tx.paid_amount, inbound: false, paidAtUnix: tx.paid_at_unix, confirmed: tx.confs > 1, service_fee: tx.service_fees, network_fee: tx.chain_fees, identifier: "", operationId: "" })
totalSpent += tx.paid_amount
})
userToUser.forEach(op => {
operations.push({ type: Types.UserOperationType.INCOMING_USER_TO_USER, amount: op.paid_amount, inbound: true, paidAtUnix: op.paid_at_unix, confirmed: true, service_fee: op.service_fees, network_fee: 0, identifier: "", operationId: "" })
})
const users = await this.storage.applicationStorage.GetApplicationUsers(app, { from: req.from_unix, to: req.to_unix })
users.forEach(u => {
totalAvailable += u.user.balance_sats
if (u.user.balance_sats > bigUser) {
totalBigUsers++
}
if (u.user.balance_sats > hugeUser) {
totalHugeUsers++
}
})
return {
app_name: app ? app.name : "unlinked to app",
app_id: app ? app.app_id : "unlinked",
app_npub: app ? (app.nostr_public_key || "") : "",
app_balance: app ? app.owner.balance_sats : 0,
total_received: totalReceived,
total_spent: totalSpent,
total_available: totalAvailable,
total_users: users.length,
total_big_users: totalBigUsers,
total_huge_users: totalHugeUsers,
unpaid_invoices: unpaidInvoices,
unpaid_addresses: receivingAddresses.length - paidAddresses,
operations
}
}
} }

View file

@ -4,8 +4,11 @@ import main from '../main/index.js'
import Main from '../main/index.js' import Main from '../main/index.js'
export default (mainHandler: Main): Types.ServerMethods => { export default (mainHandler: Main): Types.ServerMethods => {
return { return {
GetMetrics: async ({ ctx }) => { GetUsageMetrics: async ({ ctx }) => {
return mainHandler.metricsManager.GetMetrics() return mainHandler.metricsManager.GetUsageMetrics()
},
GetAppsMetrics: async ({ ctx, req }) => {
return mainHandler.metricsManager.GetAppsMetrics(req)
}, },
EncryptionExchange: async () => { }, EncryptionExchange: async () => { },
Health: async () => { await mainHandler.lnd.Health() }, Health: async () => { await mainHandler.lnd.Health() },

View file

@ -1,5 +1,5 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { DataSource, EntityManager } from "typeorm" import { Between, DataSource, EntityManager, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm"
import { generatePrivateKey, getPublicKey } from 'nostr-tools'; import { generatePrivateKey, getPublicKey } from 'nostr-tools';
import { Application } from "./entity/Application.js" import { Application } from "./entity/Application.js"
import UserStorage from './userStorage.js'; import UserStorage from './userStorage.js';
@ -123,6 +123,19 @@ export default class {
return found return found
} }
async GetApplicationUsers(application: Application | null, { from, to }: { from?: number, to?: number }, entityManager = this.DB) {
const q = application ? { app_id: application.app_id } : IsNull()
let time: { created_at?: FindOperator<Date> } = {}
if (!!from && !!to) {
time.created_at = Between<Date>(new Date(from * 1000), new Date(to * 1000))
} else if (!!from) {
time.created_at = MoreThanOrEqual<Date>(new Date(from * 1000))
} else if (!!to) {
time.created_at = LessThanOrEqual<Date>(new Date(to * 1000))
}
return entityManager.getRepository(ApplicationUser).find({ where: { application: q, ...time } })
}
async GetAppUserFromUser(application: Application, userId: string, entityManager = this.DB): Promise<ApplicationUser | null> { async GetAppUserFromUser(application: Application, userId: string, entityManager = this.DB): Promise<ApplicationUser | null> {
return await entityManager.getRepository(ApplicationUser).findOne({ where: { user: { user_id: userId }, application: { app_id: application.app_id } } }) return await entityManager.getRepository(ApplicationUser).findOne({ where: { user: { user_id: userId }, application: { app_id: application.app_id } } })
} }

View file

@ -1,5 +1,6 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"
import { User } from "./User.js" import { User } from "./User.js"
import { Application } from "./Application.js"
@Entity() @Entity()
export class UserInvoicePayment { export class UserInvoicePayment {
@ -30,6 +31,9 @@ export class UserInvoicePayment {
@Column({ default: false }) @Column({ default: false })
internal: boolean internal: boolean
@ManyToOne(type => Application, { eager: true })
linkedApplication: Application | null
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -1,5 +1,6 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"
import { User } from "./User.js" import { User } from "./User.js"
import { Application } from "./Application.js"
@Entity() @Entity()
export class UserToUserPayment { export class UserToUserPayment {
@ -24,6 +25,9 @@ export class UserToUserPayment {
@Column() @Column()
paid_at_unix: number paid_at_unix: number
@ManyToOne(type => Application, { eager: true })
linkedApplication: Application | null
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -1,5 +1,6 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"
import { User } from "./User.js" import { User } from "./User.js"
import { Application } from "./Application.js"
@Entity() @Entity()
@Index("user_transaction_unique", ["tx_hash", "output_index"], { unique: true }) @Index("user_transaction_unique", ["tx_hash", "output_index"], { unique: true })
@ -42,6 +43,9 @@ export class UserTransactionPayment {
@Column({ default: 0 }) @Column({ default: 0 })
broadcast_height: number broadcast_height: number
@ManyToOne(type => Application, { eager: true })
linkedApplication: Application | null
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -1,5 +1,5 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { DataSource, EntityManager, MoreThan, MoreThanOrEqual } from "typeorm" import { Between, DataSource, EntityManager, FindOperator, IsNull, LessThanOrEqual, MoreThan, MoreThanOrEqual } from "typeorm"
import { User } from './entity/User.js'; import { User } from './entity/User.js';
import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js';
import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js';
@ -48,7 +48,9 @@ export default class {
}) })
} }
async GetExistingUserAddress(userId: string, linkedApplication: Application, entityManager = this.DB) {
return entityManager.getRepository(UserReceivingAddress).findOne({ where: { user: { user_id: userId }, linkedApplication: { app_id: linkedApplication.app_id } } })
}
async AddUserAddress(userId: string, address: string, opts: { callbackUrl?: string, linkedApplication?: Application } = {}, entityManager = this.DB): Promise<UserReceivingAddress> { async AddUserAddress(userId: string, address: string, opts: { callbackUrl?: string, linkedApplication?: Application } = {}, entityManager = this.DB): Promise<UserReceivingAddress> {
const newUserAddress = entityManager.getRepository(UserReceivingAddress).create({ const newUserAddress = entityManager.getRepository(UserReceivingAddress).create({
@ -113,7 +115,7 @@ export default class {
}) })
} }
async AddUserInvoicePayment(userId: string, invoice: string, amount: number, routingFees: number, serviceFees: number, internal: boolean, entityManager = this.DB): Promise<UserInvoicePayment> { async AddUserInvoicePayment(userId: string, invoice: string, amount: number, routingFees: number, serviceFees: number, internal: boolean, linkedApplication: Application, entityManager = this.DB): Promise<UserInvoicePayment> {
const newPayment = entityManager.getRepository(UserInvoicePayment).create({ const newPayment = entityManager.getRepository(UserInvoicePayment).create({
user: await this.userStorage.GetUser(userId), user: await this.userStorage.GetUser(userId),
paid_amount: amount, paid_amount: amount,
@ -121,7 +123,8 @@ export default class {
routing_fees: routingFees, routing_fees: routingFees,
service_fees: serviceFees, service_fees: serviceFees,
paid_at_unix: Math.floor(Date.now() / 1000), paid_at_unix: Math.floor(Date.now() / 1000),
internal internal,
linkedApplication
}) })
return entityManager.getRepository(UserInvoicePayment).save(newPayment) return entityManager.getRepository(UserInvoicePayment).save(newPayment)
} }
@ -141,7 +144,7 @@ export default class {
}) })
} }
async AddUserTransactionPayment(userId: string, address: string, txHash: string, txOutput: number, amount: number, chainFees: number, serviceFees: number, internal: boolean, height: number, entityManager = this.DB): Promise<UserTransactionPayment> { async AddUserTransactionPayment(userId: string, address: string, txHash: string, txOutput: number, amount: number, chainFees: number, serviceFees: number, internal: boolean, height: number, linkedApplication: Application, entityManager = this.DB): Promise<UserTransactionPayment> {
const newTx = entityManager.getRepository(UserTransactionPayment).create({ const newTx = entityManager.getRepository(UserTransactionPayment).create({
user: await this.userStorage.GetUser(userId), user: await this.userStorage.GetUser(userId),
address, address,
@ -153,7 +156,8 @@ export default class {
paid_at_unix: Math.floor(Date.now() / 1000), paid_at_unix: Math.floor(Date.now() / 1000),
internal, internal,
broadcast_height: height, broadcast_height: height,
confs: internal ? 10 : 0 confs: internal ? 10 : 0,
linkedApplication
}) })
return entityManager.getRepository(UserTransactionPayment).save(newTx) return entityManager.getRepository(UserTransactionPayment).save(newTx)
} }
@ -217,13 +221,14 @@ export default class {
return found return found
} }
async AddUserToUserPayment(fromUserId: string, toUserId: string, amount: number, fee: number, entityManager = this.DB) { async AddUserToUserPayment(fromUserId: string, toUserId: string, amount: number, fee: number, linkedApplication: Application, entityManager = this.DB) {
const newKey = entityManager.getRepository(UserToUserPayment).create({ const newKey = entityManager.getRepository(UserToUserPayment).create({
from_user: await this.userStorage.GetUser(fromUserId, entityManager), from_user: await this.userStorage.GetUser(fromUserId, entityManager),
to_user: await this.userStorage.GetUser(toUserId, entityManager), to_user: await this.userStorage.GetUser(toUserId, entityManager),
paid_at_unix: Math.floor(Date.now() / 1000), paid_at_unix: Math.floor(Date.now() / 1000),
paid_amount: amount, paid_amount: amount,
service_fees: fee service_fees: fee,
linkedApplication
}) })
return entityManager.getRepository(UserToUserPayment).save(newKey) return entityManager.getRepository(UserToUserPayment).save(newKey)
} }
@ -258,4 +263,30 @@ export default class {
}) })
} }
async GetAppOperations(application: Application | null, { from, to }: { from?: number, to?: number }, entityManager = this.DB) {
const q = application ? { app_id: application.app_id } : IsNull()
let time: { created_at?: FindOperator<Date> } = {}
if (!!from && !!to) {
time.created_at = Between<Date>(new Date(from * 1000), new Date(to * 1000))
} else if (!!from) {
time.created_at = MoreThanOrEqual<Date>(new Date(from * 1000))
} else if (!!to) {
time.created_at = LessThanOrEqual<Date>(new Date(to * 1000))
}
const [receivingInvoices, receivingAddresses, outgoingInvoices, outgoingTransactions, userToUser] = await Promise.all([
entityManager.getRepository(UserReceivingInvoice).find({ where: { linkedApplication: q, ...time } }),
entityManager.getRepository(UserReceivingAddress).find({ where: { linkedApplication: q, ...time } }),
entityManager.getRepository(UserInvoicePayment).find({ where: { linkedApplication: q, ...time } }),
entityManager.getRepository(UserTransactionPayment).find({ where: { linkedApplication: q, ...time } }),
entityManager.getRepository(UserToUserPayment).find({ where: { linkedApplication: q, ...time } })
])
const receivingTransactions = await Promise.all(receivingAddresses.map(addr => entityManager.getRepository(AddressReceivingTransaction).find({ where: { user_address: { serial_id: addr.serial_id } } })))
return {
receivingInvoices, receivingAddresses, receivingTransactions,
outgoingInvoices, outgoingTransactions,
userToUser
}
}
} }