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
SKIP_SANITY_CHECK=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 }])
} 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')
app.post('/api/reports/usage', async (req, res) => {
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' }
},
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)> => {
const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null')

View file

@ -106,6 +106,12 @@ service LightningPub {
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) {
option (auth_type) = "Metrics";
option (http_method) = "post";
@ -123,6 +129,8 @@ service LightningPub {
option (http_method) = "post";
option (http_route) = "/api/reports/lnd";
}
// </Admin>
// <Guest>

View file

@ -155,6 +155,21 @@ message LndGetInfoResponse {
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 {
string name = 1;
bool allow_user_creation = 2;

View file

@ -2,10 +2,8 @@ import 'dotenv/config'
import NewServer from '../proto/autogenerated/ts/express_server.js'
import GetServerMethods from './services/serverMethods/index.js'
import serverOptions from './auth.js';
import Storage from './services/storage/index.js'
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js'
import nostrMiddleware from './nostrMiddleware.js'
import { TypeOrmMigrationRunner } from './services/storage/migrations/runner.js';
import { getLogger } from './services/helpers/logger.js';
import { initMainHandler } from './services/main/init.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, {
NostrUserAuthGuard: async (appId, pub) => {
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 || "" }
},
metricsCallback: metrics => mainHandler.settings.recordPerformance ? mainHandler.metricsManager.AddMetrics(metrics) : null,

View file

@ -10,6 +10,17 @@ export const EnvMustBeInteger = (name: string): number => {
}
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 => {
const env = process.env[name]
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 }[]>
GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse>
GetAllPayments(max: number): Promise<ListPaymentsResponse>
LockOutgoingOperations(): void
UnlockOutgoingOperations(): void
}
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => {

View file

@ -33,6 +33,7 @@ export default class {
newBlockCb: NewBlockCb
htlcCb: HtlcCb
log = getLogger({ appName: 'lndManager' })
outgoingOpsLocked = false
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
this.settings = settings
this.addressPaidCb = addressPaidCb
@ -60,6 +61,14 @@ export default class {
this.router = new RouterClient(transport)
this.chainNotifier = new ChainNotifierClient(transport)
}
LockOutgoingOperations(): void {
this.outgoingOpsLocked = true
}
UnlockOutgoingOperations(): void {
this.outgoingOpsLocked = false
}
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
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 }
}
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()
this.log("paying invoice", invoice, "for", amount, "sats")
const abortController = new AbortController()
@ -287,6 +300,10 @@ export default class {
}
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()
this.log("sending chain TX for", amount, "sats", "to", address)
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> {
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
}
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> {
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)
if (!appUser) {
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 => {
if (c.type === 'outgoing') {
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 {
this.storage.StartTransaction(async 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) {
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) {
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 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)
@ -125,12 +129,13 @@ export default class {
// 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)
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) {
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 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 {
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)
if (fee > 0) {
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)
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 }

View file

@ -15,11 +15,13 @@ export const initMainHandler = async (log: PubLogger, mainSettings: MainSettings
if (manualMigration) {
return
}
if (!mainSettings.skipSanityCheck) {
await storageManager.VerifyEventsLog()
}
const mainHandler = new Main(mainSettings, storageManager)
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 existingWalletApp = await appsData.find(app => app.name === 'wallet' || app.name === 'wallet-test')
if (!existingWalletApp) {
@ -49,7 +51,7 @@ const processArgs = async (mainHandler: Main) => {
getLogger({ userId: process.argv[3] })(`user balance updated correctly`)
return false
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`)
return false
default:

View file

@ -14,6 +14,7 @@ import { SendCoinsResponse } from '../../../proto/lnd/lightning.js'
import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js'
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
import { Watchdog } from '../lnd/watchdog.js'
interface UserOperationInfo {
serial_id: number
paid_amount: number
@ -45,10 +46,12 @@ export default class {
addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb
log = getLogger({ appName: "PaymentManager" })
watchDog: Watchdog
constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage
this.settings = settings
this.lnd = lnd
this.watchDog = new Watchdog(settings.watchDogSettings, lnd)
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
}
@ -97,6 +100,9 @@ export default class {
async NewAddress(ctx: Types.UserContext, req: Types.NewAddressRequest): Promise<Types.NewAddressResponse> {
const app = await this.storage.applicationStorage.GetApplication(ctx.app_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)
if (existingAddress) {
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> {
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 userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options)
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> {
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)
if (decoded.numSatoshis !== 0 && req.amount !== 0) {
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> {
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 app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
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> {
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([
this.storage.paymentStorage.GetUserInvoicePayments(userId, req.latestOutgoingInvoice, 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 => {
const fromUser = await this.storage.userStorage.GetUser(fromUserId, 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) {
throw new Error("not enough balance to send payment")
}

View file

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

View file

@ -360,4 +360,8 @@ export default class {
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.eventsLog = eventsLog
}
async AddUser(balance: number, dbTx: DataSource | EntityManager): Promise<User> {
if (balance && process.env.ALLOW_BALANCE_MIGRATION !== 'true') {
throw new Error("balance migration is not allowed")
@ -53,15 +54,7 @@ export default class {
return user
}
async LockUser(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) {
async UnbanUser(userId: string, entityManager = this.DB) {
const res = await entityManager.getRepository(User).update({
user_id: userId
}, { locked: false })
@ -69,6 +62,20 @@ export default class {
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) {
const user = await this.GetUser(userId, entityManager)
const res = await entityManager.getRepository(User).increment({