Merge branch 'master' into npub-linking-through-expirable-tokens

This commit is contained in:
Mothana 2024-03-30 13:54:56 +04:00
commit 0963f94989
23 changed files with 321 additions and 4551 deletions

View file

@ -44,3 +44,9 @@ MIGRATE_DB=false
RECORD_PERFORMANCE=true RECORD_PERFORMANCE=true
SKIP_SANITY_CHECK=false SKIP_SANITY_CHECK=false
DISABLE_EXTERNAL_PAYMENTS=false DISABLE_EXTERNAL_PAYMENTS=false
# Max difference between users balance and LND balance since beginning of app execution
WATCHDOG_MAX_DIFF_SATS=10000
# Max difference between users balance and LND balance after each payment
WATCHDOG_MAX_UPDATE_DIFF_SATS=1000

View file

@ -1,825 +0,0 @@
# NOSTR API DEFINITION
A nostr request will take the same parameter and give the same response as an http request, but it will use nostr as transport, to do that it will send encrypted events to the server public key, in the event 6 thing are required:
- __rpcName__: string containing the name of the method
- __params__: a map with the all the url params for the method
- __query__: a map with the the url query for the method
- __body__: the body of the method request
- __requestId__: id of the request to be able to get a response
The nostr server will send back a message response, and inside the body there will also be a __requestId__ to identify the request this response is answering
## NOSTR Methods
### These are the nostr methods the client implements to communicate with the API via nostr
- LinkNPubThroughToken
- auth type: __User__
- input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest)
- This methods has an __empty__ __response__ body
- UserHealth
- auth type: __User__
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- GetUserInfo
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [UserInfo](#UserInfo)
- AddProduct
- auth type: __User__
- input: [AddProductRequest](#AddProductRequest)
- output: [Product](#Product)
- NewProductInvoice
- auth type: __User__
- the request url __query__ can take the following string items:
- id
- This methods has an __empty__ __request__ body
- output: [NewInvoiceResponse](#NewInvoiceResponse)
- GetUserOperations
- auth type: __User__
- input: [GetUserOperationsRequest](#GetUserOperationsRequest)
- output: [GetUserOperationsResponse](#GetUserOperationsResponse)
- NewAddress
- auth type: __User__
- input: [NewAddressRequest](#NewAddressRequest)
- output: [NewAddressResponse](#NewAddressResponse)
- PayAddress
- auth type: __User__
- input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse)
- NewInvoice
- auth type: __User__
- input: [NewInvoiceRequest](#NewInvoiceRequest)
- output: [NewInvoiceResponse](#NewInvoiceResponse)
- DecodeInvoice
- auth type: __User__
- input: [DecodeInvoiceRequest](#DecodeInvoiceRequest)
- output: [DecodeInvoiceResponse](#DecodeInvoiceResponse)
- PayInvoice
- auth type: __User__
- input: [PayInvoiceRequest](#PayInvoiceRequest)
- output: [PayInvoiceResponse](#PayInvoiceResponse)
- OpenChannel
- auth type: __User__
- input: [OpenChannelRequest](#OpenChannelRequest)
- output: [OpenChannelResponse](#OpenChannelResponse)
- GetLnurlWithdrawLink
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [LnurlLinkResponse](#LnurlLinkResponse)
- GetLnurlPayLink
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [LnurlLinkResponse](#LnurlLinkResponse)
- GetLNURLChannelLink
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [LnurlLinkResponse](#LnurlLinkResponse)
- GetLiveUserOperations
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [LiveUserOperation](#LiveUserOperation)
- GetMigrationUpdate
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [MigrationUpdate](#MigrationUpdate)
- BatchUser
- auth type: __User__
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
# HTTP API DEFINITION
## Supported HTTP Auths
### These are the supported http auth types, to give different type of access to the API users
- __Guest__:
- expected context content
- __User__:
- expected context content
- __user_id__: _string_
- __app_id__: _string_
- __app_user_id__: _string_
- __Admin__:
- expected context content
- __admin_id__: _string_
- __Metrics__:
- expected context content
- __operator_id__: _string_
- __App__:
- expected context content
- __app_id__: _string_
## HTTP Methods
### These are the http methods the client implements to communicate with the API
- LndGetInfo
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/lnd/getinfo__
- input: [LndGetInfoRequest](#LndGetInfoRequest)
- output: [LndGetInfoResponse](#LndGetInfoResponse)
- AddApp
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/app/add__
- input: [AddAppRequest](#AddAppRequest)
- output: [AuthApp](#AuthApp)
- AuthApp
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/app/auth__
- input: [AuthAppRequest](#AuthAppRequest)
- output: [AuthApp](#AuthApp)
- GetUsageMetrics
- auth type: __Metrics__
- http method: __post__
- http route: __/api/reports/usage__
- This methods has an __empty__ __request__ body
- output: [UsageMetrics](#UsageMetrics)
- GetAppsMetrics
- auth type: __Metrics__
- http method: __post__
- http route: __/api/reports/apps__
- input: [AppsMetricsRequest](#AppsMetricsRequest)
- output: [AppsMetrics](#AppsMetrics)
- GetLndMetrics
- auth type: __Metrics__
- http method: __post__
- http route: __/api/reports/lnd__
- input: [LndMetricsRequest](#LndMetricsRequest)
- output: [LndMetrics](#LndMetrics)
- Health
- auth type: __Guest__
- http method: __get__
- http route: __/api/health__
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- EncryptionExchange
- auth type: __Guest__
- http method: __post__
- http route: __/api/encryption/exchange__
- input: [EncryptionExchangeRequest](#EncryptionExchangeRequest)
- This methods has an __empty__ __response__ body
- SetMockInvoiceAsPaid
- auth type: __Guest__
- http method: __post__
- http route: __/api/lnd/mock/invoice/paid__
- input: [SetMockInvoiceAsPaidRequest](#SetMockInvoiceAsPaidRequest)
- This methods has an __empty__ __response__ body
- GetLnurlWithdrawInfo
- auth type: __Guest__
- http method: __get__
- http route: __/api/guest/lnurl_withdraw/info__
- the request url __query__ can take the following string items:
- k1
- This methods has an __empty__ __request__ body
- output: [LnurlWithdrawInfoResponse](#LnurlWithdrawInfoResponse)
- HandleLnurlWithdraw
- auth type: __Guest__
- http method: __get__
- http route: __/api/guest/lnurl_withdraw/handle__
- the request url __query__ can take the following string items:
- k1
- pr
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- GetLnurlPayInfo
- auth type: __Guest__
- http method: __get__
- http route: __/api/guest/lnurl_pay/info__
- the request url __query__ can take the following string items:
- k1
- This methods has an __empty__ __request__ body
- output: [LnurlPayInfoResponse](#LnurlPayInfoResponse)
- HandleLnurlPay
- auth type: __Guest__
- http method: __get__
- http route: __/api/guest/lnurl_pay/handle__
- the request url __query__ can take the following string items:
- k1
- amount
- nostr
- lnurl
- This methods has an __empty__ __request__ body
- output: [HandleLnurlPayResponse](#HandleLnurlPayResponse)
- HandleLnurlAddress
- auth type: __Guest__
- http method: __get__
- http route: __/.well-known/lnurlp/:address_name__
- the request url __params__ are the following string items:
- address_name
- This methods has an __empty__ __request__ body
- output: [LnurlPayInfoResponse](#LnurlPayInfoResponse)
- LinkNPubThroughToken
- auth type: __User__
- http method: __post__
- http route: __/api/guest/npub/link__
- input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest)
- This methods has an __empty__ __response__ body
- GetApp
- auth type: __App__
- http method: __post__
- http route: __/api/app/get__
- This methods has an __empty__ __request__ body
- output: [Application](#Application)
- AddAppUser
- auth type: __App__
- http method: __post__
- http route: __/api/app/user/add__
- input: [AddAppUserRequest](#AddAppUserRequest)
- output: [AppUser](#AppUser)
- AddAppInvoice
- auth type: __App__
- http method: __post__
- http route: __/api/app/add/invoice__
- input: [AddAppInvoiceRequest](#AddAppInvoiceRequest)
- output: [NewInvoiceResponse](#NewInvoiceResponse)
- AddAppUserInvoice
- auth type: __App__
- http method: __post__
- http route: __/api/app/user/add/invoice__
- input: [AddAppUserInvoiceRequest](#AddAppUserInvoiceRequest)
- output: [NewInvoiceResponse](#NewInvoiceResponse)
- GetAppUser
- auth type: __App__
- http method: __post__
- http route: __/api/app/user/get__
- input: [GetAppUserRequest](#GetAppUserRequest)
- output: [AppUser](#AppUser)
- PayAppUserInvoice
- auth type: __App__
- http method: __post__
- http route: __/api/app/invoice/pay__
- input: [PayAppUserInvoiceRequest](#PayAppUserInvoiceRequest)
- output: [PayInvoiceResponse](#PayInvoiceResponse)
- SendAppUserToAppUserPayment
- auth type: __App__
- http method: __post__
- http route: __/api/app/user/internal/pay__
- input: [SendAppUserToAppUserPaymentRequest](#SendAppUserToAppUserPaymentRequest)
- This methods has an __empty__ __response__ body
- SendAppUserToAppPayment
- auth type: __App__
- http method: __post__
- http route: __/api/app/internal/pay__
- input: [SendAppUserToAppPaymentRequest](#SendAppUserToAppPaymentRequest)
- This methods has an __empty__ __response__ body
- GetAppUserLNURLInfo
- auth type: __App__
- http method: __post__
- http route: __/api/app/user/lnurl/pay/info__
- input: [GetAppUserLNURLInfoRequest](#GetAppUserLNURLInfoRequest)
- output: [LnurlPayInfoResponse](#LnurlPayInfoResponse)
- SetMockAppUserBalance
- auth type: __App__
- http method: __post__
- http route: __/api/app/mock/user/blance/set__
- input: [SetMockAppUserBalanceRequest](#SetMockAppUserBalanceRequest)
- This methods has an __empty__ __response__ body
- SetMockAppBalance
- auth type: __App__
- http method: __post__
- http route: __/api/app/mock/blance/set__
- input: [SetMockAppBalanceRequest](#SetMockAppBalanceRequest)
- This methods has an __empty__ __response__ body
- RequestNPubLinkingToken
- auth type: __App__
- http method: __post__
- http route: __/api/app/user/npub/token__
- input: [RequestNPubLinkingTokenRequest](#RequestNPubLinkingTokenRequest)
- output: [RequestNPubLinkingTokenResponse](#RequestNPubLinkingTokenResponse)
- UserHealth
- auth type: __User__
- http method: __post__
- http route: __/api/user/health__
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- GetUserInfo
- auth type: __User__
- http method: __post__
- http route: __/api/user/info__
- This methods has an __empty__ __request__ body
- output: [UserInfo](#UserInfo)
- AddProduct
- auth type: __User__
- http method: __post__
- http route: __/api/user/product/add__
- input: [AddProductRequest](#AddProductRequest)
- output: [Product](#Product)
- NewProductInvoice
- auth type: __User__
- http method: __get__
- http route: __/api/user/product/get/invoice__
- the request url __query__ can take the following string items:
- id
- This methods has an __empty__ __request__ body
- output: [NewInvoiceResponse](#NewInvoiceResponse)
- GetUserOperations
- auth type: __User__
- http method: __post__
- http route: __/api/user/operations__
- input: [GetUserOperationsRequest](#GetUserOperationsRequest)
- output: [GetUserOperationsResponse](#GetUserOperationsResponse)
- NewAddress
- auth type: __User__
- http method: __post__
- http route: __/api/user/chain/new__
- input: [NewAddressRequest](#NewAddressRequest)
- output: [NewAddressResponse](#NewAddressResponse)
- PayAddress
- auth type: __User__
- http method: __post__
- http route: __/api/user/chain/pay__
- input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse)
- NewInvoice
- auth type: __User__
- http method: __post__
- http route: __/api/user/invoice/new__
- input: [NewInvoiceRequest](#NewInvoiceRequest)
- output: [NewInvoiceResponse](#NewInvoiceResponse)
- DecodeInvoice
- auth type: __User__
- http method: __post__
- http route: __/api/user/invoice/decode__
- input: [DecodeInvoiceRequest](#DecodeInvoiceRequest)
- output: [DecodeInvoiceResponse](#DecodeInvoiceResponse)
- PayInvoice
- auth type: __User__
- http method: __post__
- http route: __/api/user/invoice/pay__
- input: [PayInvoiceRequest](#PayInvoiceRequest)
- output: [PayInvoiceResponse](#PayInvoiceResponse)
- OpenChannel
- auth type: __User__
- http method: __post__
- http route: __/api/user/open/channel__
- input: [OpenChannelRequest](#OpenChannelRequest)
- output: [OpenChannelResponse](#OpenChannelResponse)
- GetLnurlWithdrawLink
- auth type: __User__
- http method: __get__
- http route: __/api/user/lnurl_withdraw/link__
- This methods has an __empty__ __request__ body
- output: [LnurlLinkResponse](#LnurlLinkResponse)
- GetLnurlPayLink
- auth type: __User__
- http method: __get__
- http route: __/api/user/lnurl_pay/link__
- This methods has an __empty__ __request__ body
- output: [LnurlLinkResponse](#LnurlLinkResponse)
- GetLNURLChannelLink
- auth type: __User__
- http method: __post__
- http route: __/api/user/lnurl_channel/url__
- This methods has an __empty__ __request__ body
- output: [LnurlLinkResponse](#LnurlLinkResponse)
- GetLiveUserOperations
- auth type: __User__
- http method: __post__
- http route: __/api/user/operations/sub__
- This methods has an __empty__ __request__ body
- output: [LiveUserOperation](#LiveUserOperation)
- GetMigrationUpdate
- auth type: __User__
- http method: __post__
- http route: __/api/user/migrations/sub__
- This methods has an __empty__ __request__ body
- output: [MigrationUpdate](#MigrationUpdate)
- BatchUser
- auth type: __User__
- http method: __post__
- http route: __/api/user/batch__
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
# INPUTS AND OUTPUTS
## Messages
### The content of requests and response from the methods
### Application
- __name__: _string_
- __id__: _string_
- __balance__: _number_
- __npub__: _string_
### ClosureMigration
- __closes_at_unix__: _number_
### LinkNPubThroughTokenRequest
- __token__: _string_
- __nostr_pub__: _string_
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
### UsersInfo
- __total__: _number_
- __no_balance__: _number_
- __negative_balance__: _number_
- __always_been_inactive__: _number_
- __balance_avg__: _number_
- __balance_median__: _number_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### MigrationUpdate
- __closure__: _[ClosureMigration](#ClosureMigration)_ *this field is optional
- __relays__: _[RelaysMigration](#RelaysMigration)_ *this field is optional
### AppMetrics
- __app__: _[Application](#Application)_
- __users__: _[UsersInfo](#UsersInfo)_
- __received__: _number_
- __spent__: _number_
- __available__: _number_
- __fees__: _number_
- __invoices__: _number_
- __total_fees__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### NewInvoiceResponse
- __invoice__: _string_
### LndMetrics
- __nodes__: ARRAY of: _[LndNodeMetrics](#LndNodeMetrics)_
### AddProductRequest
- __name__: _string_
- __price_sats__: _number_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __satsPerVByte__: _number_
### UserOperation
- __paidAtUnix__: _number_
- __type__: _[UserOperationType](#UserOperationType)_
- __inbound__: _boolean_
- __amount__: _number_
- __identifier__: _string_
- __operationId__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
- __confirmed__: _boolean_
- __tx_hash__: _string_
- __internal__: _boolean_
### GetUserOperationsResponse
- __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingTxOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingTxOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
- __latestIncomingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
### AuthAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_ *this field is optional
### AddAppUserRequest
- __identifier__: _string_
- __fail_if_exists__: _boolean_
- __balance__: _number_
### SetMockAppBalanceRequest
- __amount__: _number_
### DecodeInvoiceResponse
- __amount__: _number_
### UserOperations
- __fromIndex__: _number_
- __toIndex__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### RelaysMigration
- __relays__: ARRAY of: _string_
### LndNodeMetrics
- __channels_balance_events__: ARRAY of: _[ChannelBalanceEvent](#ChannelBalanceEvent)_
- __chain_balance_events__: ARRAY of: _[ChainBalanceEvent](#ChainBalanceEvent)_
- __offline_channels__: _number_
- __online_channels__: _number_
- __pending_channels__: _number_
- __closing_channels__: _number_
- __open_channels__: ARRAY of: _[OpenChannel](#OpenChannel)_
- __closed_channels__: ARRAY of: _[ClosedChannel](#ClosedChannel)_
- __channel_routing__: ARRAY of: _[ChannelRouting](#ChannelRouting)_
### AddAppUserInvoiceRequest
- __receiver_identifier__: _string_
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### ClosedChannel
- __channel_id__: _string_
- __capacity__: _number_
- __closed_height__: _number_
### AddAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### UsageMetrics
- __metrics__: ARRAY of: _[UsageMetric](#UsageMetric)_
### ChainBalanceEvent
- __block_height__: _number_
- __confirmed_balance__: _number_
- __unconfirmed_balance__: _number_
- __total_balance__: _number_
### Product
- __id__: _string_
- __name__: _string_
- __price_sats__: _number_
### ChannelBalanceEvent
- __block_height__: _number_
- __channel_id__: _string_
- __local_balance_sats__: _number_
- __remote_balance_sats__: _number_
### AuthApp
- __app__: _[Application](#Application)_
- __auth_token__: _string_
### AppsMetrics
- __apps__: ARRAY of: _[AppMetrics](#AppMetrics)_
### RoutingEvent
- __incoming_channel_id__: _number_
- __incoming_htlc_id__: _number_
- __outgoing_channel_id__: _number_
- __outgoing_htlc_id__: _number_
- __timestamp_ns__: _number_
- __event_type__: _string_
- __incoming_amt_msat__: _number_
- __outgoing_amt_msat__: _number_
- __failure_string__: _string_
- __settled__: _boolean_
- __offchain__: _boolean_
- __forward_fail_event__: _boolean_
### AddAppInvoiceRequest
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### LnurlWithdrawInfoResponse
- __tag__: _string_
- __callback__: _string_
- __k1__: _string_
- __defaultDescription__: _string_
- __minWithdrawable__: _number_
- __maxWithdrawable__: _number_
- __balanceCheck__: _string_
- __payLink__: _string_
### LndGetInfoRequest
- __nodeId__: _number_
### AppUser
- __identifier__: _string_
- __info__: _[UserInfo](#UserInfo)_
- __max_withdrawable__: _number_
### PayAppUserInvoiceRequest
- __user_identifier__: _string_
- __invoice__: _string_
- __amount__: _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_
### GetProductBuyLinkResponse
- __link__: _string_
### Empty
### ChannelRouting
- __channel_id__: _string_
- __send_errors__: _number_
- __receive_errors__: _number_
- __forward_errors_as_input__: _number_
- __forward_errors_as_output__: _number_
- __missed_forward_fee_as_input__: _number_
- __missed_forward_fee_as_output__: _number_
- __forward_fee_as_input__: _number_
- __forward_fee_as_output__: _number_
- __events_number__: _number_
### OpenChannel
- __channel_id__: _string_
- __capacity__: _number_
- __active__: _boolean_
- __lifetime__: _number_
- __local_balance__: _number_
- __remote_balance__: _number_
### RequestNPubLinkingTokenResponse
- __token__: _string_
### SendAppUserToAppPaymentRequest
- __from_user_identifier__: _string_
- __amount__: _number_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
### DecodeInvoiceRequest
- __invoice__: _string_
### GetUserOperationsRequest
- __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
- __latestIncomingUserToUserPayment__: _number_
- __latestOutgoingUserToUserPayment__: _number_
- __max_size__: _number_
### LiveUserOperation
- __operation__: _[UserOperation](#UserOperation)_
### UsageMetric
- __processed_at_ms__: _number_
- __parsed_in_nano__: _number_
- __auth_in_nano__: _number_
- __validate_in_nano__: _number_
- __handle_in_nano__: _number_
- __rpc_name__: _string_
- __batch__: _boolean_
- __nostr__: _boolean_
- __batch_size__: _number_
### LndMetricsRequest
- __from_unix__: _number_ *this field is optional
- __to_unix__: _number_ *this field is optional
### GetAppUserRequest
- __user_identifier__: _string_
### SendAppUserToAppUserPaymentRequest
- __from_user_identifier__: _string_
- __to_user_identifier__: _string_
- __amount__: _number_
### PayAddressResponse
- __txId__: _string_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### OpenChannelResponse
- __channelId__: _string_
### RequestNPubLinkingTokenRequest
- __user_identifier__: _string_
### SetMockInvoiceAsPaidRequest
- __invoice__: _string_
- __amount__: _number_
### LndGetInfoResponse
- __alias__: _string_
### UserInfo
- __userId__: _string_
- __balance__: _number_
- __max_withdrawable__: _number_
- __user_identifier__: _string_
### AppsMetricsRequest
- __from_unix__: _number_ *this field is optional
- __to_unix__: _number_ *this field is optional
- __include_operations__: _boolean_ *this field is optional
### GetAppUserLNURLInfoRequest
- __user_identifier__: _string_
- __base_url_override__: _string_
### PayInvoiceRequest
- __invoice__: _string_
- __amount__: _number_
### PayInvoiceResponse
- __preimage__: _string_
- __amount_paid__: _number_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### SetMockAppUserBalanceRequest
- __user_identifier__: _string_
- __amount__: _number_
### NewAddressResponse
- __address__: _string_
### NewAddressRequest
- __addressType__: _[AddressType](#AddressType)_
## Enums
### The enumerators used in the messages
### AddressType
- __WITNESS_PUBKEY_HASH__
- __NESTED_PUBKEY_HASH__
- __TAPROOT_PUBKEY__
### UserOperationType
- __INCOMING_TX__
- __OUTGOING_TX__
- __INCOMING_INVOICE__
- __OUTGOING_INVOICE__
- __OUTGOING_USER_TO_USER__
- __INCOMING_USER_TO_USER__

File diff suppressed because it is too large Load diff

View file

@ -99,6 +99,28 @@ 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.BanUser) throw new Error('method: BanUser is not implemented')
app.post('/api/admin/user/ban', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'BanUser', 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.BanUser) throw new Error('method: BanUser 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.BanUserRequestValidate(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.BanUser({rpcName:'BanUser', 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.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented') if (!opts.allowNotImplementedMethods && !methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented')
app.post('/api/reports/usage', async (req, res) => { app.post('/api/reports/usage', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetUsageMetrics', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'GetUsageMetrics', batch: false, nostr: false, batchSize: 0}

View file

@ -58,6 +58,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
BanUser: async (request: Types.BanUserRequest): Promise<ResultError | ({ status: 'OK' }& Types.BanUserResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/user/ban'
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.BanUserResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetUsageMetrics: async (): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => { GetUsageMetrics: async (): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => {
const auth = await params.retrieveMetricsAuth() const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null') if (auth === null) throw new Error('retrieveMetricsAuth() returned null')

View file

@ -106,6 +106,12 @@ service LightningPub {
option (http_route) = "/api/admin/app/auth"; option (http_route) = "/api/admin/app/auth";
} }
rpc BanUser(structs.BanUserRequest) returns (structs.BanUserResponse) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/user/ban";
}
rpc GetUsageMetrics(structs.Empty) returns (structs.UsageMetrics) { rpc GetUsageMetrics(structs.Empty) returns (structs.UsageMetrics) {
option (auth_type) = "Metrics"; option (auth_type) = "Metrics";
option (http_method) = "post"; option (http_method) = "post";
@ -123,6 +129,8 @@ service LightningPub {
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/reports/lnd"; option (http_route) = "/api/reports/lnd";
} }
// </Admin> // </Admin>
// <Guest> // <Guest>

View file

@ -155,6 +155,21 @@ message LndGetInfoResponse {
string alias = 1; string alias = 1;
} }
message BanUserRequest {
string user_id = 1;
}
message BannedAppUser {
string app_name = 1;
string app_id = 2;
string user_identifier = 3;
string nostr_pub = 4;
}
message BanUserResponse {
int64 balance_sats = 1;
repeated BannedAppUser banned_app_users = 2;
}
message AddAppRequest { message AddAppRequest {
string name = 1; string name = 1;
bool allow_user_creation = 2; bool allow_user_creation = 2;

View file

@ -2,10 +2,8 @@ import 'dotenv/config'
import NewServer from '../proto/autogenerated/ts/express_server.js' import NewServer from '../proto/autogenerated/ts/express_server.js'
import GetServerMethods from './services/serverMethods/index.js' import GetServerMethods from './services/serverMethods/index.js'
import serverOptions from './auth.js'; import serverOptions from './auth.js';
import Storage from './services/storage/index.js'
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js' import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js'
import nostrMiddleware from './nostrMiddleware.js' import nostrMiddleware from './nostrMiddleware.js'
import { TypeOrmMigrationRunner } from './services/storage/migrations/runner.js';
import { getLogger } from './services/helpers/logger.js'; import { getLogger } from './services/helpers/logger.js';
import { initMainHandler } from './services/main/init.js'; import { initMainHandler } from './services/main/init.js';
import { LoadMainSettingsFromEnv } from './services/main/settings.js'; import { LoadMainSettingsFromEnv } from './services/main/settings.js';

View file

@ -10,7 +10,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
const nostrTransport = NewNostrTransport(serverMethods, { const nostrTransport = NewNostrTransport(serverMethods, {
NostrUserAuthGuard: async (appId, pub) => { NostrUserAuthGuard: async (appId, pub) => {
const app = await mainHandler.storage.applicationStorage.GetApplication(appId || "") const app = await mainHandler.storage.applicationStorage.GetApplication(appId || "")
let nostrUser = await mainHandler.storage.applicationStorage.GetOrCreateNostrAppUser(app, pub || "") const nostrUser = await mainHandler.storage.applicationStorage.GetOrCreateNostrAppUser(app, pub || "")
return { user_id: nostrUser.user.user_id, app_user_id: nostrUser.identifier, app_id: appId || "" } return { user_id: nostrUser.user.user_id, app_user_id: nostrUser.identifier, app_id: appId || "" }
}, },
metricsCallback: metrics => mainHandler.settings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null, metricsCallback: metrics => mainHandler.settings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null,

View file

@ -10,6 +10,17 @@ export const EnvMustBeInteger = (name: string): number => {
} }
return +env return +env
} }
export const EnvCanBeInteger = (name: string, defaultValue = 0): number => {
const env = process.env[name]
if (!env) {
return defaultValue
}
const envNum = +env
if (isNaN(envNum) || !Number.isInteger(envNum)) {
throw new Error(`${name} ENV must be an integer number or nothing`);
}
return envNum
}
export const EnvCanBeBoolean = (name: string): boolean => { export const EnvCanBeBoolean = (name: string): boolean => {
const env = process.env[name] const env = process.env[name]
if (!env) return false if (!env) return false

View file

@ -38,6 +38,8 @@ export interface LightningHandler {
GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]> GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]>
GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse> GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse>
GetAllPayments(max: number): Promise<ListPaymentsResponse> GetAllPayments(max: number): Promise<ListPaymentsResponse>
LockOutgoingOperations(): void
UnlockOutgoingOperations(): void
} }
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => { export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => {

View file

@ -33,6 +33,7 @@ export default class {
newBlockCb: NewBlockCb newBlockCb: NewBlockCb
htlcCb: HtlcCb htlcCb: HtlcCb
log = getLogger({ appName: 'lndManager' }) log = getLogger({ appName: 'lndManager' })
outgoingOpsLocked = false
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) { constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
this.settings = settings this.settings = settings
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
@ -60,6 +61,14 @@ export default class {
this.router = new RouterClient(transport) this.router = new RouterClient(transport)
this.chainNotifier = new ChainNotifierClient(transport) this.chainNotifier = new ChainNotifierClient(transport)
} }
LockOutgoingOperations(): void {
this.outgoingOpsLocked = true
}
UnlockOutgoingOperations(): void {
this.outgoingOpsLocked = false
}
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> { SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
throw new Error("SetMockInvoiceAsPaid only available in mock mode") throw new Error("SetMockInvoiceAsPaid only available in mock mode")
} }
@ -251,6 +260,10 @@ export default class {
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 } return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
} }
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> { async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync")
}
await this.Health() await this.Health()
this.log("paying invoice", invoice, "for", amount, "sats") this.log("paying invoice", invoice, "for", amount, "sats")
const abortController = new AbortController() const abortController = new AbortController()
@ -287,6 +300,10 @@ export default class {
} }
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> { async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> {
if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync")
}
await this.Health() await this.Health()
this.log("sending chain TX for", amount, "sats", "to", address) this.log("sending chain TX for", amount, "sats", "to", address)
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata()) const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())

View file

@ -131,6 +131,12 @@ export default class {
async GetAllPayments(max: number): Promise<ListPaymentsResponse> { async GetAllPayments(max: number): Promise<ListPaymentsResponse> {
throw new Error("not implemented") throw new Error("not implemented")
} }
LockOutgoingOperations() {
throw new Error("not implemented")
}
UnlockOutgoingOperations() {
throw new Error("not implemented")
}
} }

View file

@ -0,0 +1,117 @@
import { EnvCanBeInteger } from "../helpers/envParser.js";
import { getLogger } from "../helpers/logger.js";
import { LightningHandler } from "./index.js";
export type WatchdogSettings = {
maxDiffSats: number
}
export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => {
return {
maxDiffSats: EnvCanBeInteger("WATCHDOG_MAX_DIFF_SATS")
}
}
export class Watchdog {
initialLndBalance: number;
initialUsersBalance: number;
lnd: LightningHandler;
settings: WatchdogSettings;
log = getLogger({ appName: "watchdog" })
enabled = false
constructor(settings: WatchdogSettings, lnd: LightningHandler) {
this.lnd = lnd;
this.settings = settings;
}
SeedLndBalance = async (totalUsersBalance: number) => {
this.initialLndBalance = await this.getTotalLndBalance()
this.initialUsersBalance = totalUsersBalance
this.enabled = true
}
getTotalLndBalance = async () => {
const { channelsBalance, confirmedBalance } = await this.lnd.GetBalance()
return confirmedBalance + channelsBalance.reduce((acc, { localBalanceSats }) => acc + localBalanceSats, 0)
}
checkBalanceUpdate = (deltaLnd: number, deltaUsers: number) => {
this.log("LND balance update:", deltaLnd, "sats since app startup")
this.log("Users balance update:", deltaUsers, "sats since app startup")
const result = this.checkDeltas(deltaLnd, deltaUsers)
switch (result.type) {
case 'mismatch':
if (deltaLnd < 0) {
this.log("WARNING! LND balance decreased while users balance increased creating a difference of", result.absoluteDiff, "sats")
if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations")
return true
}
} else {
this.log("LND balance increased while users balance decreased creating a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection")
return false
}
break
case 'negative':
if (Math.abs(deltaLnd) > Math.abs(deltaUsers)) {
this.log("WARNING! LND balance decreased more than users balance with a difference of", result.absoluteDiff, "sats")
if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations")
return true
}
} else {
this.log("LND balance decreased less than users balance with a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection")
return false
}
break
case 'positive':
if (deltaLnd < deltaUsers) {
this.log("WARNING! LND balance increased less than users balance with a difference of", result.absoluteDiff, "sats")
if (result.absoluteDiff > this.settings.maxDiffSats) {
this.log("Difference is too big for an update, locking outgoing operations")
return true
}
} else {
this.log("LND balance increased more than users balance with a difference of", result.absoluteDiff, "sats, could be caused by data loss, or liquidity injection")
return false
}
}
return false
}
PaymentRequested = async (totalUsersBalance: number) => {
this.log("Payment requested, checking balance")
if (!this.enabled) {
this.log("WARNING! Watchdog not enabled, skipping balance check")
return
}
const totalLndBalance = await this.getTotalLndBalance()
const deltaLnd = totalLndBalance - this.initialLndBalance
const deltaUsers = totalUsersBalance - this.initialUsersBalance
const deny = this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (deny) {
this.log("Balance mismatch detected in absolute update, locking outgoing operations")
this.lnd.LockOutgoingOperations()
return
}
}
checkDeltas = (deltaLnd: number, deltaUsers: number): DeltaCheckResult => {
if (deltaLnd < 0) {
if (deltaUsers < 0) {
const diff = Math.abs(deltaLnd - deltaUsers)
return { type: 'negative', absoluteDiff: diff, relativeDiff: diff / Math.max(deltaLnd, deltaUsers) }
} else {
const diff = Math.abs(deltaLnd) + deltaUsers
return { type: 'mismatch', absoluteDiff: diff }
}
} else {
if (deltaUsers < 0) {
const diff = deltaLnd + Math.abs(deltaUsers)
return { type: 'mismatch', absoluteDiff: diff }
} else {
const diff = Math.abs(deltaLnd - deltaUsers)
return { type: 'positive', absoluteDiff: diff, relativeDiff: diff / Math.max(deltaLnd, deltaUsers) }
}
}
}
}
type DeltaCheckResult = { type: 'negative' | 'positive', absoluteDiff: number, relativeDiff: number } | { type: 'mismatch', absoluteDiff: number }

View file

@ -31,9 +31,23 @@ export default class {
return decoded return decoded
} }
async BanUser(userId: string): Promise<Types.BanUserResponse> {
const banned = await this.storage.userStorage.BanUser(userId)
const appUsers = await this.storage.applicationStorage.GetAllAppUsersFromUser(userId)
return {
balance_sats: banned.balance_sats,
banned_app_users: appUsers.map(appUser => ({
app_id: appUser.application.app_id,
app_name: appUser.application.name,
user_identifier: appUser.identifier,
nostr_pub: appUser.nostr_public_key || ""
}))
}
}
async GetUserInfo(ctx: Types.UserContext): Promise<Types.UserInfo> { async GetUserInfo(ctx: Types.UserContext): Promise<Types.UserInfo> {
const user = await this.storage.userStorage.GetUser(ctx.user_id) const user = await this.storage.userStorage.GetUser(ctx.user_id)
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id); const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const appUser = await this.storage.applicationStorage.GetAppUserFromUser(app, user.user_id) const appUser = await this.storage.applicationStorage.GetAppUserFromUser(app, user.user_id)
if (!appUser) { if (!appUser) {
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing

View file

@ -79,6 +79,10 @@ export default class {
await Promise.all(confirmed.map(async c => { await Promise.all(confirmed.map(async c => {
if (c.type === 'outgoing') { if (c.type === 'outgoing') {
await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs }) await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs })
const { linkedApplication, user, address, paid_amount: amount, service_fees: serviceFee, serial_id: serialId, chain_fees } = c.tx;
const operationId = `${Types.UserOperationType.OUTGOING_TX}-${serialId}`
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: false, type: Types.UserOperationType.OUTGOING_TX, identifier: address, operationId, network_fee: chain_fees, service_fee: serviceFee, confirmed: true, tx_hash: c.tx.tx_hash, internal: c.tx.internal }
this.sendOperationToNostr(linkedApplication!, user.user_id, op)
} else { } else {
this.storage.StartTransaction(async tx => { this.storage.StartTransaction(async tx => {
const { user_address: userAddress, paid_amount: amount, service_fee: serviceFee, serial_id: serialId, tx_hash } = c.tx const { user_address: userAddress, paid_amount: amount, service_fee: serviceFee, serial_id: serialId, tx_hash } = c.tx
@ -90,12 +94,12 @@ export default class {
if (!updateResult.affected) { if (!updateResult.affected) {
throw new Error("unable to flag chain transaction as paid") throw new Error("unable to flag chain transaction as paid")
} }
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, amount - serviceFee, userAddress.address, tx) const addressData = `${userAddress.address}:${tx_hash}`
this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount })
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, amount - serviceFee, addressData, tx)
if (serviceFee > 0) { if (serviceFee > 0) {
await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, serviceFee, 'fees', tx) await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, serviceFee, 'fees', tx)
} }
const addressData = `${userAddress.address}:${tx_hash}`
this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount })
const operationId = `${Types.UserOperationType.INCOMING_TX}-${serialId}` const operationId = `${Types.UserOperationType.INCOMING_TX}-${serialId}`
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: serviceFee, confirmed: true, tx_hash: c.tx.tx_hash, internal: c.tx.internal } const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: serviceFee, confirmed: true, tx_hash: c.tx.tx_hash, internal: c.tx.internal }
this.sendOperationToNostr(userAddress.linkedApplication!, userAddress.user.user_id, op) this.sendOperationToNostr(userAddress.linkedApplication!, userAddress.user.user_id, op)
@ -125,12 +129,13 @@ export default class {
// This call will fail if the transaction is already registered // This call will fail if the transaction is already registered
const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, tx) const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, tx)
if (internal) { if (internal) {
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, addedTx.paid_amount - fee, userAddress.address, tx) const addressData = `${address}:${txOutput.hash}`
this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount })
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, addedTx.paid_amount - fee, addressData, tx)
if (fee > 0) { if (fee > 0) {
await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, fee, 'fees', tx) await this.storage.userStorage.IncrementUserBalance(userAddress.linkedApplication.owner.user_id, fee, 'fees', tx)
} }
const addressData = `${address}:${txOutput.hash}`
this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount })
} }
const operationId = `${Types.UserOperationType.INCOMING_TX}-${addedTx.serial_id}` const operationId = `${Types.UserOperationType.INCOMING_TX}-${addedTx.serial_id}`
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: fee, confirmed: internal, tx_hash: txOutput.hash, internal: false } const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: fee, confirmed: internal, tx_hash: txOutput.hash, internal: false }
@ -160,11 +165,11 @@ export default class {
} }
try { try {
await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, internal, tx) await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, internal, tx)
this.storage.eventsLog.LogEvent({ type: 'invoice_paid', userId: userInvoice.user.user_id, appId: userInvoice.linkedApplication.app_id, appUserId: "", balance: userInvoice.user.balance_sats, data: paymentRequest, amount })
await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, userInvoice.invoice, tx) await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, userInvoice.invoice, tx)
if (fee > 0) { if (fee > 0) {
await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, 'fees', tx) await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, 'fees', tx)
} }
this.storage.eventsLog.LogEvent({ type: 'invoice_paid', userId: userInvoice.user.user_id, appId: userInvoice.linkedApplication.app_id, appUserId: "", balance: userInvoice.user.balance_sats, data: paymentRequest, amount })
await this.triggerPaidCallback(log, userInvoice.callbackUrl) await this.triggerPaidCallback(log, userInvoice.callbackUrl)
const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}` const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}`
const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee, confirmed: true, tx_hash: "", internal } const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee, confirmed: true, tx_hash: "", internal }

View file

@ -15,11 +15,13 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings
if (manualMigration) { if (manualMigration) {
return return
} }
if (!mainSettings.skipSanityCheck) {
await storageManager.VerifyEventsLog()
}
const mainHandler = new Main(mainSettings, storageManager) const mainHandler = new Main(mainSettings, storageManager)
await mainHandler.lnd.Warmup() await mainHandler.lnd.Warmup()
if (!mainSettings.skipSanityCheck) {
await mainHandler.VerifyEventsLog()
}
const totalUsersBalance = await mainHandler.storage.paymentStorage.GetTotalUsersBalance()
await mainHandler.paymentManager.watchDog.SeedLndBalance(totalUsersBalance || 0)
const appsData = await mainHandler.storage.applicationStorage.GetApplications() const appsData = await mainHandler.storage.applicationStorage.GetApplications()
const existingWalletApp = await appsData.find(app => app.name === 'wallet' || app.name === 'wallet-test') const existingWalletApp = await appsData.find(app => app.name === 'wallet' || app.name === 'wallet-test')
if (!existingWalletApp) { if (!existingWalletApp) {
@ -49,7 +51,7 @@ const processArgs = async (mainHandler: Main) => {
getLogger({ userId: process.argv[3] })(`user balance updated correctly`) getLogger({ userId: process.argv[3] })(`user balance updated correctly`)
return false return false
case 'unlock': case 'unlock':
await mainHandler.storage.userStorage.UnlockUser(process.argv[3]) await mainHandler.storage.userStorage.UnbanUser(process.argv[3])
getLogger({ userId: process.argv[3] })(`user unlocked`) getLogger({ userId: process.argv[3] })(`user unlocked`)
return false return false
default: default:

View file

@ -14,6 +14,7 @@ import { SendCoinsResponse } from '../../../proto/lnd/lightning.js'
import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js' import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js' import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js'
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js' import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
import { Watchdog } from '../lnd/watchdog.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -45,10 +46,12 @@ export default class {
addressPaidCb: AddressPaidCb addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
log = getLogger({ appName: "PaymentManager" }) log = getLogger({ appName: "PaymentManager" })
watchDog: Watchdog
constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage this.storage = storage
this.settings = settings this.settings = settings
this.lnd = lnd this.lnd = lnd
this.watchDog = new Watchdog(settings.watchDogSettings, lnd)
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
} }
@ -97,6 +100,9 @@ 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 user = await this.storage.userStorage.GetUser(ctx.user_id) const user = await this.storage.userStorage.GetUser(ctx.user_id)
if (user.locked) {
throw new Error("user is banned, cannot generate address")
}
const existingAddress = await this.storage.paymentStorage.GetExistingUserAddress(ctx.user_id, app) const existingAddress = await this.storage.paymentStorage.GetExistingUserAddress(ctx.user_id, app)
if (existingAddress) { if (existingAddress) {
return { address: existingAddress.address } return { address: existingAddress.address }
@ -109,6 +115,9 @@ export default class {
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> {
const user = await this.storage.userStorage.GetUser(userId) const user = await this.storage.userStorage.GetUser(userId)
if (user.locked) {
throw new Error("user is banned, cannot generate invoice")
}
const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry) const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry)
const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options) const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options)
const appId = options.linkedApplication ? options.linkedApplication.app_id : "" const appId = options.linkedApplication ? options.linkedApplication.app_id : ""
@ -134,8 +143,18 @@ export default class {
} }
} }
async WatchdogCheck() {
const total = await this.storage.paymentStorage.GetTotalUsersBalance()
await this.watchDog.PaymentRequested(total || 0)
}
async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> { async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> {
this.log("paying invoice", req.invoice, "for user", userId, "with amount", req.amount) this.log("paying invoice", req.invoice, "for user", userId, "with amount", req.amount)
await this.WatchdogCheck()
const maybeBanned = await this.storage.userStorage.GetUser(userId)
if (maybeBanned.locked) {
throw new Error("user is banned, cannot send payment")
}
const decoded = await this.lnd.DecodeInvoice(req.invoice) const decoded = await this.lnd.DecodeInvoice(req.invoice)
if (decoded.numSatoshis !== 0 && req.amount !== 0) { if (decoded.numSatoshis !== 0 && req.amount !== 0) {
throw new Error("invoice has value, do not provide amount the the request") throw new Error("invoice has value, do not provide amount the the request")
@ -192,6 +211,12 @@ export default class {
async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> { async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
throw new Error("address payment currently disabled, use Lightning instead") throw new Error("address payment currently disabled, use Lightning instead")
await this.WatchdogCheck()
this.log("paying address", req.address, "for user", ctx.user_id, "with amount", req.amoutSats)
const maybeBanned = await this.storage.userStorage.GetUser(ctx.user_id)
if (maybeBanned.locked) {
throw new Error("user is banned, cannot send chain tx")
}
const { blockHeight } = await this.lnd.GetInfo() const { blockHeight } = await this.lnd.GetInfo()
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false)
@ -459,6 +484,10 @@ export default class {
} }
async GetUserOperations(userId: string, req: Types.GetUserOperationsRequest): Promise<Types.GetUserOperationsResponse> { async GetUserOperations(userId: string, req: Types.GetUserOperationsRequest): Promise<Types.GetUserOperationsResponse> {
const user = await this.storage.userStorage.GetUser(userId)
if (user.locked) {
throw new Error("user is banned, cannot retrieve operations")
}
const [outgoingInvoices, outgoingTransactions, incomingInvoices, incomingTransactions, incomingUserToUser, outgoingUserToUser] = await Promise.all([ const [outgoingInvoices, outgoingTransactions, incomingInvoices, incomingTransactions, incomingUserToUser, outgoingUserToUser] = await Promise.all([
this.storage.paymentStorage.GetUserInvoicePayments(userId, req.latestOutgoingInvoice, req.max_size), this.storage.paymentStorage.GetUserInvoicePayments(userId, req.latestOutgoingInvoice, req.max_size),
this.storage.paymentStorage.GetUserTransactionPayments(userId, req.latestOutgoingTx, req.max_size), this.storage.paymentStorage.GetUserTransactionPayments(userId, req.latestOutgoingTx, req.max_size),
@ -482,6 +511,9 @@ export default class {
await this.storage.StartTransaction(async tx => { await this.storage.StartTransaction(async tx => {
const fromUser = await this.storage.userStorage.GetUser(fromUserId, tx) const fromUser = await this.storage.userStorage.GetUser(fromUserId, tx)
const toUser = await this.storage.userStorage.GetUser(toUserId, tx) const toUser = await this.storage.userStorage.GetUser(toUserId, tx)
if (fromUser.locked || toUser.locked) {
throw new Error("one of the users is banned, cannot send payment")
}
if (fromUser.balance_sats < amount) { if (fromUser.balance_sats < amount) {
throw new Error("not enough balance to send payment") throw new Error("not enough balance to send payment")
} }

View file

@ -1,10 +1,12 @@
import { LoadStorageSettingsFromEnv, StorageSettings } from '../storage/index.js' import { LoadStorageSettingsFromEnv, StorageSettings } from '../storage/index.js'
import { LndSettings } from '../lnd/settings.js' import { LndSettings } from '../lnd/settings.js'
import { LoadWatchdogSettingsFromEnv, WatchdogSettings } from '../lnd/watchdog.js'
import { LoadLndSettingsFromEnv } from '../lnd/index.js' import { LoadLndSettingsFromEnv } from '../lnd/index.js'
import { EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js' import { EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js'
export type MainSettings = { export type MainSettings = {
storageSettings: StorageSettings, storageSettings: StorageSettings,
lndSettings: LndSettings, lndSettings: LndSettings,
watchDogSettings: WatchdogSettings,
jwtSecret: string jwtSecret: string
incomingTxFee: number incomingTxFee: number
outgoingTxFee: number outgoingTxFee: number
@ -22,6 +24,7 @@ export type MainSettings = {
} }
export const LoadMainSettingsFromEnv = (): MainSettings => { export const LoadMainSettingsFromEnv = (): MainSettings => {
return { return {
watchDogSettings: LoadWatchdogSettingsFromEnv(),
lndSettings: LoadLndSettingsFromEnv(), lndSettings: LoadLndSettingsFromEnv(),
storageSettings: LoadStorageSettingsFromEnv(), storageSettings: LoadStorageSettingsFromEnv(),
jwtSecret: EnvMustBeNonEmptyString("JWT_SECRET"), jwtSecret: EnvMustBeNonEmptyString("JWT_SECRET"),
@ -37,7 +40,8 @@ export const LoadMainSettingsFromEnv = (): MainSettings => {
servicePort: EnvMustBeInteger("PORT"), servicePort: EnvMustBeInteger("PORT"),
recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false, recordPerformance: process.env.RECORD_PERFORMANCE === 'true' || false,
skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false, skipSanityCheck: process.env.SKIP_SANITY_CHECK === 'true' || false,
disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false disableExternalPayments: process.env.DISABLE_EXTERNAL_PAYMENTS === 'true' || false,
} }
} }

View file

@ -19,6 +19,13 @@ export default (mainHandler: Main): Types.ServerMethods => {
const info = await mainHandler.lnd.GetInfo() const info = await mainHandler.lnd.GetInfo()
return { alias: info.alias } return { alias: info.alias }
}, },
BanUser: async ({ ctx, req }) => {
const err = Types.BanUserRequestValidate(req, {
user_id_CustomCheck: id => id !== ''
})
if (err != null) throw new Error(err.message)
return mainHandler.appUserManager.BanUser(req.user_id)
},
SetMockInvoiceAsPaid: async ({ ctx, req }) => { SetMockInvoiceAsPaid: async ({ ctx, req }) => {
const err = Types.SetMockInvoiceAsPaidRequestValidate(req, { const err = Types.SetMockInvoiceAsPaidRequestValidate(req, {
invoice_CustomCheck: invoice => invoice !== '', invoice_CustomCheck: invoice => invoice !== '',

View file

@ -143,7 +143,11 @@ export default class {
} }
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 entityManager.getRepository(ApplicationUser).findOne({ where: { user: { user_id: userId }, application: { app_id: application.app_id } } })
}
async GetAllAppUsersFromUser(userId: string, entityManager = this.DB): Promise<ApplicationUser[]> {
return entityManager.getRepository(ApplicationUser).find({ where: { user: { user_id: userId } } })
} }
async IsApplicationOwner(userId: string, entityManager = this.DB) { async IsApplicationOwner(userId: string, entityManager = this.DB) {

View file

@ -360,4 +360,8 @@ export default class {
break; break;
} }
} }
async GetTotalUsersBalance(entityManager = this.DB) {
return entityManager.getRepository(User).sum("balance_sats")
}
} }

View file

@ -14,6 +14,7 @@ export default class {
this.txQueue = txQueue this.txQueue = txQueue
this.eventsLog = eventsLog this.eventsLog = eventsLog
} }
async AddUser(balance: number, dbTx: DataSource | EntityManager): Promise<User> { async AddUser(balance: number, dbTx: DataSource | EntityManager): Promise<User> {
if (balance && process.env.ALLOW_BALANCE_MIGRATION !== 'true') { if (balance && process.env.ALLOW_BALANCE_MIGRATION !== 'true') {
throw new Error("balance migration is not allowed") throw new Error("balance migration is not allowed")
@ -53,15 +54,7 @@ export default class {
return user return user
} }
async LockUser(userId: string, entityManager = this.DB) { async UnbanUser(userId: string, entityManager = this.DB) {
const res = await entityManager.getRepository(User).update({
user_id: userId
}, { locked: true })
if (!res.affected) {
throw new Error("unaffected user lock for " + userId) // TODO: fix logs doxing
}
}
async UnlockUser(userId: string, entityManager = this.DB) {
const res = await entityManager.getRepository(User).update({ const res = await entityManager.getRepository(User).update({
user_id: userId user_id: userId
}, { locked: false }) }, { locked: false })
@ -69,6 +62,20 @@ export default class {
throw new Error("unaffected user unlock for " + userId) // TODO: fix logs doxing throw new Error("unaffected user unlock for " + userId) // TODO: fix logs doxing
} }
} }
async BanUser(userId: string, entityManager = this.DB) {
const user = await this.GetUser(userId, entityManager)
const res = await entityManager.getRepository(User).update({
user_id: userId
}, { balance_sats: 0, locked: true })
if (!res.affected) {
throw new Error("unaffected ban user for " + userId) // TODO: fix logs doxing
}
if (user.balance_sats > 0) {
this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: 'ban', amount: user.balance_sats })
}
return user
}
async IncrementUserBalance(userId: string, increment: number, reason: string, entityManager = this.DB) { async IncrementUserBalance(userId: string, increment: number, reason: string, entityManager = this.DB) {
const user = await this.GetUser(userId, entityManager) const user = await this.GetUser(userId, entityManager)
const res = await entityManager.getRepository(User).increment({ const res = await entityManager.getRepository(User).increment({