diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index e70e0e3d..d9440569 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -24,6 +24,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to the Container registry uses: docker/login-action@v2 with: @@ -50,6 +56,7 @@ jobs: context: . file: ./Dockerfile push: true + platforms: linux/amd64,linux/arm64 tags: | ghcr.io/${{ github.repository_owner }}/lightning-pub:latest ghcr.io/${{ github.repository_owner }}/lightning-pub:master @@ -58,8 +65,9 @@ jobs: - name: Capture image digest id: capture-digest run: | - DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/${{ github.repository_owner }}/lightning-pub:latest | cut -d'@' -f2) - echo "Raw Digest is $DIGEST" + # For multi-arch builds, use the digest from build output + DIGEST="${{ steps.build-and-push.outputs.digest }}" + echo "Multi-arch manifest digest: $DIGEST" echo "digest=$DIGEST" >> $GITHUB_OUTPUT - name: Debug Print Digest @@ -71,3 +79,30 @@ jobs: subject-digest: ${{ steps.capture-digest.outputs.digest }} subject-name: ghcr.io/${{ github.repository_owner }}/lightning-pub push-to-registry: true + + - name: Trigger StartOS package build + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' + env: + STARTOS_BUILD_TRIGGER: ${{ secrets.STARTOS_BUILD_TRIGGER }} + run: | + if [ -z "$STARTOS_BUILD_TRIGGER" ]; then + echo "⚠️ STARTOS_BUILD_TRIGGER not configured, skipping StartOS build trigger" + exit 0 + fi + + echo "🚀 Triggering StartOS package build with digest: ${{ steps.capture-digest.outputs.digest }}" + + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $STARTOS_BUILD_TRIGGER" \ + https://api.github.com/repos/shocknet/start9-LightningPub/dispatches \ + -d "{ + \"event_type\": \"docker-image-updated\", + \"client_payload\": { + \"docker_tag\": \"latest\", + \"digest\": \"${{ steps.capture-digest.outputs.digest }}\", + \"commit\": \"${{ github.sha }}\" + } + }" + + echo "✓ StartOS build triggered" \ No newline at end of file diff --git a/DOCKER.md b/DOCKER.md index 8ffbb87d..1e4bebae 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -1,8 +1,5 @@ # Docker Installation -> [!WARNING] -> The Docker deployment method is currently unmaintained and may not work as expected. Help is wanted! If you are a Docker enjoyer, please consider contributing to this deployment method. - 1. Pull the Docker image: ```ssh @@ -21,4 +18,5 @@ docker run -d \ -v $HOME/.lnd:/root/.lnd \ ghcr.io/shocknet/lightning-pub:latest ``` -Network host is used so the service can reach a local LND via localhost. LND is assumed to be under the users home folder, update this location as needed. + +Network host is used so the service can reach a local LND via localhost. LND is assumed to be under the users home folder, update these resources as needed. diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index a5dfc69f..7eac86ae 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1104,6 +1104,13 @@ The nostr server will send back a message response, and inside the body there wi - __nostr_pub__: _string_ - __user_identifier__: _string_ +### BeaconData + - __avatarUrl__: _string_ *this field is optional + - __fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional + - __name__: _string_ + - __nextRelay__: _string_ *this field is optional + - __type__: _string_ + ### BundleData - __available_chunks__: ARRAY of: _number_ - __base_64_data__: ARRAY of: _string_ @@ -1149,6 +1156,10 @@ The nostr server will send back a message response, and inside the body there wi ### CreateOneTimeInviteLinkResponse - __invitation_link__: _string_ +### CumulativeFees + - __networkFeeFixed__: _number_ + - __serviceFeeBps__: _number_ + ### DebitAuthorization - __authorized__: _boolean_ - __debit_id__: _string_ @@ -1287,6 +1298,7 @@ The nostr server will send back a message response, and inside the body there wi - __request_id__: _string_ ### LiveUserOperation + - __latest_balance__: _number_ - __operation__: _[UserOperation](#UserOperation)_ ### LndChannels @@ -1473,16 +1485,19 @@ The nostr server will send back a message response, and inside the body there wi ### PayAppUserInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional + - __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional - __invoice__: _string_ - __user_identifier__: _string_ ### PayInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional + - __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional - __invoice__: _string_ ### PayInvoiceResponse - __amount_paid__: _number_ + - __latest_balance__: _number_ - __network_fee__: _number_ - __operation_id__: _string_ - __preimage__: _string_ @@ -1570,7 +1585,6 @@ The nostr server will send back a message response, and inside the body there wi ### TransactionSwapQuote - __chain_fee_sats__: _number_ - __invoice_amount_sats__: _number_ - - __routing_fee_reserve_sats__: _number_ - __service_fee_sats__: _number_ - __swap_fee_sats__: _number_ - __swap_operation_id__: _string_ diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 2623e357..cb1389cb 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -177,6 +177,13 @@ type BannedAppUser struct { Nostr_pub string `json:"nostr_pub"` User_identifier string `json:"user_identifier"` } +type BeaconData struct { + Avatarurl string `json:"avatarUrl"` + Fees *CumulativeFees `json:"fees"` + Name string `json:"name"` + Nextrelay string `json:"nextRelay"` + Type string `json:"type"` +} type BundleData struct { Available_chunks []int64 `json:"available_chunks"` Base_64_data []string `json:"base_64_data"` @@ -222,6 +229,10 @@ type CreateOneTimeInviteLinkRequest struct { type CreateOneTimeInviteLinkResponse struct { Invitation_link string `json:"invitation_link"` } +type CumulativeFees struct { + Networkfeefixed int64 `json:"networkFeeFixed"` + Servicefeebps int64 `json:"serviceFeeBps"` +} type DebitAuthorization struct { Authorized bool `json:"authorized"` Debit_id string `json:"debit_id"` @@ -360,7 +371,8 @@ type LiveManageRequest struct { Request_id string `json:"request_id"` } type LiveUserOperation struct { - Operation *UserOperation `json:"operation"` + Latest_balance int64 `json:"latest_balance"` + Operation *UserOperation `json:"operation"` } type LndChannels struct { Open_channels []OpenChannel `json:"open_channels"` @@ -544,22 +556,25 @@ type PayAddressResponse struct { Txid string `json:"txId"` } type PayAppUserInvoiceRequest struct { - Amount int64 `json:"amount"` - Debit_npub string `json:"debit_npub"` - Invoice string `json:"invoice"` - User_identifier string `json:"user_identifier"` + Amount int64 `json:"amount"` + Debit_npub string `json:"debit_npub"` + Expected_fees *CumulativeFees `json:"expected_fees"` + Invoice string `json:"invoice"` + User_identifier string `json:"user_identifier"` } type PayInvoiceRequest struct { - Amount int64 `json:"amount"` - Debit_npub string `json:"debit_npub"` - Invoice string `json:"invoice"` + Amount int64 `json:"amount"` + Debit_npub string `json:"debit_npub"` + Expected_fees *CumulativeFees `json:"expected_fees"` + Invoice string `json:"invoice"` } type PayInvoiceResponse struct { - Amount_paid int64 `json:"amount_paid"` - Network_fee int64 `json:"network_fee"` - Operation_id string `json:"operation_id"` - Preimage string `json:"preimage"` - Service_fee int64 `json:"service_fee"` + Amount_paid int64 `json:"amount_paid"` + Latest_balance int64 `json:"latest_balance"` + Network_fee int64 `json:"network_fee"` + Operation_id string `json:"operation_id"` + Preimage string `json:"preimage"` + Service_fee int64 `json:"service_fee"` } type PayerData struct { Data map[string]string `json:"data"` @@ -641,13 +656,12 @@ type SingleMetricReq struct { Request_id int64 `json:"request_id"` } type TransactionSwapQuote struct { - Chain_fee_sats int64 `json:"chain_fee_sats"` - Invoice_amount_sats int64 `json:"invoice_amount_sats"` - Routing_fee_reserve_sats int64 `json:"routing_fee_reserve_sats"` - Service_fee_sats int64 `json:"service_fee_sats"` - Swap_fee_sats int64 `json:"swap_fee_sats"` - Swap_operation_id string `json:"swap_operation_id"` - Transaction_amount_sats int64 `json:"transaction_amount_sats"` + Chain_fee_sats int64 `json:"chain_fee_sats"` + Invoice_amount_sats int64 `json:"invoice_amount_sats"` + Service_fee_sats int64 `json:"service_fee_sats"` + Swap_fee_sats int64 `json:"swap_fee_sats"` + Swap_operation_id string `json:"swap_operation_id"` + Transaction_amount_sats int64 `json:"transaction_amount_sats"` } type TransactionSwapRequest struct { Transaction_amount_sats int64 `json:"transaction_amount_sats"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 1afad70e..c0518754 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -983,6 +983,48 @@ export const BannedAppUserValidate = (o?: BannedAppUser, opts: BannedAppUserOpti return null } +export type BeaconData = { + avatarUrl?: string + fees?: CumulativeFees + name: string + nextRelay?: string + type: string +} +export type BeaconDataOptionalField = 'avatarUrl' | 'fees' | 'nextRelay' +export const BeaconDataOptionalFields: BeaconDataOptionalField[] = ['avatarUrl', 'fees', 'nextRelay'] +export type BeaconDataOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: BeaconDataOptionalField[] + avatarUrl_CustomCheck?: (v?: string) => boolean + fees_Options?: CumulativeFeesOptions + name_CustomCheck?: (v: string) => boolean + nextRelay_CustomCheck?: (v?: string) => boolean + type_CustomCheck?: (v: string) => boolean +} +export const BeaconDataValidate = (o?: BeaconData, opts: BeaconDataOptions = {}, path: string = 'BeaconData::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.avatarUrl || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('avatarUrl')) && typeof o.avatarUrl !== 'string') return new Error(`${path}.avatarUrl: is not a string`) + if (opts.avatarUrl_CustomCheck && !opts.avatarUrl_CustomCheck(o.avatarUrl)) return new Error(`${path}.avatarUrl: custom check failed`) + + if (typeof o.fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('fees')) { + const feesErr = CumulativeFeesValidate(o.fees, opts.fees_Options, `${path}.fees`) + if (feesErr !== null) return feesErr + } + + + if (typeof o.name !== 'string') return new Error(`${path}.name: is not a string`) + if (opts.name_CustomCheck && !opts.name_CustomCheck(o.name)) return new Error(`${path}.name: custom check failed`) + + if ((o.nextRelay || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('nextRelay')) && typeof o.nextRelay !== 'string') return new Error(`${path}.nextRelay: is not a string`) + if (opts.nextRelay_CustomCheck && !opts.nextRelay_CustomCheck(o.nextRelay)) return new Error(`${path}.nextRelay: custom check failed`) + + if (typeof o.type !== 'string') return new Error(`${path}.type: is not a string`) + if (opts.type_CustomCheck && !opts.type_CustomCheck(o.type)) return new Error(`${path}.type: custom check failed`) + + return null +} + export type BundleData = { available_chunks: number[] base_64_data: string[] @@ -1256,6 +1298,29 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL return null } +export type CumulativeFees = { + networkFeeFixed: number + serviceFeeBps: number +} +export const CumulativeFeesOptionalFields: [] = [] +export type CumulativeFeesOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + networkFeeFixed_CustomCheck?: (v: number) => boolean + serviceFeeBps_CustomCheck?: (v: number) => boolean +} +export const CumulativeFeesValidate = (o?: CumulativeFees, opts: CumulativeFeesOptions = {}, path: string = 'CumulativeFees::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.networkFeeFixed !== 'number') return new Error(`${path}.networkFeeFixed: is not a number`) + if (opts.networkFeeFixed_CustomCheck && !opts.networkFeeFixed_CustomCheck(o.networkFeeFixed)) return new Error(`${path}.networkFeeFixed: custom check failed`) + + if (typeof o.serviceFeeBps !== 'number') return new Error(`${path}.serviceFeeBps: is not a number`) + if (opts.serviceFeeBps_CustomCheck && !opts.serviceFeeBps_CustomCheck(o.serviceFeeBps)) return new Error(`${path}.serviceFeeBps: custom check failed`) + + return null +} + export type DebitAuthorization = { authorized: boolean debit_id: string @@ -2093,17 +2158,22 @@ export const LiveManageRequestValidate = (o?: LiveManageRequest, opts: LiveManag } export type LiveUserOperation = { + latest_balance: number operation: UserOperation } export const LiveUserOperationOptionalFields: [] = [] export type LiveUserOperationOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] + latest_balance_CustomCheck?: (v: number) => boolean operation_Options?: UserOperationOptions } export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserOperationOptions = {}, path: string = 'LiveUserOperation::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + if (typeof o.latest_balance !== 'number') return new Error(`${path}.latest_balance: is not a number`) + if (opts.latest_balance_CustomCheck && !opts.latest_balance_CustomCheck(o.latest_balance)) return new Error(`${path}.latest_balance: custom check failed`) + const operationErr = UserOperationValidate(o.operation, opts.operation_Options, `${path}.operation`) if (operationErr !== null) return operationErr @@ -3202,15 +3272,17 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr export type PayAppUserInvoiceRequest = { amount: number debit_npub?: string + expected_fees?: CumulativeFees invoice: string user_identifier: string } -export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' -export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub'] +export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees' +export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees'] export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean + expected_fees_Options?: CumulativeFeesOptions invoice_CustomCheck?: (v: string) => boolean user_identifier_CustomCheck?: (v: string) => boolean } @@ -3224,6 +3296,12 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) + if (typeof o.expected_fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('expected_fees')) { + const expected_feesErr = CumulativeFeesValidate(o.expected_fees, opts.expected_fees_Options, `${path}.expected_fees`) + if (expected_feesErr !== null) return expected_feesErr + } + + if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) @@ -3236,14 +3314,16 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o export type PayInvoiceRequest = { amount: number debit_npub?: string + expected_fees?: CumulativeFees invoice: string } -export type PayInvoiceRequestOptionalField = 'debit_npub' -export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub'] +export type PayInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees' +export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees'] export type PayInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean + expected_fees_Options?: CumulativeFeesOptions invoice_CustomCheck?: (v: string) => boolean } export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoiceRequestOptions = {}, path: string = 'PayInvoiceRequest::root.'): Error | null => { @@ -3256,6 +3336,12 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) + if (typeof o.expected_fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('expected_fees')) { + const expected_feesErr = CumulativeFeesValidate(o.expected_fees, opts.expected_fees_Options, `${path}.expected_fees`) + if (expected_feesErr !== null) return expected_feesErr + } + + if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) @@ -3264,6 +3350,7 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic export type PayInvoiceResponse = { amount_paid: number + latest_balance: number network_fee: number operation_id: string preimage: string @@ -3273,6 +3360,7 @@ export const PayInvoiceResponseOptionalFields: [] = [] export type PayInvoiceResponseOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] amount_paid_CustomCheck?: (v: number) => boolean + latest_balance_CustomCheck?: (v: number) => boolean network_fee_CustomCheck?: (v: number) => boolean operation_id_CustomCheck?: (v: string) => boolean preimage_CustomCheck?: (v: string) => boolean @@ -3285,6 +3373,9 @@ export const PayInvoiceResponseValidate = (o?: PayInvoiceResponse, opts: PayInvo if (typeof o.amount_paid !== 'number') return new Error(`${path}.amount_paid: is not a number`) if (opts.amount_paid_CustomCheck && !opts.amount_paid_CustomCheck(o.amount_paid)) return new Error(`${path}.amount_paid: custom check failed`) + if (typeof o.latest_balance !== 'number') return new Error(`${path}.latest_balance: is not a number`) + if (opts.latest_balance_CustomCheck && !opts.latest_balance_CustomCheck(o.latest_balance)) return new Error(`${path}.latest_balance: custom check failed`) + if (typeof o.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`) if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`) @@ -3757,7 +3848,6 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR export type TransactionSwapQuote = { chain_fee_sats: number invoice_amount_sats: number - routing_fee_reserve_sats: number service_fee_sats: number swap_fee_sats: number swap_operation_id: string @@ -3768,7 +3858,6 @@ export type TransactionSwapQuoteOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] chain_fee_sats_CustomCheck?: (v: number) => boolean invoice_amount_sats_CustomCheck?: (v: number) => boolean - routing_fee_reserve_sats_CustomCheck?: (v: number) => boolean service_fee_sats_CustomCheck?: (v: number) => boolean swap_fee_sats_CustomCheck?: (v: number) => boolean swap_operation_id_CustomCheck?: (v: string) => boolean @@ -3784,9 +3873,6 @@ export const TransactionSwapQuoteValidate = (o?: TransactionSwapQuote, opts: Tra if (typeof o.invoice_amount_sats !== 'number') return new Error(`${path}.invoice_amount_sats: is not a number`) if (opts.invoice_amount_sats_CustomCheck && !opts.invoice_amount_sats_CustomCheck(o.invoice_amount_sats)) return new Error(`${path}.invoice_amount_sats: custom check failed`) - if (typeof o.routing_fee_reserve_sats !== 'number') return new Error(`${path}.routing_fee_reserve_sats: is not a number`) - if (opts.routing_fee_reserve_sats_CustomCheck && !opts.routing_fee_reserve_sats_CustomCheck(o.routing_fee_reserve_sats)) return new Error(`${path}.routing_fee_reserve_sats: custom check failed`) - if (typeof o.service_fee_sats !== 'number') return new Error(`${path}.service_fee_sats: is not a number`) if (opts.service_fee_sats_CustomCheck && !opts.service_fee_sats_CustomCheck(o.service_fee_sats)) return new Error(`${path}.service_fee_sats: custom check failed`) diff --git a/proto/service/structs.proto b/proto/service/structs.proto index c1b5011e..400a47ef 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -389,7 +389,8 @@ message PayAppUserInvoiceRequest { string user_identifier = 1; string invoice = 2; int64 amount = 3; - optional string debit_npub = 4; + optional string debit_npub = 4; + optional CumulativeFees expected_fees = 5; } message SendAppUserToAppUserPaymentRequest { @@ -466,7 +467,8 @@ message DecodeInvoiceResponse{ message PayInvoiceRequest{ string invoice = 1; int64 amount = 2; - optional string debit_npub = 3; + optional string debit_npub = 3; + optional CumulativeFees expected_fees = 4; } message PayInvoiceResponse{ @@ -475,6 +477,7 @@ message PayInvoiceResponse{ string operation_id = 3; int64 service_fee = 4; int64 network_fee = 5; + int64 latest_balance = 6; } message GetPaymentStateRequest{ @@ -605,6 +608,7 @@ message GetProductBuyLinkResponse { message LiveUserOperation { UserOperation operation = 1; + int64 latest_balance = 2; } message MigrationUpdate { optional ClosureMigration closure = 1; @@ -837,6 +841,17 @@ message TransactionSwapQuote { int64 swap_fee_sats = 4; int64 chain_fee_sats = 5; - int64 routing_fee_reserve_sats = 6; int64 service_fee_sats = 7; +} +message CumulativeFees { + int64 networkFeeFixed = 2; + int64 serviceFeeBps = 3; +} + +message BeaconData { + string type = 1; + string name = 2; + optional string avatarUrl = 3; + optional string nextRelay = 4; + optional CumulativeFees fees = 5; } \ No newline at end of file diff --git a/src/e2e.ts b/src/e2e.ts index d46e2d59..5e0fda11 100644 --- a/src/e2e.ts +++ b/src/e2e.ts @@ -25,7 +25,10 @@ const start = async () => { const nostrSettings = settingsManager.getSettings().nostrRelaySettings log("initializing nostr middleware") const { Send } = nostrMiddleware(serverMethods, mainHandler, - { ...nostrSettings, apps, clients: [liquidityProviderInfo] }, + { + ...nostrSettings, apps, clients: [liquidityProviderInfo], + providerDestinationPub: settingsManager.getSettings().liquiditySettings.liquidityProviderPub + }, (e, p) => mainHandler.liquidityProvider.onEvent(e, p) ) log("starting server") diff --git a/src/index.ts b/src/index.ts index aa4c3c0b..0b53b7ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,10 +25,13 @@ const start = async () => { const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const serverMethods = GetServerMethods(mainHandler) log("initializing nostr middleware") - const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays - const maxEventContentLength = mainHandler.settings.getSettings().nostrRelaySettings.maxEventContentLength + const relays = settingsManager.getSettings().nostrRelaySettings.relays + const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler, - { relays, maxEventContentLength, apps, clients: [liquidityProviderInfo] }, + { + relays, maxEventContentLength, apps, clients: [liquidityProviderInfo], + providerDestinationPub: settingsManager.getSettings().liquiditySettings.liquidityProviderPub + }, (e, p) => mainHandler.liquidityProvider.onEvent(e, p) ) exitHandler(() => { Stop(); mainHandler.Stop() }) @@ -43,7 +46,7 @@ const start = async () => { } adminManager.setAppNprofile(appNprofile) const Server = NewServer(serverMethods, serverOptions(mainHandler)) - Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) + Server.Listen(settingsManager.getSettings().serviceSettings.servicePort) } start() diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index cc1db630..da1172f3 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -79,7 +79,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett nostrTransport({ ...j, appId: event.appId }, res => { nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) }) }, event.startAtNano, event.startAtMs) - }) + }, beacon => mainHandler.liquidityProvider.onBeaconEvent(beacon)) // Mark nostr connected/ready after initial subscription tick mainHandler.adminManager.setNostrConnected(true) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 9e00d996..5f71285c 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -16,7 +16,7 @@ import { SendCoinsReq } from './sendCoinsReq.js'; import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js'; import { ERROR, getLogger } from '../helpers/logger.js'; import { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; -import { LiquidityProvider, LiquidityRequest } from '../main/liquidityProvider.js'; +import { LiquidityProvider } from '../main/liquidityProvider.js'; import { Utils } from '../helpers/utilsWrapper.js'; import { TxPointSettings } from '../storage/tlv/stateBundler.js'; import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js'; @@ -24,7 +24,7 @@ import SettingsManager from '../main/settingsManager.js'; import { LndNodeSettings, LndSettings } from '../main/settings.js'; const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) -const deadLndRetrySeconds = 5 +const deadLndRetrySeconds = 20 type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' } type NodeSettingsOverride = { lndAddr: string @@ -51,9 +51,11 @@ export default class { outgoingOpsLocked = false liquidProvider: LiquidityProvider utils: Utils - constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { + unlockLnd: () => Promise + constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, unlockLnd: () => Promise, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { this.getSettings = getSettings this.utils = utils + this.unlockLnd = unlockLnd this.addressPaidCb = addressPaidCb this.invoicePaidCb = invoicePaidCb this.newBlockCb = newBlockCb @@ -103,9 +105,10 @@ export default class { } async Warmup() { + // console.log("Warming up LND") this.SubscribeAddressPaid() this.SubscribeInvoicePaid() - this.SubscribeNewBlock() + await this.SubscribeNewBlock() this.SubscribeHtlcEvents() this.SubscribeChannelEvents() const now = Date.now() @@ -127,20 +130,24 @@ export default class { } async GetInfo(): Promise { + // console.log("Getting info") const res = await this.lightning.getInfo({}, DeadLineMetadata()) return res.response } async ListPendingChannels(): Promise { + // console.log("Listing pending channels") const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata()) return res.response } async ListChannels(peerLookup = false): Promise { + // console.log("Listing channels") const res = await this.lightning.listChannels({ activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup }, DeadLineMetadata()) return res.response } async ListClosedChannels(): Promise { + // console.log("Listing closed channels") const res = await this.lightning.closedChannels({ abandoned: true, breach: true, @@ -153,6 +160,7 @@ export default class { } async Health(): Promise { + // console.log("Checking health") if (!this.ready) { throw new Error("not ready") } @@ -163,16 +171,17 @@ export default class { } RestartStreams() { - if (!this.ready) { + // console.log("Restarting streams") + if (!this.ready || this.abortController.signal.aborted) { return } this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds") const interval = setInterval(async () => { try { - await this.Health() + await this.unlockLnd() this.log("LND is back online") clearInterval(interval) - this.Warmup() + await this.Warmup() } catch (err) { this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds") } @@ -180,6 +189,7 @@ export default class { } async SubscribeChannelEvents() { + // console.log("Subscribing to channel events") const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal }) stream.responses.onMessage(async channel => { const channels = await this.ListChannels() @@ -194,6 +204,7 @@ export default class { } async SubscribeHtlcEvents() { + // console.log("Subscribing to htlc events") const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal }) stream.responses.onMessage(htlc => { this.htlcCb(htlc) @@ -207,20 +218,22 @@ export default class { } async SubscribeNewBlock() { + // console.log("Subscribing to new block") const { blockHeight } = await this.GetInfo() const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal }) stream.responses.onMessage(block => { this.newBlockCb(block.height) }) stream.responses.onError(error => { - this.log("Error with onchain tx stream") + this.log("Error with new block stream") }) stream.responses.onComplete(() => { - this.log("onchain tx stream closed") + this.log("new block stream closed") }) } SubscribeAddressPaid(): void { + // console.log("Subscribing to address paid") const stream = this.lightning.subscribeTransactions({ account: "", endHeight: 0, @@ -247,6 +260,7 @@ export default class { } SubscribeInvoicePaid(): void { + // console.log("Subscribing to invoice paid") const stream = this.lightning.subscribeInvoices({ settleIndex: BigInt(this.latestKnownSettleIndex), addIndex: 0n, @@ -257,17 +271,25 @@ export default class { this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), 'lnd') } }) + let restarted = false stream.responses.onError(error => { this.log("Error with invoice stream") + if (!restarted) { + restarted = true + this.RestartStreams() + } }) stream.responses.onComplete(() => { this.log("invoice stream closed") - this.RestartStreams() + if (!restarted) { + restarted = true + this.RestartStreams() + } }) } async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise { - + // console.log("Creating new address") let lndAddressType: AddressType switch (addressType) { case Types.AddressType.NESTED_PUBKEY_HASH: @@ -297,6 +319,7 @@ export default class { } async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise { + // console.log("Creating new invoice") if (useProvider) { console.log("using provider") const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) @@ -314,24 +337,19 @@ export default class { } async DecodeInvoice(paymentRequest: string): Promise { + // console.log("Decoding invoice") const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } } - GetFeeLimitAmount(amount: number): number { - return Math.ceil(amount * this.getSettings().lndSettings.feeRateLimit + this.getSettings().lndSettings.feeFixedLimit); - } - - GetMaxWithinLimit(amount: number): number { - return Math.max(0, Math.floor(amount * (1 - this.getSettings().lndSettings.feeRateLimit) - this.getSettings().lndSettings.feeFixedLimit)) - } - async ChannelBalance(): Promise<{ local: number, remote: number }> { + // console.log("Getting channel balance") const res = await this.lightning.channelBalance({}) const r = res.response 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, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise { + // console.log("Paying invoice") if (this.outgoingOpsLocked) { this.log("outgoing ops locked, rejecting payment request") throw new Error("lnd node is currently out of sync") @@ -339,7 +357,7 @@ export default class { if (useProvider) { const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from) const providerDst = this.liquidProvider.GetProviderDestination() - return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } + return { feeSat: res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } } await this.Health() try { @@ -378,6 +396,7 @@ export default class { } async EstimateChainFees(address: string, amount: number, targetConf: number): Promise { + // console.log("Estimating chain fees") await this.Health() const res = await this.lightning.estimateFee({ addrToAmount: { [address]: BigInt(amount) }, @@ -390,6 +409,7 @@ export default class { } async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise { + // console.log("Paying address") if (this.outgoingOpsLocked) { this.log("outgoing ops locked, rejecting payment request") throw new Error("lnd node is currently out of sync") @@ -409,16 +429,19 @@ export default class { } async GetTransactions(startHeight: number): Promise { + // console.log("Getting transactions") const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata()) return res.response } async GetChannelInfo(chanId: string) { + // console.log("Getting channel info") const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata()) return res.response } async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) { + // console.log("Updating channel policy") const split = chanPoint.split(':') const res = await this.lightning.updateChannelPolicy({ @@ -436,16 +459,19 @@ export default class { } async GetChannelBalance() { + // console.log("Getting channel balance") const res = await this.lightning.channelBalance({}, DeadLineMetadata()) return res.response } async GetWalletBalance() { + // console.log("Getting wallet balance") const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) return res.response } async GetTotalBalace() { + // console.log("Getting total balance") const walletBalance = await this.GetWalletBalance() const confirmedWalletBalance = Number(walletBalance.confirmedBalance) this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance) @@ -460,6 +486,7 @@ export default class { } async GetBalance(): Promise { // TODO: remove this + // console.log("Getting balance") const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response const { response } = await this.lightning.listChannels({ @@ -475,20 +502,24 @@ export default class { } async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise { + // console.log("Getting forwarding history") const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata()) return response } async GetAllPaidInvoices(max: number) { + // console.log("Getting all paid invoices") const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response } async GetAllPayments(max: number) { + // console.log("Getting all payments") const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }) return res.response } async GetPayment(paymentIndex: number) { + // console.log("Getting payment") if (paymentIndex === 0) { throw new Error("payment index starts from 1") } @@ -500,6 +531,7 @@ export default class { } async GetLatestPaymentIndex(from = 0) { + // console.log("Getting latest payment index") let indexOffset = BigInt(from) while (true) { const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset, maxPayments: 0n, reversed: false, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) @@ -511,6 +543,7 @@ export default class { } async ConnectPeer(addr: { pubkey: string, host: string }) { + // console.log("Connecting to peer") const res = await this.lightning.connectPeer({ addr, perm: true, @@ -520,6 +553,7 @@ export default class { } async GetPaymentFromHash(paymentHash: string): Promise { + // console.log("Getting payment from hash") const abortController = new AbortController() const stream = this.router.trackPaymentV2({ paymentHash: Buffer.from(paymentHash, 'hex'), @@ -541,11 +575,13 @@ export default class { } async GetTx(txid: string) { + // console.log("Getting transaction") const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata()) return res.response } async AddPeer(pub: string, host: string, port: number) { + // console.log("Adding peer") const res = await this.lightning.connectPeer({ addr: { pubkey: pub, @@ -558,11 +594,13 @@ export default class { } async ListPeers() { + // console.log("Listing peers") const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata()) return res.response } async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number, satsPerVByte: number): Promise { + // console.log("Opening channel") const abortController = new AbortController() const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte) const stream = this.lightning.openChannel(req, { abort: abortController.signal }) @@ -583,6 +621,7 @@ export default class { } async CloseChannel(fundingTx: string, outputIndex: number, force: boolean, satPerVByte: number): Promise { + // console.log("Closing channel") const stream = this.lightning.closeChannel({ deliveryAddress: "", force: force, diff --git a/src/services/lnd/lsp.ts b/src/services/lnd/lsp.ts index e734c62c..92643c2a 100644 --- a/src/services/lnd/lsp.ts +++ b/src/services/lnd/lsp.ts @@ -101,7 +101,7 @@ export class FlashsatsLSP extends LSP { return null } const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system') - const fees = +order.payment.fee_total_sat + res.network_fee + res.service_fee + const fees = +order.payment.fee_total_sat + res.service_fee this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees } @@ -177,7 +177,7 @@ export class OlympusLSP extends LSP { return null } const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system') - const fees = +order.payment.bolt11.fee_total_sat + res.network_fee + res.service_fee + const fees = +order.payment.bolt11.fee_total_sat + res.service_fee this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) return { orderId: order.order_id, invoice: order.payment.bolt11.invoice, totalSats: +order.payment.bolt11.order_total_sat, fees } } @@ -279,7 +279,7 @@ export class VoltageLSP extends LSP { return null } const res = await this.liquidityProvider.PayInvoice(proposalRes.jit_bolt11, decoded.numSatoshis, 'system') - const fees = feeSats + res.network_fee + res.service_fee + const fees = feeSats + res.service_fee this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) return { orderId: fee.id, invoice: proposalRes.jit_bolt11, totalSats: decoded.numSatoshis, fees } } diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 5f78570e..bf75246c 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -69,14 +69,15 @@ export default class { throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing } const nostrSettings = this.settings.getSettings().nostrRelaySettings + const { max, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) return { userId: ctx.user_id, balance: user.balance_sats, - max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true), + max_withdrawable: max, user_identifier: appUser.identifier, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: 0, + network_max_fee_fixed: networkFeeFixed, + service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), @@ -104,14 +105,9 @@ export default class { return this.applicationManager.PayAppUserInvoice(ctx.app_id, { amount: req.amount, invoice: req.invoice, - user_identifier: ctx.app_user_id - }) - } - async PayAddress(ctx: Types.UserContext, req: Types.PayInvoiceRequest): Promise { - return this.applicationManager.PayAppUserInvoice(ctx.app_id, { - amount: req.amount, - invoice: req.invoice, - user_identifier: ctx.app_user_id + user_identifier: ctx.app_user_id, + debit_npub: req.debit_npub, + expected_fees: req.expected_fees, }) } diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 76ae76db..b1befeb8 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -10,6 +10,7 @@ import { Application } from '../storage/entity/Application.js' import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' import SettingsManager from './settingsManager.js' +import { NostrSend, SendData, SendInitiator } from '../nostr/handler.js' const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds type NsecLinkingData = { @@ -17,7 +18,7 @@ type NsecLinkingData = { expiry: number } export default class { - + _nostrSend: NostrSend | null = null storage: Storage settings: SettingsManager paymentManager: PaymentManager @@ -33,6 +34,17 @@ export default class { this.StartLinkingTokenInterval() } + attachNostrSend = (nostrSend: NostrSend) => { + this._nostrSend = nostrSend + } + + nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { + if (!this._nostrSend) { + throw new Error("No nostrSend attached") + } + this._nostrSend(initiator, data, relays) + } + StartLinkingTokenInterval() { this.linkingTokenInterval = setInterval(() => { const now = Date.now(); @@ -47,12 +59,13 @@ export default class { }, 60 * 1000); // 1 minute } - async StartAppsServiceBeacon(publishBeacon: (app: Application) => void) { + async StartAppsServiceBeacon(publishBeacon: (app: Application, fees: Types.CumulativeFees) => void) { this.serviceBeaconInterval = setInterval(async () => { try { + const fees = this.paymentManager.GetFees() const apps = await this.storage.applicationStorage.GetApplications() apps.forEach(app => { - publishBeacon(app) + publishBeacon(app, fees) }) } catch (e) { this.log("error in beacon", (e as any).message) @@ -154,17 +167,17 @@ export default class { const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }) log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString }) - + const { max, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats) return { identifier: u.identifier, info: { userId: u.user.user_id, balance: u.user.balance_sats, - max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true), + max_withdrawable: max, user_identifier: u.identifier, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: 0, + network_max_fee_fixed: networkFeeFixed, + service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), @@ -172,7 +185,7 @@ export default class { bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl }, - max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) + max_withdrawable: max } } @@ -211,16 +224,16 @@ export default class { async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) - const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true) const nostrSettings = this.settings.getSettings().nostrRelaySettings + const { max, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) return { max_withdrawable: max, identifier: req.user_identifier, info: { userId: user.user.user_id, balance: user.user.balance_sats, - max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true), + max_withdrawable: max, user_identifier: user.identifier, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: 0, + network_max_fee_fixed: networkFeeFixed, + service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), @@ -233,11 +246,23 @@ export default class { async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) - const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app) + const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app, pendingOp => { + this.notifyAppUserPayment(appUser, pendingOp) + }) + this.notifyAppUserPayment(appUser, paid.operation) getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats") return paid } + notifyAppUserPayment = (appUser: ApplicationUser, op: Types.UserOperation) => { + const balance = appUser.user.balance_sats + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = + { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } + if (appUser.nostr_public_key) { // TODO - fix before support for http streams + this.nostrSend({ type: 'app', appId: appUser.application.app_id }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key }) + } + } + async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const fromUser = await this.storage.applicationStorage.GetApplicationUser(app, req.from_user_identifier) diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 126a469c..e1d85310 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -9,9 +9,11 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; import { UnsignedEvent } from 'nostr-tools'; import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; -import { debitAccessRulesToDebitRules, newNdebitResponse,debitRulesToDebitAccessRules, - nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, - frequencyRuleName,IntervalTypeToSeconds,unitToIntervalType } from "./debitTypes.js"; +import { + debitAccessRulesToDebitRules, newNdebitResponse, debitRulesToDebitAccessRules, + nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, + frequencyRuleName, IntervalTypeToSeconds, unitToIntervalType +} from "./debitTypes.js"; export class DebitManager { @@ -32,6 +34,7 @@ export class DebitManager { attachNostrSend = (nostrSend: NostrSend) => { this._nostrSend = nostrSend } + nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { if (!this._nostrSend) { throw new Error("No nostrSend attached") @@ -72,7 +75,7 @@ export class DebitManager { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: req.npub, id: req.request_id, appId: ctx.app_id }) return case Types.DebitResponse_response_type.INVOICE: - await this.paySingleInvoice(ctx, {invoice: req.response.invoice, npub: req.npub, request_id: req.request_id}) + await this.paySingleInvoice(ctx, { invoice: req.response.invoice, npub: req.npub, request_id: req.request_id }) return case Types.DebitResponse_response_type.AUTHORIZE: await this.handleAuthorization(ctx, req.response.authorize, { npub: req.npub, request_id: req.request_id }) @@ -82,14 +85,12 @@ export class DebitManager { } } - paySingleInvoice = async (ctx: Types.UserContext, {invoice,npub,request_id}:{invoice:string, npub:string, request_id:string}) => { + paySingleInvoice = async (ctx: Types.UserContext, { invoice, npub, request_id }: { invoice: string, npub: string, request_id: string }) => { try { this.logger("🔍 [DEBIT REQUEST] Paying single invoice") - const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) - const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) - const { op, payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) + const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } - this.notifyPaymentSuccess(appUser, debitRes, op, { appId: ctx.app_id, pub: npub, id: request_id }) + this.notifyPaymentSuccess(debitRes, { appId: ctx.app_id, pub: npub, id: request_id }) } catch (e: any) { this.logger("❌ [DEBIT REQUEST] Error in single invoice payment") this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) @@ -97,7 +98,7 @@ export class DebitManager { } } - handleAuthorization = async (ctx: Types.UserContext,debit:Types.DebitToAuthorize, {npub,request_id}:{ npub:string, request_id:string})=>{ + handleAuthorization = async (ctx: Types.UserContext, debit: Types.DebitToAuthorize, { npub, request_id }: { npub: string, request_id: string }) => { this.logger("🔍 [DEBIT REQUEST] Handling authorization", { npub, request_id, @@ -122,15 +123,15 @@ export class DebitManager { const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) this.validateAccessRules(access, app, appUser) this.logger("🔍 [DEBIT REQUEST] Sending debit payment") - const { op, payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) + const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } - this.notifyPaymentSuccess(appUser, debitRes, op, { appId: ctx.app_id, pub: npub, id: request_id }) + this.notifyPaymentSuccess(debitRes, { appId: ctx.app_id, pub: npub, id: request_id }) } catch (e: any) { this.logger("❌ [DEBIT REQUEST] Error in debit authorization") this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) throw e } - + } handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => { @@ -144,7 +145,7 @@ export class DebitManager { pointerdata }) const res = await this.payNdebitInvoice(event, pointerdata) - this.logger("🔍 [DEBIT REQUEST] Sending ",res.status," response") + this.logger("🔍 [DEBIT REQUEST] Sending ", res.status, " response") if (res.status === 'fail' || res.status === 'authOk') { const e = newNdebitResponse(JSON.stringify(res.debitRes), event) this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) @@ -155,11 +156,11 @@ export class DebitManager { this.handleAuthRequired(pointerdata, event, res) return } - const { op, debitRes } = res - this.notifyPaymentSuccess(appUser, debitRes, op, event) + const { debitRes } = res + this.notifyPaymentSuccess(debitRes, event) } - handleAuthRequired = (data:NdebitData, event: NostrEvent, res: AuthRequiredRes) => { + handleAuthRequired = (data: NdebitData, event: NostrEvent, res: AuthRequiredRes) => { if (!res.appUser.nostr_public_key) { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId }) return @@ -168,11 +169,7 @@ export class DebitManager { this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: res.appUser.nostr_public_key }) } - notifyPaymentSuccess = (appUser: ApplicationUser, debitRes: NdebitSuccess, op: Types.UserOperation, event: { pub: string, id: string, appId: string }) => { - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } - if (appUser.nostr_public_key) { // TODO - fix before support for http streams - this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key }) - } + notifyPaymentSuccess = (debitRes: NdebitSuccess, event: { pub: string, id: string, appId: string }) => { this.sendDebitResponse(debitRes, event) } @@ -286,15 +283,14 @@ export class DebitManager { } await this.validateAccessRules(authorization, app, appUser) this.logger("🔍 [DEBIT REQUEST] Sending requested debit payment") - const { op, payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) - return { status: 'invoicePaid', op, app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } + const { payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) + return { status: 'invoicePaid', app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } } sendDebitPayment = async (appId: string, appUserId: string, requestorPub: string, bolt11: string) => { const payment = await this.applicationManager.PayAppUserInvoice(appId, { amount: 0, invoice: bolt11, user_identifier: appUserId, debit_npub: requestorPub }) - await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee + payment.network_fee) - const op = this.newPaymentOperation(payment, bolt11) - return { payment, op } + await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee) + return { payment } } validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise => { @@ -325,21 +321,5 @@ export class DebitManager { } return true } - - newPaymentOperation = (payment: Types.PayInvoiceResponse, bolt11: string) => { - return { - amount: payment.amount_paid, - paidAtUnix: Math.floor(Date.now() / 1000), - inbound: false, - type: Types.UserOperationType.OUTGOING_INVOICE, - identifier: bolt11, - operationId: payment.operation_id, - network_fee: payment.network_fee, - service_fee: payment.service_fee, - confirmed: true, - tx_hash: "", - internal: payment.network_fee === 0 - } - } } diff --git a/src/services/main/debitTypes.ts b/src/services/main/debitTypes.ts index 83aca293..719de95f 100644 --- a/src/services/main/debitTypes.ts +++ b/src/services/main/debitTypes.ts @@ -1,9 +1,9 @@ import * as Types from "../../../proto/autogenerated/ts/types.js"; -import { DebitAccessRules } from '../storage/entity/DebitAccess.js'; +import { DebitAccessRules } from '../storage/entity/DebitAccess.js'; import { Application } from '../storage/entity/Application.js'; import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { UnsignedEvent } from 'nostr-tools'; -import { NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; +import { NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; export const expirationRuleName = 'expiration' export const frequencyRuleName = 'frequency' @@ -96,7 +96,7 @@ export const nofferErrors = { } export type AuthRequiredRes = { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, app: Application, appUser: ApplicationUser } export type HandleNdebitRes = { status: 'fail', debitRes: NdebitFailure } - | { status: 'invoicePaid', op: Types.UserOperation, app: Application, appUser: ApplicationUser, debitRes: NdebitSuccess } + | { status: 'invoicePaid', app: Application, appUser: ApplicationUser, debitRes: NdebitSuccess } | AuthRequiredRes | { status: 'authOk', debitRes: NdebitSuccess } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 143d25b5..9afa2ba5 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -79,7 +79,7 @@ export default class { lndSettings: settings.getSettings().lndSettings, lndNodeSettings: settings.getSettings().lndNodeSettings }) - this.lnd = new LND(lndGetSettings, this.liquidityProvider, this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb) + this.lnd = new LND(lndGetSettings, this.liquidityProvider, () => this.unlocker.Unlock(), this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb) this.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker) this.metricsManager = new MetricsManager(this.storage, this.lnd) @@ -103,8 +103,8 @@ export default class { } StartBeacons() { - this.applicationManager.StartAppsServiceBeacon(app => { - this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url }) + this.applicationManager.StartAppsServiceBeacon((app, fees) => { + this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, fees }) }) } @@ -115,6 +115,7 @@ export default class { this.offerManager.attachNostrSend(f) this.managementManager.attachNostrSend(f) this.utils.attachNostrSend(f) + this.applicationManager.attachNostrSend(f) //this.webRTC.attachNostrSend(f) } @@ -227,7 +228,7 @@ export default class { } log = getLogger({ appName: userAddress.linkedApplication.name }) const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id - let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment) + let fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment) if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) { fee = 0 } @@ -271,7 +272,7 @@ export default class { } log = getLogger({ appName: userInvoice.linkedApplication.name }) const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id - let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment) + let fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment) if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) { fee = 0 } @@ -373,8 +374,9 @@ export default class { getLogger({ appName: app.name })("cannot notify user, not a nostr user") return } - - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } + const balance = user.user.balance_sats + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = + { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } const j = JSON.stringify(message) this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key }) this.SendEncryptedNotification(app, user, op) @@ -396,7 +398,7 @@ export default class { }) } - async UpdateBeacon(app: Application, content: { type: 'service', name: string, avatarUrl?: string, nextRelay?: string }) { + async UpdateBeacon(app: Application, content: Types.BeaconData) { if (!app.nostr_public_key) { getLogger({ appName: app.name })("cannot update beacon, public key not set") return @@ -435,8 +437,9 @@ export default class { async ResetNostr() { const apps = await this.storage.applicationStorage.GetApplications() const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] + const fees = this.paymentManager.GetFees() for (const app of apps) { - await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay }) + await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay, fees }) } const defaultNames = ['wallet', 'wallet-test', this.settings.getSettings().serviceSettings.defaultAppName] @@ -453,7 +456,8 @@ export default class { apps: apps.map(a => ({ appId: a.app_id, name: a.name, privateKey: a.nostr_private_key || "", publicKey: a.nostr_public_key || "" })), relays: this.settings.getSettings().nostrRelaySettings.relays, maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength, - clients: [liquidityProviderInfo] + clients: [liquidityProviderInfo], + providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub } this.nostrReset(s) } diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index ac72975e..8e4d635c 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -50,8 +50,11 @@ export class LiquidityManager { } beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { - + const providerReady = this.liquidityProvider.IsReady() if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + if (!providerReady) { + throw new Error("cannot use liquidity provider, it is not ready") + } return 'provider' } @@ -63,7 +66,7 @@ export class LiquidityManager { if (remote > amount) { return 'lnd' } - const providerCanHandle = await this.liquidityProvider.CanProviderHandle({ action: 'receive', amount }) + const providerCanHandle = this.liquidityProvider.IsReady() if (!providerCanHandle) { return 'lnd' } @@ -78,20 +81,28 @@ export class LiquidityManager { } } - beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => { + beforeOutInvoicePayment = async (amount: number, localServiceFee: number): Promise<'lnd' | 'provider'> => { + const providerReady = this.liquidityProvider.IsReady() if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + if (!providerReady) { + throw new Error("cannot use liquidity provider, it is not ready") + } return 'provider' } - const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount }) - if (canHandle) { - return 'provider' + if (!providerReady) { + return 'lnd' } - return 'lnd' + const canHandle = await this.liquidityProvider.CanProviderPay(amount, localServiceFee) + if (!canHandle) { + return 'lnd' + } + return 'provider' } + afterOutInvoicePaid = async () => { } shouldDrainProvider = async () => { - const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() + const maxW = await this.liquidityProvider.GetMaxWithdrawable() const { remote } = await this.lnd.ChannelBalance() const drainable = Math.min(maxW, remote) if (drainable < 500) { @@ -121,7 +132,7 @@ export class LiquidityManager { try { const invoice = await this.lnd.NewInvoice(amt, "liqudity provider drain", defaultInvoiceExpiry, { from: 'system', useProvider: false }) const res = await this.liquidityProvider.PayInvoice(invoice.payRequest, amt, 'system') - const fees = res.network_fee + res.service_fee + const fees = res.service_fee this.feesPaid += fees this.updateLatestDrain(true, amt) } catch (err: any) { @@ -160,7 +171,7 @@ export class LiquidityManager { if (pendingChannels.pendingOpenChannels.length > 0) { return { shouldOpen: false } } - const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() + const maxW = await this.liquidityProvider.GetMaxWithdrawable() if (maxW < threshold) { return { shouldOpen: false } } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 10f9bfad..963610a6 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -1,15 +1,13 @@ import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js' import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js' import * as Types from '../../../proto/autogenerated/ts/types.js' -import { getLogger } from '../helpers/logger.js' +import { ERROR, getLogger } from '../helpers/logger.js' import { Utils } from '../helpers/utilsWrapper.js' import { NostrEvent, NostrSend } from '../nostr/handler.js' import { InvoicePaidCb } from '../lnd/settings.js' import Storage from '../storage/index.js' import SettingsManager from './settingsManager.js' import { LiquiditySettings } from './settings.js' -export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } - export type nostrCallback = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export class LiquidityProvider { getSettings: () => LiquiditySettings @@ -28,7 +26,11 @@ export class LiquidityProvider { queue: ((state: 'ready') => void)[] = [] utils: Utils pendingPayments: Record = {} + feesCache: Types.CumulativeFees | null = null + lastSeenBeacon = 0 + latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise + pendingPaymentsAck: Record = {} // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { this.utils = utils @@ -68,7 +70,8 @@ export class LiquidityProvider { } IsReady = () => { - return this.ready && !this.getSettings().disableLiquidityProvider + const seenInPast2Minutes = Date.now() - this.lastSeenBeacon < 1000 * 60 * 2 + return this.ready && !this.getSettings().disableLiquidityProvider && seenInPast2Minutes } AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { @@ -94,6 +97,7 @@ export class LiquidityProvider { return } this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0) + this.lastSeenBeacon = Date.now() this.ready = true this.queue.forEach(q => q('ready')) this.log("subbing to user operations") @@ -107,6 +111,10 @@ export class LiquidityProvider { try { await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') this.incrementProviderBalance(res.operation.amount) + this.latestReceivedBalance = res.latest_balance + if (!res.operation.inbound && !res.operation.confirmed) { + delete this.pendingPaymentsAck[res.operation.identifier] + } } catch (err: any) { this.log("error processing incoming invoice", err.message) } @@ -122,62 +130,77 @@ export class LiquidityProvider { } return res } + this.feesCache = { + networkFeeFixed: res.network_max_fee_fixed, + serviceFeeBps: res.service_fee_bps + } + this.latestReceivedBalance = res.balance this.utils.stateBundler.AddBalancePoint('providerBalance', res.balance) this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable) return res } - GetLatestMaxWithdrawable = async () => { - if (!this.IsReady()) { - return 0 + GetFees = () => { + if (!this.feesCache) { + throw new Error("fees not cached") } - const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.max_withdrawable + return this.feesCache } - GetLatestBalance = async () => { + GetMaxWithdrawable = () => { + if (!this.IsReady() || !this.feesCache) { + return 0 + } + const balance = this.latestReceivedBalance + const { networkFeeFixed, serviceFeeBps } = this.feesCache + const div = 1 + (serviceFeeBps / 10000) + const maxWithoutFixed = Math.floor(balance / div) + const fee = balance - maxWithoutFixed + return balance - Math.max(fee, networkFeeFixed) + } + + GetLatestBalance = () => { if (!this.IsReady()) { return 0 } - const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.balance + return this.latestReceivedBalance } GetPendingBalance = async () => { return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0) } - CalculateExpectedFeeLimit = (amount: number, info: Types.UserInfo) => { - const serviceFeeRate = info.service_fee_bps / 10000 + GetServiceFee = (amount: number, f?: Types.CumulativeFees) => { + const fees = f ? f : this.GetFees() + const serviceFeeRate = fees.serviceFeeBps / 10000 const serviceFee = Math.ceil(serviceFeeRate * amount) - const networkMaxFeeRate = info.network_max_fee_bps / 10000 - const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + info.network_max_fee_fixed) - return serviceFee + networkFeeLimit + return Math.max(serviceFee, fees.networkFeeFixed) } - CanProviderHandle = async (req: LiquidityRequest) => { + CanProviderPay = async (amount: number, localServiceFee: number): Promise => { if (!this.IsReady()) { + this.log("provider is not ready") return false } - const maxW = await this.GetLatestMaxWithdrawable() - if (req.action === 'spend') { - return maxW > req.amount + const maxW = this.GetMaxWithdrawable() + if (maxW < amount) { + this.log("provider does not have enough funds to pay the invoice") + return false } + + const providerServiceFee = this.GetServiceFee(amount) + if (localServiceFee < providerServiceFee) { + this.log(`local service fee ${localServiceFee} is less than the provider's service fee ${providerServiceFee}`) + return false + } + return true } AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { try { if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet or disabled") + throw new Error("liquidity provider is not ready yet, disabled or unreachable") } const res = await this.client.NewInvoice({ amountSats: amount, memo, expiry }) if (res.status === 'ERROR') { @@ -196,20 +219,29 @@ export class LiquidityProvider { PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => { try { if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet or disabled") + throw new Error("liquidity provider is not ready yet, disabled or unreachable") } - const userInfo = await this.GetUserState() - if (userInfo.status === 'ERROR') { - throw new Error(userInfo.reason) - } - this.pendingPayments[invoice] = decodedAmount + this.CalculateExpectedFeeLimit(decodedAmount, userInfo) - const res = await this.client.PayInvoice({ invoice, amount: 0 }) + const fees = this.GetFees() + const providerServiceFee = this.GetServiceFee(decodedAmount, fees) + this.pendingPayments[invoice] = decodedAmount + providerServiceFee + const timeout = setTimeout(() => { + if (!this.pendingPaymentsAck[invoice]) { + return + } + this.log("10 seconds passed without a payment ack, locking provider until the next beacon") + this.lastSeenBeacon = 0 + }, 1000 * 10) + this.pendingPaymentsAck[invoice] = true + const res = await this.client.PayInvoice({ invoice, amount: 0, expected_fees: fees }) + delete this.pendingPaymentsAck[invoice] + clearTimeout(timeout) if (res.status === 'ERROR') { this.log("error paying invoice", res.reason) throw new Error(res.reason) } - const totalPaid = res.amount_paid + res.network_fee + res.service_fee + const totalPaid = res.amount_paid + res.service_fee this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) + this.latestReceivedBalance = res.latest_balance this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) return res } catch (err) { @@ -221,7 +253,7 @@ export class LiquidityProvider { GetPaymentState = async (invoice: string) => { if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet or disabled") + throw new Error("liquidity provider is not ready yet, disabled or unreachable") } const res = await this.client.GetPaymentState({ invoice }) if (res.status === 'ERROR') { @@ -233,7 +265,7 @@ export class LiquidityProvider { GetOperations = async () => { if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet or disabled") + throw new Error("liquidity provider is not ready yet, disabled or unreachable") } const res = await this.client.GetUserOperations({ latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, @@ -265,7 +297,26 @@ export class LiquidityProvider { setSetIfConfigured = () => { if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) { this.configured = true - this.log("configured to send to ", this.pubDestination) + this.log("configured to send to ") + } + } + onBeaconEvent = async (beaconData: { content: string, pub: string }) => { + if (beaconData.pub !== this.pubDestination) { + this.log(ERROR, "got beacon from invalid pub", beaconData.pub, this.pubDestination) + return + } + const beacon = JSON.parse(beaconData.content) as Types.BeaconData + const err = Types.BeaconDataValidate(beacon) + if (err) { + this.log(ERROR, "error validating beacon data", err.message) + return + } + if (beacon.type !== 'service') { + this.log(ERROR, "got beacon from invalid type", beacon.type) + return + } + if (beacon.fees) { + this.feesCache = beacon.fees } } diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 77740d57..b315b170 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -37,6 +37,8 @@ interface UserOperationInfo { }; internal?: boolean; } + + export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment } const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]` const defaultLnAddressMetadata = (text: string, id: string) => `[["text/plain", "${text}"],["text/identifier", "${id}"]]` @@ -88,7 +90,7 @@ export default class { checkPendingProviderPayment = async (log: PubLogger, p: UserInvoicePayment) => { const state = await this.lnd.liquidProvider.GetPaymentState(p.invoice) if (state.paid_at_unix < 0) { - const fullAmount = p.paid_amount + p.service_fees + p.routing_fees + const fullAmount = p.paid_amount + p.service_fees log("found a failed provider payment, refunding", fullAmount, "sats to user", p.user.user_id) await this.storage.StartTransaction(async tx => { await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx) @@ -97,18 +99,16 @@ export default class { return } else if (state.paid_at_unix > 0) { log("provider payment succeeded", p.serial_id, "updating payment info") - const routingFeeLimit = p.routing_fees const serviceFee = p.service_fees - const actualFee = state.network_fee + state.service_fee - await this.storage.StartTransaction(async tx => { - if (routingFeeLimit - actualFee > 0) { - this.log("refund pending provider payment routing fee", routingFeeLimit, actualFee, "sats") - await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) - } - await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, actualFee, p.service_fees, true, undefined, tx) - }, "pending provider payment success after restart") - if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees") + const networkFee = state.service_fee + await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, serviceFee, true) + const remainingFee = serviceFee - networkFee + if (remainingFee < 0) { + this.log("WARNING: provider fee was higher than expected,", remainingFee, "were lost") + } + + if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && remainingFee > 0) { + await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees") } const user = await this.storage.userStorage.GetUser(p.user.user_id) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId: p.user.user_id, appId: p.linkedApplication?.app_id || "", appUserId: "", balance: user.balance_sats, data: p.invoice, amount: p.paid_amount }) @@ -124,7 +124,6 @@ export default class { log(ERROR, "lnd payment not found for pending payment hash ", decoded.paymentHash) return } - switch (payment.status) { case Payment_PaymentStatus.UNKNOWN: log("pending payment in unknown state", p.serial_id, "no action will be performed") @@ -134,24 +133,22 @@ export default class { return case Payment_PaymentStatus.SUCCEEDED: log("pending payment succeeded", p.serial_id, "updating payment info") - const routingFeeLimit = p.routing_fees const serviceFee = p.service_fees - const actualFee = Number(payment.feeSat) - await this.storage.StartTransaction(async tx => { - if (routingFeeLimit - actualFee > 0) { - this.log("refund pending payment routing fee", routingFeeLimit, actualFee, "sats") - await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) - } - await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, actualFee, p.service_fees, true, undefined, tx) - }, "pending payment success after restart") - if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees") + const networkFee = Number(payment.feeSat) + + await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, p.service_fees, true, undefined) + const remainingFee = serviceFee - networkFee + if (remainingFee < 0) { // should not be possible beacuse of the fee limit + this.log("WARNING: lnd fee was higher than expected,", remainingFee, "were lost") + } + if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && remainingFee > 0) { + await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees") } const user = await this.storage.userStorage.GetUser(p.user.user_id) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId: p.user.user_id, appId: p.linkedApplication?.app_id || "", appUserId: "", balance: user.balance_sats, data: p.invoice, amount: p.paid_amount }) return case Payment_PaymentStatus.FAILED: - const fullAmount = p.paid_amount + p.service_fees + p.routing_fees + const fullAmount = p.paid_amount + p.service_fees log("found a failed pending payment, refunding", fullAmount, "sats to user", p.user.user_id) await this.storage.StartTransaction(async tx => { await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx) @@ -163,23 +160,40 @@ export default class { } } - getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { + getReceiveServiceFee = (action: Types.UserOperationType, amount: number, appUser: boolean): number => { switch (action) { case Types.UserOperationType.INCOMING_TX: return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount) - case Types.UserOperationType.OUTGOING_TX: - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingTxFee * amount) case Types.UserOperationType.INCOMING_INVOICE: if (appUser) { return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount) } return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount) - case Types.UserOperationType.OUTGOING_INVOICE: + case Types.UserOperationType.INCOMING_USER_TO_USER: if (appUser) { - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount) + return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) } - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount) - case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER: + return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount) + default: + throw new Error("Unknown receive action type") + } + } + + getInvoicePaymentServiceFee = (amount: number, appUser: boolean): number => { + if (appUser) { + return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount) + } + return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount) + } + + getSendServiceFee = (action: Types.UserOperationType, amount: number, appUser: boolean): number => { + switch (action) { + case Types.UserOperationType.OUTGOING_TX: + throw new Error("Sending a transaction is not supported") + case Types.UserOperationType.OUTGOING_INVOICE: + const fee = this.getInvoicePaymentServiceFee(amount, appUser) + return Math.max(fee, this.settings.getSettings().lndSettings.feeFixedLimit) + case Types.UserOperationType.OUTGOING_USER_TO_USER: if (appUser) { return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) } @@ -235,14 +249,19 @@ export default class { } } - GetMaxPayableInvoice(balance: number, appUser: boolean): number { - let maxWithinServiceFee = 0 - if (appUser) { - maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) - } else { - maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee))) - } - return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) + GetFees = (): Types.CumulativeFees => { + const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings + const { feeFixedLimit } = this.settings.getSettings().lndSettings + return { networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps } + } + + GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } { + const { networkFeeFixed, serviceFeeBps } = this.GetFees() + const div = 1 + (serviceFeeBps / 10000) + const maxWithoutFixed = Math.floor(balance / div) + const fee = balance - maxWithoutFixed + const max = balance - Math.max(fee, networkFeeFixed) + return { max, networkFeeFixed, serviceFeeBps } } async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise { const decoded = await this.lnd.DecodeInvoice(req.invoice) @@ -251,12 +270,20 @@ export default class { } } - async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, swapOperationId?: string): Promise { + async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, optionals: { swapOperationId?: string, ack?: (op: Types.UserOperation) => void } = {}): Promise { await this.watchDog.PaymentRequested() const maybeBanned = await this.storage.userStorage.GetUser(userId) if (maybeBanned.locked) { throw new Error("user is banned, cannot send payment") } + if (req.expected_fees) { + const { networkFeeFixed, serviceFeeBps } = req.expected_fees + const serviceFixed = this.settings.getSettings().lndSettings.feeFixedLimit + const serviceBps = this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps + if (serviceFixed !== networkFeeFixed || serviceBps !== serviceFeeBps) { + throw new Error("fees do not match the expected fees") + } + } 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") @@ -266,7 +293,7 @@ export default class { } const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) const isAppUserPayment = userId !== linkedApplication.owner.user_id - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) + const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice) if (internalInvoice && internalInvoice.paid_at_unix > 0) { throw new Error("this invoice was already paid") @@ -279,23 +306,28 @@ export default class { if (internalInvoice) { paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub) } else { - paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, { debitNpub: req.debit_npub, swapOperationId }) + paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, { ...optionals, debitNpub: req.debit_npub }) } - if (isAppUserPayment && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee, "fees") + const feeDiff = serviceFee - paymentInfo.networkFee + if (isAppUserPayment && feeDiff > 0) { + await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, feeDiff, "fees") } const user = await this.storage.userStorage.GetUser(userId) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId, appId: linkedApplication.app_id, appUserId: "", balance: user.balance_sats, data: req.invoice, amount: payAmount }) + const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}` + const operation = this.newInvoicePaymentOperation({ invoice: req.invoice, opId, amount: paymentInfo.amtPaid, networkFee: paymentInfo.networkFee, serviceFee: serviceFee, confirmed: true }) return { preimage: paymentInfo.preimage, amount_paid: paymentInfo.amtPaid, - operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, - network_fee: paymentInfo.networkFee, + operation_id: opId, + network_fee: 0, service_fee: serviceFee, + latest_balance: user.balance_sats, + operation } } - async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, optionals: { debitNpub?: string, swapOperationId?: string } = {}) { + async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, optionals: { debitNpub?: string, swapOperationId?: string, ack?: (op: Types.UserOperation) => void } = {}) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) { throw new Error("something went wrong sending payment, please try again later") @@ -309,30 +341,32 @@ export default class { } throw new Error("payment already in progress") } + const { amountForLnd, payAmount, serviceFee } = amounts const totalAmountToDecrement = payAmount + serviceFee - const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) - const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) + const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee) const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined const pendingPayment = await this.storage.StartTransaction(async tx => { - await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx) - return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, optionals) + await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, invoice, tx) + return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: 0 }, linkedApplication, provider, tx, optionals) }, "payment started") this.log("ready to pay") + const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${pendingPayment.serial_id}` + const op = this.newInvoicePaymentOperation({ invoice, opId, amount: payAmount, networkFee: 0, serviceFee: serviceFee, confirmed: false }) + optionals.ack?.(op) try { - const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { + const payment = await this.lnd.PayInvoice(invoice, amountForLnd, serviceFee, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) }) - if (routingFeeLimit - payment.feeSat > 0) { - this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats") - await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice) - } - await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerDst) + const feeDiff = serviceFee - payment.feeSat + if (feeDiff < 0) { // should not happen to lnd beacuse of the fee limit, culd happen to provider if the fee used to calculate the provider fee are out of date + this.log("WARNING: network fee was higher than expected,", feeDiff, "were lost by", use === 'provider' ? "provider" : "lnd") + } return { preimage: payment.paymentPreimage, amtPaid: payment.valueSat, networkFee: payment.feeSat, serialId: pendingPayment.serial_id } } catch (err) { - await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, "payment_refund:" + invoice) + await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "payment_refund:" + invoice) await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, 0, 0, false) throw err } @@ -353,10 +387,8 @@ export default class { } catch (err) { await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "internal_payment_refund:" + internalInvoice.invoice) this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', payAmount, { used: 'internal', from: 'user' }, linkedApplication.app_id) - throw err } - } async GetTransactionSwapQuote(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise { @@ -375,8 +407,8 @@ export default class { const swapFee = decoded.numSatoshis - chainTotal const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const isAppUserPayment = ctx.user_id !== app.owner.user_id - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isAppUserPayment) - const routingFeeLimit = this.lnd.GetFeeLimitAmount(decoded.numSatoshis) + const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isAppUserPayment) + // const routingFeeLimit = this.lnd.GetFeeLimitAmount(decoded.numSatoshis) const newSwap = await this.storage.paymentStorage.AddTransactionSwap({ app_user_id: ctx.app_user_id, swap_quote_id: res.createdResponse.id, @@ -400,7 +432,6 @@ export default class { transaction_amount_sats: req.transaction_amount_sats, chain_fee_sats: minerFee, service_fee_sats: serviceFee, - routing_fee_reserve_sats: routingFeeLimit } } @@ -457,7 +488,7 @@ export default class { }) let payment: Types.PayInvoiceResponse try { - payment = await this.PayInvoice(ctx.user_id, { amount: 0, invoice: txSwap.invoice }, app, req.swap_operation_id) + payment = await this.PayInvoice(ctx.user_id, { amount: 0, invoice: txSwap.invoice }, app, { swapOperationId: req.swap_operation_id }) if (!swapResult.ok) { this.log("invoice payment successful, but swap failed") await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, swapResult.error) @@ -475,7 +506,7 @@ export default class { } throw err } - const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats + payment.network_fee + const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats // + payment.network_fee return { txId: swapResult.txId, network_fee: networkFeesTotal, @@ -491,7 +522,7 @@ export default class { } 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) + const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) const isAppUserPayment = ctx.user_id !== app.owner.user_id const txId = crypto.randomBytes(32).toString("hex") @@ -771,6 +802,23 @@ export default class { } } + newInvoicePaymentOperation = (opInfo: { invoice: string, opId: string, amount: number, networkFee: number, serviceFee: number, confirmed: boolean }): Types.UserOperation => { + const { invoice, opId, amount, networkFee, serviceFee, confirmed } = opInfo + return { + amount: amount, + paidAtUnix: Math.floor(Date.now() / 1000), + inbound: false, + type: Types.UserOperationType.OUTGOING_INVOICE, + identifier: invoice, + operationId: opId, + network_fee: networkFee, + service_fee: serviceFee, + confirmed, + tx_hash: "", + internal: networkFee === 0 + } + } + async GetPaymentState(userId: string, req: Types.GetPaymentStateRequest): Promise { const user = await this.storage.userStorage.GetUser(userId) if (user.locked) { @@ -783,7 +831,7 @@ export default class { return { paid_at_unix: invoice.paid_at_unix, amount: invoice.paid_amount, - network_fee: invoice.routing_fees, + network_fee: 0, service_fee: invoice.service_fees, } } @@ -822,7 +870,7 @@ export default class { throw new Error("not enough balance to send payment") } const isAppUserPayment = fromUser.user_id !== linkedApplication.owner.user_id - let fee = this.getServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) + let fee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) const toDecrement = amount + fee const paymentEntry = await this.storage.paymentStorage.AddPendingUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication, tx) await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, toDecrement, `${toUserId}:${paymentEntry.serial_id}`, tx) diff --git a/src/services/main/rugPullTracker.ts b/src/services/main/rugPullTracker.ts index ab5889a5..8feecb30 100644 --- a/src/services/main/rugPullTracker.ts +++ b/src/services/main/rugPullTracker.ts @@ -27,7 +27,7 @@ export class RugPullTracker { const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst) const ready = this.liquidProvider.IsReady() if (ready) { - const balance = await this.liquidProvider.GetLatestBalance() + const balance = this.liquidProvider.GetLatestBalance() const pendingBalance = await this.liquidProvider.GetPendingBalance() const trackedBalance = balance + pendingBalance if (!providerTracker) { diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index 2e557b81..4e85db82 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -78,9 +78,7 @@ export type LndNodeSettings = { } export type LndSettings = { lndLogDir: string - feeRateLimit: number feeFixedLimit: number - feeRateBps: number mockLnd: boolean } @@ -104,12 +102,9 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): LndSettings => { - const feeRateBps: number = chooseEnvInt('OUTBOUND_MAX_FEE_BPS', dbEnv, 60, addToDb) return { lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb), - feeRateBps: feeRateBps, - feeRateLimit: feeRateBps / 10000, - feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 100, addToDb), + feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb), mockLnd: false } } diff --git a/src/services/main/settingsManager.ts b/src/services/main/settingsManager.ts index bf30bc63..e2e2d001 100644 --- a/src/services/main/settingsManager.ts +++ b/src/services/main/settingsManager.ts @@ -46,7 +46,7 @@ export default class SettingsManager { toAdd[key] = value } this.settings = this.loadEnvs(dbSettings, addToDb) - this.log("adding", toAdd.length, "settings to db") + this.log("adding", Object.keys(toAdd).length, "settings to db") for (const key in toAdd) { await this.storage.settingsStorage.setDbEnvIFNeeded(key, toAdd[key]) } diff --git a/src/services/main/unlocker.ts b/src/services/main/unlocker.ts index 68bb7468..f129834f 100644 --- a/src/services/main/unlocker.ts +++ b/src/services/main/unlocker.ts @@ -301,12 +301,12 @@ export class Unlocker { GetWalletPassword = () => { const path = this.settings.getStorageSettings().walletPasswordPath - let password = Buffer.alloc(0) + let password: Buffer | null = null try { password = fs.readFileSync(path) } catch { } - if (password.length === 0) { + if (!password || password.length === 0) { this.log("no wallet password configured, using wallet secret") const secret = this.GetWalletSecret(false) if (secret === "") { diff --git a/src/services/main/watchdog.ts b/src/services/main/watchdog.ts index e84c929a..97e5bc91 100644 --- a/src/services/main/watchdog.ts +++ b/src/services/main/watchdog.ts @@ -173,7 +173,12 @@ export class Watchdog { StartCheck = async () => { this.latestCheckStart = Date.now() - await this.updateAccumulatedHtlcFees() + try { + await this.updateAccumulatedHtlcFees() + } catch (err: any) { + this.log("Error updating accumulated htlc fees", err.message || err) + return + } const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance) const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance() diff --git a/src/services/metrics/index.ts b/src/services/metrics/index.ts index 9774d4c1..1e1b30c7 100644 --- a/src/services/metrics/index.ts +++ b/src/services/metrics/index.ts @@ -241,12 +241,12 @@ export default class Handler { ops.outgoingInvoices.forEach(i => { if (req.include_operations) operations.push({ type: Types.UserOperationType.OUTGOING_INVOICE, amount: i.paid_amount, inbound: false, paidAtUnix: i.paid_at_unix, confirmed: true, service_fee: i.service_fees, network_fee: i.routing_fees, identifier: "", operationId: "", tx_hash: "", internal: i.internal }) totalSpent += i.paid_amount - feesInRange += i.service_fees + feesInRange += (i.service_fees - i.routing_fees) }) ops.outgoingTransactions.forEach(tx => { if (req.include_operations) operations.push({ type: Types.UserOperationType.OUTGOING_TX, amount: tx.paid_amount, inbound: false, paidAtUnix: tx.paid_at_unix, confirmed: tx.confs > 1, service_fee: tx.service_fees, network_fee: tx.chain_fees, identifier: "", operationId: "", tx_hash: tx.tx_hash, internal: tx.internal }) totalSpent += tx.paid_amount - feesInRange += tx.service_fees + feesInRange += (tx.service_fees - tx.chain_fees) }) ops.userToUser.forEach(op => { diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index e054d8de..06c83c08 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -2,7 +2,7 @@ import WebSocket from 'ws' Object.assign(global, { WebSocket: WebSocket }); import crypto from 'crypto' -import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44 } from 'nostr-tools' +import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools' import { ERROR, getLogger } from '../helpers/logger.js' import { nip19 } from 'nostr-tools' import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' @@ -26,6 +26,7 @@ export type NostrSettings = { relays: string[] clients: ClientInfo[] maxEventContentLength: number + providerDestinationPub: string } export type NostrEvent = { @@ -69,9 +70,14 @@ type ProcessMetricsResponse = { type: 'processMetrics' metrics: ProcessMetrics } +type BeaconResponse = { + type: 'beacon' + content: string + pub: string +} export type ChildProcessRequest = SettingsRequest | SendRequest | PingRequest -export type ChildProcessResponse = ReadyResponse | EventResponse | ProcessMetricsResponse | PingResponse +export type ChildProcessResponse = ReadyResponse | EventResponse | ProcessMetricsResponse | PingResponse | BeaconResponse const send = (message: ChildProcessResponse) => { if (process.send) { process.send(message, undefined, undefined, err => { @@ -218,18 +224,28 @@ export default class Handler { appIds: appIds, listeningForPubkeys: appIds }) - - return relay.subscribe([ + const subs: Filter[] = [ { since: Math.ceil(Date.now() / 1000), kinds: supportedKinds, '#p': appIds, } - ], { + ] + if (this.settings.providerDestinationPub) { + subs.push({ + kinds: [30078], '#d': ['Lightning.Pub'], + authors: [this.settings.providerDestinationPub] + }) + } + return relay.subscribe(subs, { oneose: () => { this.log("up to date with nostr events") }, onevent: async (e) => { + if (e.kind === 30078 && e.pubkey === this.settings.providerDestinationPub) { + send({ type: 'beacon', content: e.content, pub: e.pubkey }) + return + } if (!supportedKinds.includes(e.kind) || !e.pubkey) { return } diff --git a/src/services/nostr/index.ts b/src/services/nostr/index.ts index 50fdf61b..68773ee5 100644 --- a/src/services/nostr/index.ts +++ b/src/services/nostr/index.ts @@ -3,7 +3,7 @@ import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, S import { Utils } from '../helpers/utilsWrapper.js' import { getLogger, ERROR } from '../helpers/logger.js' type EventCallback = (event: NostrEvent) => void - +type BeaconCallback = (beacon: { content: string, pub: string }) => void @@ -13,7 +13,7 @@ export default class NostrSubprocess { utils: Utils awaitingPongs: (() => void)[] = [] log = getLogger({}) - constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback) { + constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) { this.utils = utils this.childProcess = fork("./build/src/services/nostr/handler") this.childProcess.on("error", (error) => { @@ -43,6 +43,9 @@ export default class NostrSubprocess { this.awaitingPongs.forEach(resolve => resolve()) this.awaitingPongs = [] break + case 'beacon': + beaconCallback({ content: message.content, pub: message.pub }) + break default: console.error("unknown nostr event response", message) break; diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 5e75f367..0ff4410f 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -422,8 +422,8 @@ export default class { async VerifyDbEvent(e: LoggedEvent) { switch (e.type) { - case "new_invoice": - return orFail(this.dbs.FindOne('UserReceivingInvoice', { where: { invoice: e.data, user: { user_id: e.userId } } })) + /* case "new_invoice": + return orFail(this.dbs.FindOne('UserReceivingInvoice', { where: { invoice: e.data, user: { user_id: e.userId } } })) */ case 'new_address': return orFail(this.dbs.FindOne('UserReceivingAddress', { where: { address: e.data, user: { user_id: e.userId } } })) case 'invoice_paid': diff --git a/src/services/storage/tlv/tlvFilesStorageFactory.ts b/src/services/storage/tlv/tlvFilesStorageFactory.ts index 6f8bbb51..625f21bd 100644 --- a/src/services/storage/tlv/tlvFilesStorageFactory.ts +++ b/src/services/storage/tlv/tlvFilesStorageFactory.ts @@ -19,7 +19,7 @@ export class TlvStorageFactory extends EventEmitter { private debug: boolean = false; private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') } private allowResetMetricsStorages: boolean - log = getLogger({component: 'TlvStorageFactory'}) + log = getLogger({ component: 'TlvStorageFactory' }) constructor(allowResetMetricsStorages: boolean) { super(); this.allowResetMetricsStorages = allowResetMetricsStorages @@ -134,10 +134,15 @@ export class TlvStorageFactory extends EventEmitter { return this.handleOp(op) } - ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise { + async ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise { const opId = Math.random().toString() const op: ProcessMetricsTlvOperation = { type: 'processMetrics', opId, metrics, processName } - return this.handleOp(op) + try { + return this.handleOp(op) + } catch (error: any) { + this.log(ERROR, 'Error processing metrics', error.message) + } + return } diff --git a/src/tests/.env.test b/src/tests/.env.test index 8d0e92e2..dd0221bc 100644 --- a/src/tests/.env.test +++ b/src/tests/.env.test @@ -5,7 +5,7 @@ DATABASE_FILE=db.sqlite JWT_SECRET=bigsecrethere ALLOW_BALANCE_MIGRATION=true OUTBOUND_MAX_FEE_BPS=60 -OUTBOUND_MAX_FEE_EXTRA_SATS=100 +OUTBOUND_MAX_FEE_EXTRA_SATS=10 INCOMING_CHAIN_FEE_ROOT_BPS=0 OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets INCOMING_INVOICE_FEE_ROOT_BPS=0 diff --git a/src/tests/externalPayment.spec.ts b/src/tests/externalPayment.spec.ts index a92d0054..7a03e828 100644 --- a/src/tests/externalPayment.spec.ts +++ b/src/tests/externalPayment.spec.ts @@ -24,11 +24,10 @@ const testSuccessfulExternalPayment = async (T: TestBase) => { T.d("paid 500 sats invoice from user1") const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(u1.balance_sats).to.be.equal(1496) - T.d("user1 balance is now 1496 (2000 - (500 + 3 fee + 1 routing))") - expect(owner.balance_sats).to.be.equal(3) - T.d("app balance is 3 sats") - + expect(u1.balance_sats).to.be.equal(1490) + T.d("user1 balance is now 1490 (2000 - (500 + 10fee))") + expect(owner.balance_sats).to.be.equal(9) + T.d("app balance is 9 sats") } const testFailedExternalPayment = async (T: TestBase) => { @@ -41,11 +40,11 @@ const testFailedExternalPayment = async (T: TestBase) => { await expectThrowsAsync(T.main.paymentManager.PayInvoice(T.user1.userId, { invoice: invoice.payRequest, amount: 0 }, application), "not enough balance to decrement") T.d("payment failed as expected, with the expected error message") const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) - expect(u1.balance_sats).to.be.equal(1496) - T.d("user1 balance is still 1496") + expect(u1.balance_sats).to.be.equal(1490) + T.d("user1 balance is still 1490") const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(owner.balance_sats).to.be.equal(3) - T.d("app balance is still 3 sats") + expect(owner.balance_sats).to.be.equal(9) + T.d("app balance is still 9 sats") } const testSuccesfulReceivedExternalChainPayment = async (T: TestBase) => { diff --git a/src/tests/internalPayment.spec.ts b/src/tests/internalPayment.spec.ts index f94bb994..80b35675 100644 --- a/src/tests/internalPayment.spec.ts +++ b/src/tests/internalPayment.spec.ts @@ -23,10 +23,10 @@ const testSuccessfulInternalPayment = async (T: TestBase) => { const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) expect(u2.balance_sats).to.be.equal(1000) T.d("user2 balance is 1000") - expect(u1.balance_sats).to.be.equal(994) - T.d("user1 balance is 994 cuz he paid 6 sats fee") - expect(owner.balance_sats).to.be.equal(6) - T.d("app balance is 6 sats") + expect(u1.balance_sats).to.be.equal(990) + T.d("user1 balance is 990 cuz he paid 10 sats fee") + expect(owner.balance_sats).to.be.equal(10) + T.d("app balance is 10 sats") } const testFailedInternalPayment = async (T: TestBase) => { diff --git a/src/tests/liquidityProvider.spec.ts b/src/tests/liquidityProvider.spec.ts index 30b9f37d..ad3685b7 100644 --- a/src/tests/liquidityProvider.spec.ts +++ b/src/tests/liquidityProvider.spec.ts @@ -21,30 +21,30 @@ export default async (T: TestBase) => { const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => { T.d("starting testInboundPaymentFromProvider") - const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" }) + const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 3000, memo: "liquidityTest" }) - await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100, 2000, { from: 'system', useProvider: false }) + await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100, 3000, { from: 'system', useProvider: false }) await new Promise((resolve) => setTimeout(resolve, 200)) const userBalance = await bootstrapped.appUserManager.GetUserInfo({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }) - T.expect(userBalance.balance).to.equal(2000) - T.d("user balance is 2000") + T.expect(userBalance.balance).to.equal(3000) + T.d("user balance is 3000") const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() - T.expect(providerBalance).to.equal(2000) - T.d("provider balance is 2000") + T.expect(providerBalance).to.equal(3000) + T.d("provider balance is 3000") T.d("testInboundPaymentFromProvider done") } const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => { T.d("starting testOutboundPaymentFromProvider") - const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60, { from: 'system', useProvider: false }) + const invoice = await T.externalAccessToOtherLnd.NewInvoice(2000, "", 60 * 60, { from: 'system', useProvider: false }) const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier } const res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 }) const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx) - T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2) + T.expect(userBalance.balance).to.equal(988) // 3000 - (2000 + 12) const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() - T.expect(providerBalance).to.equal(992) // 2000 - (1000 + 6 +2) + T.expect(providerBalance).to.equal(988) // 3000 - (2000 + 12) T.d("testOutboundPaymentFromProvider done") } \ No newline at end of file diff --git a/src/tests/networkSetup.ts b/src/tests/networkSetup.ts index 8331531d..870016e6 100644 --- a/src/tests/networkSetup.ts +++ b/src/tests/networkSetup.ts @@ -24,8 +24,8 @@ export const setupNetwork = async (): Promise => { const lndNodeSettings = LoadLndNodeSettingsFromEnv({}) const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false } - const alice = new LND(() => ({ lndSettings, lndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) - const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const alice = new LND(() => ({ lndSettings, lndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await tryUntil(async i => { const peers = await alice.ListPeers() if (peers.peers.length > 0) { diff --git a/src/tests/setupBootstrapped.ts b/src/tests/setupBootstrapped.ts index dfcf16c7..32aa3144 100644 --- a/src/tests/setupBootstrapped.ts +++ b/src/tests/setupBootstrapped.ts @@ -45,7 +45,7 @@ export const initBootstrappedInstance = async (T: TestBase) => { bootstrapped.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) await new Promise(res => { const interval = setInterval(async () => { - const canHandle = await bootstrapped.liquidityProvider.CanProviderHandle({ action: 'receive', amount: 2000 }) + const canHandle = bootstrapped.liquidityProvider.IsReady() if (canHandle) { clearInterval(interval) res() diff --git a/src/tests/spamExternalPayments.spec.ts b/src/tests/spamExternalPayments.spec.ts index e7956632..c81567c5 100644 --- a/src/tests/spamExternalPayments.spec.ts +++ b/src/tests/spamExternalPayments.spec.ts @@ -29,16 +29,15 @@ const testSpamExternalPayment = async (T: TestBase) => { const failedPayments = res.filter(r => !r.success) console.log(failedPayments) failedPayments.forEach(f => expect(f.err).to.be.equal("not enough balance to decrement")) - successfulPayments.forEach(s => expect(s.result).to.contain({ amount_paid: 500, network_fee: 1, service_fee: 3 })) + successfulPayments.forEach(s => expect(s.result).to.contain({ amount_paid: 500, network_fee: 0, service_fee: 10 })) expect(successfulPayments.length).to.be.equal(3) expect(failedPayments.length).to.be.equal(7) T.d("3 payments succeeded, 7 failed as expected") const u = await T.main.storage.userStorage.GetUser(T.user1.userId) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(u.balance_sats).to.be.equal(488) - T.d("user1 balance is now 488 (2000 - (500 + 3 fee + 1 routing) * 3)") - expect(owner.balance_sats).to.be.equal(9) - T.d("app balance is 9 sats") - + expect(u.balance_sats).to.be.equal(470) + T.d("user1 balance is now 470 (2000 - (500 + 10 fee) * 3)") + expect(owner.balance_sats).to.be.equal(27) + T.d("app balance is 27 sats") } diff --git a/src/tests/testBase.ts b/src/tests/testBase.ts index 235e8f86..fb3f0e90 100644 --- a/src/tests/testBase.ts +++ b/src/tests/testBase.ts @@ -87,12 +87,12 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise ({ lndSettings, lndNodeSettings: secondLndNodeSettings }) - const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await externalAccessToOtherLnd.Warmup() const thirdLndNodeSettings = LoadThirdLndSettingsFromEnv() const thirdLndSetting = () => ({ lndSettings, lndNodeSettings: thirdLndNodeSettings }) - const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await externalAccessToThirdLnd.Warmup()