Merge branch 'provider-fixes' into swaps-test

This commit is contained in:
boufni95 2025-11-28 16:24:00 +00:00
commit 7dc49e3f50
37 changed files with 674 additions and 330 deletions

View file

@ -24,6 +24,12 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 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 - name: Log in to the Container registry
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
@ -50,6 +56,7 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
platforms: linux/amd64,linux/arm64
tags: | tags: |
ghcr.io/${{ github.repository_owner }}/lightning-pub:latest ghcr.io/${{ github.repository_owner }}/lightning-pub:latest
ghcr.io/${{ github.repository_owner }}/lightning-pub:master ghcr.io/${{ github.repository_owner }}/lightning-pub:master
@ -58,8 +65,9 @@ jobs:
- name: Capture image digest - name: Capture image digest
id: capture-digest id: capture-digest
run: | run: |
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/${{ github.repository_owner }}/lightning-pub:latest | cut -d'@' -f2) # For multi-arch builds, use the digest from build output
echo "Raw Digest is $DIGEST" DIGEST="${{ steps.build-and-push.outputs.digest }}"
echo "Multi-arch manifest digest: $DIGEST"
echo "digest=$DIGEST" >> $GITHUB_OUTPUT echo "digest=$DIGEST" >> $GITHUB_OUTPUT
- name: Debug Print Digest - name: Debug Print Digest
@ -71,3 +79,30 @@ jobs:
subject-digest: ${{ steps.capture-digest.outputs.digest }} subject-digest: ${{ steps.capture-digest.outputs.digest }}
subject-name: ghcr.io/${{ github.repository_owner }}/lightning-pub subject-name: ghcr.io/${{ github.repository_owner }}/lightning-pub
push-to-registry: true 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"

View file

@ -1,8 +1,5 @@
# Docker Installation # 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: 1. Pull the Docker image:
```ssh ```ssh
@ -21,4 +18,5 @@ docker run -d \
-v $HOME/.lnd:/root/.lnd \ -v $HOME/.lnd:/root/.lnd \
ghcr.io/shocknet/lightning-pub:latest 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.

View file

@ -1104,6 +1104,13 @@ The nostr server will send back a message response, and inside the body there wi
- __nostr_pub__: _string_ - __nostr_pub__: _string_
- __user_identifier__: _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 ### BundleData
- __available_chunks__: ARRAY of: _number_ - __available_chunks__: ARRAY of: _number_
- __base_64_data__: ARRAY of: _string_ - __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 ### CreateOneTimeInviteLinkResponse
- __invitation_link__: _string_ - __invitation_link__: _string_
### CumulativeFees
- __networkFeeFixed__: _number_
- __serviceFeeBps__: _number_
### DebitAuthorization ### DebitAuthorization
- __authorized__: _boolean_ - __authorized__: _boolean_
- __debit_id__: _string_ - __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_ - __request_id__: _string_
### LiveUserOperation ### LiveUserOperation
- __latest_balance__: _number_
- __operation__: _[UserOperation](#UserOperation)_ - __operation__: _[UserOperation](#UserOperation)_
### LndChannels ### LndChannels
@ -1473,16 +1485,19 @@ The nostr server will send back a message response, and inside the body there wi
### PayAppUserInvoiceRequest ### PayAppUserInvoiceRequest
- __amount__: _number_ - __amount__: _number_
- __debit_npub__: _string_ *this field is optional - __debit_npub__: _string_ *this field is optional
- __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional
- __invoice__: _string_ - __invoice__: _string_
- __user_identifier__: _string_ - __user_identifier__: _string_
### PayInvoiceRequest ### PayInvoiceRequest
- __amount__: _number_ - __amount__: _number_
- __debit_npub__: _string_ *this field is optional - __debit_npub__: _string_ *this field is optional
- __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional
- __invoice__: _string_ - __invoice__: _string_
### PayInvoiceResponse ### PayInvoiceResponse
- __amount_paid__: _number_ - __amount_paid__: _number_
- __latest_balance__: _number_
- __network_fee__: _number_ - __network_fee__: _number_
- __operation_id__: _string_ - __operation_id__: _string_
- __preimage__: _string_ - __preimage__: _string_
@ -1570,7 +1585,6 @@ The nostr server will send back a message response, and inside the body there wi
### TransactionSwapQuote ### TransactionSwapQuote
- __chain_fee_sats__: _number_ - __chain_fee_sats__: _number_
- __invoice_amount_sats__: _number_ - __invoice_amount_sats__: _number_
- __routing_fee_reserve_sats__: _number_
- __service_fee_sats__: _number_ - __service_fee_sats__: _number_
- __swap_fee_sats__: _number_ - __swap_fee_sats__: _number_
- __swap_operation_id__: _string_ - __swap_operation_id__: _string_

View file

@ -177,6 +177,13 @@ type BannedAppUser struct {
Nostr_pub string `json:"nostr_pub"` Nostr_pub string `json:"nostr_pub"`
User_identifier string `json:"user_identifier"` 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 { type BundleData struct {
Available_chunks []int64 `json:"available_chunks"` Available_chunks []int64 `json:"available_chunks"`
Base_64_data []string `json:"base_64_data"` Base_64_data []string `json:"base_64_data"`
@ -222,6 +229,10 @@ type CreateOneTimeInviteLinkRequest struct {
type CreateOneTimeInviteLinkResponse struct { type CreateOneTimeInviteLinkResponse struct {
Invitation_link string `json:"invitation_link"` Invitation_link string `json:"invitation_link"`
} }
type CumulativeFees struct {
Networkfeefixed int64 `json:"networkFeeFixed"`
Servicefeebps int64 `json:"serviceFeeBps"`
}
type DebitAuthorization struct { type DebitAuthorization struct {
Authorized bool `json:"authorized"` Authorized bool `json:"authorized"`
Debit_id string `json:"debit_id"` Debit_id string `json:"debit_id"`
@ -360,7 +371,8 @@ type LiveManageRequest struct {
Request_id string `json:"request_id"` Request_id string `json:"request_id"`
} }
type LiveUserOperation struct { type LiveUserOperation struct {
Operation *UserOperation `json:"operation"` Latest_balance int64 `json:"latest_balance"`
Operation *UserOperation `json:"operation"`
} }
type LndChannels struct { type LndChannels struct {
Open_channels []OpenChannel `json:"open_channels"` Open_channels []OpenChannel `json:"open_channels"`
@ -544,22 +556,25 @@ type PayAddressResponse struct {
Txid string `json:"txId"` Txid string `json:"txId"`
} }
type PayAppUserInvoiceRequest struct { type PayAppUserInvoiceRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Debit_npub string `json:"debit_npub"` Debit_npub string `json:"debit_npub"`
Invoice string `json:"invoice"` Expected_fees *CumulativeFees `json:"expected_fees"`
User_identifier string `json:"user_identifier"` Invoice string `json:"invoice"`
User_identifier string `json:"user_identifier"`
} }
type PayInvoiceRequest struct { type PayInvoiceRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Debit_npub string `json:"debit_npub"` Debit_npub string `json:"debit_npub"`
Invoice string `json:"invoice"` Expected_fees *CumulativeFees `json:"expected_fees"`
Invoice string `json:"invoice"`
} }
type PayInvoiceResponse struct { type PayInvoiceResponse struct {
Amount_paid int64 `json:"amount_paid"` Amount_paid int64 `json:"amount_paid"`
Network_fee int64 `json:"network_fee"` Latest_balance int64 `json:"latest_balance"`
Operation_id string `json:"operation_id"` Network_fee int64 `json:"network_fee"`
Preimage string `json:"preimage"` Operation_id string `json:"operation_id"`
Service_fee int64 `json:"service_fee"` Preimage string `json:"preimage"`
Service_fee int64 `json:"service_fee"`
} }
type PayerData struct { type PayerData struct {
Data map[string]string `json:"data"` Data map[string]string `json:"data"`
@ -641,13 +656,12 @@ type SingleMetricReq struct {
Request_id int64 `json:"request_id"` Request_id int64 `json:"request_id"`
} }
type TransactionSwapQuote struct { type TransactionSwapQuote struct {
Chain_fee_sats int64 `json:"chain_fee_sats"` Chain_fee_sats int64 `json:"chain_fee_sats"`
Invoice_amount_sats int64 `json:"invoice_amount_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"`
Service_fee_sats int64 `json:"service_fee_sats"` Swap_fee_sats int64 `json:"swap_fee_sats"`
Swap_fee_sats int64 `json:"swap_fee_sats"` Swap_operation_id string `json:"swap_operation_id"`
Swap_operation_id string `json:"swap_operation_id"` Transaction_amount_sats int64 `json:"transaction_amount_sats"`
Transaction_amount_sats int64 `json:"transaction_amount_sats"`
} }
type TransactionSwapRequest struct { type TransactionSwapRequest struct {
Transaction_amount_sats int64 `json:"transaction_amount_sats"` Transaction_amount_sats int64 `json:"transaction_amount_sats"`

View file

@ -983,6 +983,48 @@ export const BannedAppUserValidate = (o?: BannedAppUser, opts: BannedAppUserOpti
return null 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 = { export type BundleData = {
available_chunks: number[] available_chunks: number[]
base_64_data: string[] base_64_data: string[]
@ -1256,6 +1298,29 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL
return null 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 = { export type DebitAuthorization = {
authorized: boolean authorized: boolean
debit_id: string debit_id: string
@ -2093,17 +2158,22 @@ export const LiveManageRequestValidate = (o?: LiveManageRequest, opts: LiveManag
} }
export type LiveUserOperation = { export type LiveUserOperation = {
latest_balance: number
operation: UserOperation operation: UserOperation
} }
export const LiveUserOperationOptionalFields: [] = [] export const LiveUserOperationOptionalFields: [] = []
export type LiveUserOperationOptions = OptionsBaseMessage & { export type LiveUserOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
latest_balance_CustomCheck?: (v: number) => boolean
operation_Options?: UserOperationOptions operation_Options?: UserOperationOptions
} }
export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserOperationOptions = {}, path: string = 'LiveUserOperation::root.'): Error | null => { 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 (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 !== '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`) const operationErr = UserOperationValidate(o.operation, opts.operation_Options, `${path}.operation`)
if (operationErr !== null) return operationErr if (operationErr !== null) return operationErr
@ -3202,15 +3272,17 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr
export type PayAppUserInvoiceRequest = { export type PayAppUserInvoiceRequest = {
amount: number amount: number
debit_npub?: string debit_npub?: string
expected_fees?: CumulativeFees
invoice: string invoice: string
user_identifier: string user_identifier: string
} }
export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees'
export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub'] export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees']
export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & { export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[] checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[]
amount_CustomCheck?: (v: number) => boolean amount_CustomCheck?: (v: number) => boolean
debit_npub_CustomCheck?: (v?: string) => boolean debit_npub_CustomCheck?: (v?: string) => boolean
expected_fees_Options?: CumulativeFeesOptions
invoice_CustomCheck?: (v: string) => boolean invoice_CustomCheck?: (v: string) => boolean
user_identifier_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 ((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 (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 (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`) 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 = { export type PayInvoiceRequest = {
amount: number amount: number
debit_npub?: string debit_npub?: string
expected_fees?: CumulativeFees
invoice: string invoice: string
} }
export type PayInvoiceRequestOptionalField = 'debit_npub' export type PayInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees'
export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub'] export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees']
export type PayInvoiceRequestOptions = OptionsBaseMessage & { export type PayInvoiceRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: PayInvoiceRequestOptionalField[] checkOptionalsAreSet?: PayInvoiceRequestOptionalField[]
amount_CustomCheck?: (v: number) => boolean amount_CustomCheck?: (v: number) => boolean
debit_npub_CustomCheck?: (v?: string) => boolean debit_npub_CustomCheck?: (v?: string) => boolean
expected_fees_Options?: CumulativeFeesOptions
invoice_CustomCheck?: (v: string) => boolean invoice_CustomCheck?: (v: string) => boolean
} }
export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoiceRequestOptions = {}, path: string = 'PayInvoiceRequest::root.'): Error | null => { 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 ((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 (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 (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`) 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 = { export type PayInvoiceResponse = {
amount_paid: number amount_paid: number
latest_balance: number
network_fee: number network_fee: number
operation_id: string operation_id: string
preimage: string preimage: string
@ -3273,6 +3360,7 @@ export const PayInvoiceResponseOptionalFields: [] = []
export type PayInvoiceResponseOptions = OptionsBaseMessage & { export type PayInvoiceResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
amount_paid_CustomCheck?: (v: number) => boolean amount_paid_CustomCheck?: (v: number) => boolean
latest_balance_CustomCheck?: (v: number) => boolean
network_fee_CustomCheck?: (v: number) => boolean network_fee_CustomCheck?: (v: number) => boolean
operation_id_CustomCheck?: (v: string) => boolean operation_id_CustomCheck?: (v: string) => boolean
preimage_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 (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 (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 (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`) 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 = { export type TransactionSwapQuote = {
chain_fee_sats: number chain_fee_sats: number
invoice_amount_sats: number invoice_amount_sats: number
routing_fee_reserve_sats: number
service_fee_sats: number service_fee_sats: number
swap_fee_sats: number swap_fee_sats: number
swap_operation_id: string swap_operation_id: string
@ -3768,7 +3858,6 @@ export type TransactionSwapQuoteOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
chain_fee_sats_CustomCheck?: (v: number) => boolean chain_fee_sats_CustomCheck?: (v: number) => boolean
invoice_amount_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 service_fee_sats_CustomCheck?: (v: number) => boolean
swap_fee_sats_CustomCheck?: (v: number) => boolean swap_fee_sats_CustomCheck?: (v: number) => boolean
swap_operation_id_CustomCheck?: (v: string) => 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 (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 (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 (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`) 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`)

View file

@ -389,7 +389,8 @@ message PayAppUserInvoiceRequest {
string user_identifier = 1; string user_identifier = 1;
string invoice = 2; string invoice = 2;
int64 amount = 3; int64 amount = 3;
optional string debit_npub = 4; optional string debit_npub = 4;
optional CumulativeFees expected_fees = 5;
} }
message SendAppUserToAppUserPaymentRequest { message SendAppUserToAppUserPaymentRequest {
@ -466,7 +467,8 @@ message DecodeInvoiceResponse{
message PayInvoiceRequest{ message PayInvoiceRequest{
string invoice = 1; string invoice = 1;
int64 amount = 2; int64 amount = 2;
optional string debit_npub = 3; optional string debit_npub = 3;
optional CumulativeFees expected_fees = 4;
} }
message PayInvoiceResponse{ message PayInvoiceResponse{
@ -475,6 +477,7 @@ message PayInvoiceResponse{
string operation_id = 3; string operation_id = 3;
int64 service_fee = 4; int64 service_fee = 4;
int64 network_fee = 5; int64 network_fee = 5;
int64 latest_balance = 6;
} }
message GetPaymentStateRequest{ message GetPaymentStateRequest{
@ -605,6 +608,7 @@ message GetProductBuyLinkResponse {
message LiveUserOperation { message LiveUserOperation {
UserOperation operation = 1; UserOperation operation = 1;
int64 latest_balance = 2;
} }
message MigrationUpdate { message MigrationUpdate {
optional ClosureMigration closure = 1; optional ClosureMigration closure = 1;
@ -837,6 +841,17 @@ message TransactionSwapQuote {
int64 swap_fee_sats = 4; int64 swap_fee_sats = 4;
int64 chain_fee_sats = 5; int64 chain_fee_sats = 5;
int64 routing_fee_reserve_sats = 6;
int64 service_fee_sats = 7; 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;
} }

View file

@ -25,7 +25,10 @@ const start = async () => {
const nostrSettings = settingsManager.getSettings().nostrRelaySettings const nostrSettings = settingsManager.getSettings().nostrRelaySettings
log("initializing nostr middleware") log("initializing nostr middleware")
const { Send } = nostrMiddleware(serverMethods, mainHandler, 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) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
) )
log("starting server") log("starting server")

View file

@ -25,10 +25,13 @@ const start = async () => {
const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler) const serverMethods = GetServerMethods(mainHandler)
log("initializing nostr middleware") log("initializing nostr middleware")
const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays const relays = settingsManager.getSettings().nostrRelaySettings.relays
const maxEventContentLength = mainHandler.settings.getSettings().nostrRelaySettings.maxEventContentLength const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength
const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler, 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) (e, p) => mainHandler.liquidityProvider.onEvent(e, p)
) )
exitHandler(() => { Stop(); mainHandler.Stop() }) exitHandler(() => { Stop(); mainHandler.Stop() })
@ -43,7 +46,7 @@ const start = async () => {
} }
adminManager.setAppNprofile(appNprofile) adminManager.setAppNprofile(appNprofile)
const Server = NewServer(serverMethods, serverOptions(mainHandler)) const Server = NewServer(serverMethods, serverOptions(mainHandler))
Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) Server.Listen(settingsManager.getSettings().serviceSettings.servicePort)
} }
start() start()

View file

@ -79,7 +79,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
nostrTransport({ ...j, appId: event.appId }, res => { 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 }) }) nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) })
}, event.startAtNano, event.startAtMs) }, event.startAtNano, event.startAtMs)
}) }, beacon => mainHandler.liquidityProvider.onBeaconEvent(beacon))
// Mark nostr connected/ready after initial subscription tick // Mark nostr connected/ready after initial subscription tick
mainHandler.adminManager.setNostrConnected(true) mainHandler.adminManager.setNostrConnected(true)

View file

@ -16,7 +16,7 @@ import { SendCoinsReq } from './sendCoinsReq.js';
import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js'; import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js';
import { ERROR, getLogger } from '../helpers/logger.js'; import { ERROR, getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.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 { Utils } from '../helpers/utilsWrapper.js';
import { TxPointSettings } from '../storage/tlv/stateBundler.js'; import { TxPointSettings } from '../storage/tlv/stateBundler.js';
import { WalletKitClient } from '../../../proto/lnd/walletkit.client.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'; import { LndNodeSettings, LndSettings } from '../main/settings.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 5 const deadLndRetrySeconds = 20
type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' } type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' }
type NodeSettingsOverride = { type NodeSettingsOverride = {
lndAddr: string lndAddr: string
@ -51,9 +51,11 @@ export default class {
outgoingOpsLocked = false outgoingOpsLocked = false
liquidProvider: LiquidityProvider liquidProvider: LiquidityProvider
utils: Utils utils: Utils
constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { unlockLnd: () => Promise<void>
constructor(getSettings: () => { lndSettings: LndSettings, lndNodeSettings: LndNodeSettings }, liquidProvider: LiquidityProvider, unlockLnd: () => Promise<any>, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) {
this.getSettings = getSettings this.getSettings = getSettings
this.utils = utils this.utils = utils
this.unlockLnd = unlockLnd
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
this.newBlockCb = newBlockCb this.newBlockCb = newBlockCb
@ -103,9 +105,10 @@ export default class {
} }
async Warmup() { async Warmup() {
// console.log("Warming up LND")
this.SubscribeAddressPaid() this.SubscribeAddressPaid()
this.SubscribeInvoicePaid() this.SubscribeInvoicePaid()
this.SubscribeNewBlock() await this.SubscribeNewBlock()
this.SubscribeHtlcEvents() this.SubscribeHtlcEvents()
this.SubscribeChannelEvents() this.SubscribeChannelEvents()
const now = Date.now() const now = Date.now()
@ -127,20 +130,24 @@ export default class {
} }
async GetInfo(): Promise<NodeInfo> { async GetInfo(): Promise<NodeInfo> {
// console.log("Getting info")
const res = await this.lightning.getInfo({}, DeadLineMetadata()) const res = await this.lightning.getInfo({}, DeadLineMetadata())
return res.response return res.response
} }
async ListPendingChannels(): Promise<PendingChannelsResponse> { async ListPendingChannels(): Promise<PendingChannelsResponse> {
// console.log("Listing pending channels")
const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata()) const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata())
return res.response return res.response
} }
async ListChannels(peerLookup = false): Promise<ListChannelsResponse> { async ListChannels(peerLookup = false): Promise<ListChannelsResponse> {
// console.log("Listing channels")
const res = await this.lightning.listChannels({ const res = await this.lightning.listChannels({
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup
}, DeadLineMetadata()) }, DeadLineMetadata())
return res.response return res.response
} }
async ListClosedChannels(): Promise<ClosedChannelsResponse> { async ListClosedChannels(): Promise<ClosedChannelsResponse> {
// console.log("Listing closed channels")
const res = await this.lightning.closedChannels({ const res = await this.lightning.closedChannels({
abandoned: true, abandoned: true,
breach: true, breach: true,
@ -153,6 +160,7 @@ export default class {
} }
async Health(): Promise<void> { async Health(): Promise<void> {
// console.log("Checking health")
if (!this.ready) { if (!this.ready) {
throw new Error("not ready") throw new Error("not ready")
} }
@ -163,16 +171,17 @@ export default class {
} }
RestartStreams() { RestartStreams() {
if (!this.ready) { // console.log("Restarting streams")
if (!this.ready || this.abortController.signal.aborted) {
return return
} }
this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds") this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds")
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
await this.Health() await this.unlockLnd()
this.log("LND is back online") this.log("LND is back online")
clearInterval(interval) clearInterval(interval)
this.Warmup() await this.Warmup()
} catch (err) { } catch (err) {
this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds") this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds")
} }
@ -180,6 +189,7 @@ export default class {
} }
async SubscribeChannelEvents() { async SubscribeChannelEvents() {
// console.log("Subscribing to channel events")
const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal }) const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(async channel => { stream.responses.onMessage(async channel => {
const channels = await this.ListChannels() const channels = await this.ListChannels()
@ -194,6 +204,7 @@ export default class {
} }
async SubscribeHtlcEvents() { async SubscribeHtlcEvents() {
// console.log("Subscribing to htlc events")
const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal }) const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(htlc => { stream.responses.onMessage(htlc => {
this.htlcCb(htlc) this.htlcCb(htlc)
@ -207,20 +218,22 @@ export default class {
} }
async SubscribeNewBlock() { async SubscribeNewBlock() {
// console.log("Subscribing to new block")
const { blockHeight } = await this.GetInfo() const { blockHeight } = await this.GetInfo()
const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal }) const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal })
stream.responses.onMessage(block => { stream.responses.onMessage(block => {
this.newBlockCb(block.height) this.newBlockCb(block.height)
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
this.log("Error with onchain tx stream") this.log("Error with new block stream")
}) })
stream.responses.onComplete(() => { stream.responses.onComplete(() => {
this.log("onchain tx stream closed") this.log("new block stream closed")
}) })
} }
SubscribeAddressPaid(): void { SubscribeAddressPaid(): void {
// console.log("Subscribing to address paid")
const stream = this.lightning.subscribeTransactions({ const stream = this.lightning.subscribeTransactions({
account: "", account: "",
endHeight: 0, endHeight: 0,
@ -247,6 +260,7 @@ export default class {
} }
SubscribeInvoicePaid(): void { SubscribeInvoicePaid(): void {
// console.log("Subscribing to invoice paid")
const stream = this.lightning.subscribeInvoices({ const stream = this.lightning.subscribeInvoices({
settleIndex: BigInt(this.latestKnownSettleIndex), settleIndex: BigInt(this.latestKnownSettleIndex),
addIndex: 0n, addIndex: 0n,
@ -257,17 +271,25 @@ export default class {
this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), 'lnd') this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), 'lnd')
} }
}) })
let restarted = false
stream.responses.onError(error => { stream.responses.onError(error => {
this.log("Error with invoice stream") this.log("Error with invoice stream")
if (!restarted) {
restarted = true
this.RestartStreams()
}
}) })
stream.responses.onComplete(() => { stream.responses.onComplete(() => {
this.log("invoice stream closed") this.log("invoice stream closed")
this.RestartStreams() if (!restarted) {
restarted = true
this.RestartStreams()
}
}) })
} }
async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> { async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> {
// console.log("Creating new address")
let lndAddressType: AddressType let lndAddressType: AddressType
switch (addressType) { switch (addressType) {
case Types.AddressType.NESTED_PUBKEY_HASH: 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<Invoice> { async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise<Invoice> {
// console.log("Creating new invoice")
if (useProvider) { if (useProvider) {
console.log("using provider") console.log("using provider")
const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry)
@ -314,24 +337,19 @@ export default class {
} }
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> { async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
// console.log("Decoding invoice")
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } 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 }> { async ChannelBalance(): Promise<{ local: number, remote: number }> {
// console.log("Getting channel balance")
const res = await this.lightning.channelBalance({}) const res = await this.lightning.channelBalance({})
const r = res.response const r = res.response
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 } return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
} }
async PayInvoice(invoice: string, amount: number, feeLimit: number, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise<PaidInvoice> { async PayInvoice(invoice: string, amount: number, feeLimit: number, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise<PaidInvoice> {
// console.log("Paying invoice")
if (this.outgoingOpsLocked) { if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync") throw new Error("lnd node is currently out of sync")
@ -339,7 +357,7 @@ export default class {
if (useProvider) { if (useProvider) {
const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from) const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from)
const providerDst = this.liquidProvider.GetProviderDestination() 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() await this.Health()
try { try {
@ -378,6 +396,7 @@ export default class {
} }
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> { async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
// console.log("Estimating chain fees")
await this.Health() await this.Health()
const res = await this.lightning.estimateFee({ const res = await this.lightning.estimateFee({
addrToAmount: { [address]: BigInt(amount) }, addrToAmount: { [address]: BigInt(amount) },
@ -390,6 +409,7 @@ export default class {
} }
async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise<SendCoinsResponse> { async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise<SendCoinsResponse> {
// console.log("Paying address")
if (this.outgoingOpsLocked) { if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync") throw new Error("lnd node is currently out of sync")
@ -409,16 +429,19 @@ export default class {
} }
async GetTransactions(startHeight: number): Promise<TransactionDetails> { async GetTransactions(startHeight: number): Promise<TransactionDetails> {
// console.log("Getting transactions")
const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata()) const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata())
return res.response return res.response
} }
async GetChannelInfo(chanId: string) { async GetChannelInfo(chanId: string) {
// console.log("Getting channel info")
const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata()) const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata())
return res.response return res.response
} }
async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) { async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) {
// console.log("Updating channel policy")
const split = chanPoint.split(':') const split = chanPoint.split(':')
const res = await this.lightning.updateChannelPolicy({ const res = await this.lightning.updateChannelPolicy({
@ -436,16 +459,19 @@ export default class {
} }
async GetChannelBalance() { async GetChannelBalance() {
// console.log("Getting channel balance")
const res = await this.lightning.channelBalance({}, DeadLineMetadata()) const res = await this.lightning.channelBalance({}, DeadLineMetadata())
return res.response return res.response
} }
async GetWalletBalance() { async GetWalletBalance() {
// console.log("Getting wallet balance")
const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
return res.response return res.response
} }
async GetTotalBalace() { async GetTotalBalace() {
// console.log("Getting total balance")
const walletBalance = await this.GetWalletBalance() const walletBalance = await this.GetWalletBalance()
const confirmedWalletBalance = Number(walletBalance.confirmedBalance) const confirmedWalletBalance = Number(walletBalance.confirmedBalance)
this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance) this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance)
@ -460,6 +486,7 @@ export default class {
} }
async GetBalance(): Promise<BalanceInfo> { // TODO: remove this async GetBalance(): Promise<BalanceInfo> { // TODO: remove this
// console.log("Getting balance")
const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
const { response } = await this.lightning.listChannels({ const { response } = await this.lightning.listChannels({
@ -475,20 +502,24 @@ export default class {
} }
async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> { async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> {
// console.log("Getting forwarding history")
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata()) const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata())
return response return response
} }
async GetAllPaidInvoices(max: number) { 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()) const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata())
return res.response return res.response
} }
async GetAllPayments(max: number) { 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 }) const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n })
return res.response return res.response
} }
async GetPayment(paymentIndex: number) { async GetPayment(paymentIndex: number) {
// console.log("Getting payment")
if (paymentIndex === 0) { if (paymentIndex === 0) {
throw new Error("payment index starts from 1") throw new Error("payment index starts from 1")
} }
@ -500,6 +531,7 @@ export default class {
} }
async GetLatestPaymentIndex(from = 0) { async GetLatestPaymentIndex(from = 0) {
// console.log("Getting latest payment index")
let indexOffset = BigInt(from) let indexOffset = BigInt(from)
while (true) { while (true) {
const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset, maxPayments: 0n, reversed: false, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) 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 }) { async ConnectPeer(addr: { pubkey: string, host: string }) {
// console.log("Connecting to peer")
const res = await this.lightning.connectPeer({ const res = await this.lightning.connectPeer({
addr, addr,
perm: true, perm: true,
@ -520,6 +553,7 @@ export default class {
} }
async GetPaymentFromHash(paymentHash: string): Promise<Payment | null> { async GetPaymentFromHash(paymentHash: string): Promise<Payment | null> {
// console.log("Getting payment from hash")
const abortController = new AbortController() const abortController = new AbortController()
const stream = this.router.trackPaymentV2({ const stream = this.router.trackPaymentV2({
paymentHash: Buffer.from(paymentHash, 'hex'), paymentHash: Buffer.from(paymentHash, 'hex'),
@ -541,11 +575,13 @@ export default class {
} }
async GetTx(txid: string) { async GetTx(txid: string) {
// console.log("Getting transaction")
const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata()) const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata())
return res.response return res.response
} }
async AddPeer(pub: string, host: string, port: number) { async AddPeer(pub: string, host: string, port: number) {
// console.log("Adding peer")
const res = await this.lightning.connectPeer({ const res = await this.lightning.connectPeer({
addr: { addr: {
pubkey: pub, pubkey: pub,
@ -558,11 +594,13 @@ export default class {
} }
async ListPeers() { async ListPeers() {
// console.log("Listing peers")
const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata()) const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata())
return res.response return res.response
} }
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number, satsPerVByte: number): Promise<OpenStatusUpdate> { async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number, satsPerVByte: number): Promise<OpenStatusUpdate> {
// console.log("Opening channel")
const abortController = new AbortController() const abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte) const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte)
const stream = this.lightning.openChannel(req, { abort: abortController.signal }) 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<PendingUpdate> { async CloseChannel(fundingTx: string, outputIndex: number, force: boolean, satPerVByte: number): Promise<PendingUpdate> {
// console.log("Closing channel")
const stream = this.lightning.closeChannel({ const stream = this.lightning.closeChannel({
deliveryAddress: "", deliveryAddress: "",
force: force, force: force,

View file

@ -101,7 +101,7 @@ export class FlashsatsLSP extends LSP {
return null return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system') 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) 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 } 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 return null
} }
const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system') 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) 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 } 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 return null
} }
const res = await this.liquidityProvider.PayInvoice(proposalRes.jit_bolt11, decoded.numSatoshis, 'system') 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) 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 } return { orderId: fee.id, invoice: proposalRes.jit_bolt11, totalSats: decoded.numSatoshis, fees }
} }

View file

@ -69,14 +69,15 @@ export default class {
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
} }
const nostrSettings = this.settings.getSettings().nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
const { max, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats)
return { return {
userId: ctx.user_id, userId: ctx.user_id,
balance: user.balance_sats, balance: user.balance_sats,
max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true), max_withdrawable: max,
user_identifier: appUser.identifier, user_identifier: appUser.identifier,
network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, network_max_fee_bps: 0,
network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, network_max_fee_fixed: networkFeeFixed,
service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, service_fee_bps: serviceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), 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] }), 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] }), 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, { return this.applicationManager.PayAppUserInvoice(ctx.app_id, {
amount: req.amount, amount: req.amount,
invoice: req.invoice, 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,
async PayAddress(ctx: Types.UserContext, req: Types.PayInvoiceRequest): Promise<Types.PayInvoiceResponse> {
return this.applicationManager.PayAppUserInvoice(ctx.app_id, {
amount: req.amount,
invoice: req.invoice,
user_identifier: ctx.app_user_id
}) })
} }

View file

@ -10,6 +10,7 @@ import { Application } from '../storage/entity/Application.js'
import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js' import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { NostrSend, SendData, SendInitiator } from '../nostr/handler.js'
const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds
type NsecLinkingData = { type NsecLinkingData = {
@ -17,7 +18,7 @@ type NsecLinkingData = {
expiry: number expiry: number
} }
export default class { export default class {
_nostrSend: NostrSend | null = null
storage: Storage storage: Storage
settings: SettingsManager settings: SettingsManager
paymentManager: PaymentManager paymentManager: PaymentManager
@ -33,6 +34,17 @@ export default class {
this.StartLinkingTokenInterval() 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() { StartLinkingTokenInterval() {
this.linkingTokenInterval = setInterval(() => { this.linkingTokenInterval = setInterval(() => {
const now = Date.now(); const now = Date.now();
@ -47,12 +59,13 @@ export default class {
}, 60 * 1000); // 1 minute }, 60 * 1000); // 1 minute
} }
async StartAppsServiceBeacon(publishBeacon: (app: Application) => void) { async StartAppsServiceBeacon(publishBeacon: (app: Application, fees: Types.CumulativeFees) => void) {
this.serviceBeaconInterval = setInterval(async () => { this.serviceBeaconInterval = setInterval(async () => {
try { try {
const fees = this.paymentManager.GetFees()
const apps = await this.storage.applicationStorage.GetApplications() const apps = await this.storage.applicationStorage.GetApplications()
apps.forEach(app => { apps.forEach(app => {
publishBeacon(app) publishBeacon(app, fees)
}) })
} catch (e) { } catch (e) {
this.log("error in beacon", (e as any).message) 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] }) 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 }) 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 { return {
identifier: u.identifier, identifier: u.identifier,
info: { info: {
userId: u.user.user_id, userId: u.user.user_id,
balance: u.user.balance_sats, balance: u.user.balance_sats,
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true), max_withdrawable: max,
user_identifier: u.identifier, user_identifier: u.identifier,
network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, network_max_fee_bps: 0,
network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, network_max_fee_fixed: networkFeeFixed,
service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, service_fee_bps: serviceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), 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] }), 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] }), 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 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<Types.AppUser> { async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise<Types.AppUser> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) 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 nostrSettings = this.settings.getSettings().nostrRelaySettings
const { max, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats)
return { return {
max_withdrawable: max, identifier: req.user_identifier, info: { max_withdrawable: max, identifier: req.user_identifier, info: {
userId: user.user.user_id, balance: user.user.balance_sats, 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, user_identifier: user.identifier,
network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, network_max_fee_bps: 0,
network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, network_max_fee_fixed: networkFeeFixed,
service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, service_fee_bps: serviceFeeBps,
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), 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] }), 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] }), 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<Types.PayInvoiceResponse> { async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise<Types.PayInvoiceResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) 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") getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats")
return paid 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<void> { async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise<void> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const fromUser = await this.storage.applicationStorage.GetApplicationUser(app, req.from_user_identifier) const fromUser = await this.storage.applicationStorage.GetApplicationUser(app, req.from_user_identifier)

View file

@ -9,9 +9,11 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js';
import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js';
import { UnsignedEvent } from 'nostr-tools'; import { UnsignedEvent } from 'nostr-tools';
import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk";
import { debitAccessRulesToDebitRules, newNdebitResponse,debitRulesToDebitAccessRules, import {
nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, debitAccessRulesToDebitRules, newNdebitResponse, debitRulesToDebitAccessRules,
frequencyRuleName,IntervalTypeToSeconds,unitToIntervalType } from "./debitTypes.js"; nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName,
frequencyRuleName, IntervalTypeToSeconds, unitToIntervalType
} from "./debitTypes.js";
export class DebitManager { export class DebitManager {
@ -32,6 +34,7 @@ export class DebitManager {
attachNostrSend = (nostrSend: NostrSend) => { attachNostrSend = (nostrSend: NostrSend) => {
this._nostrSend = nostrSend this._nostrSend = nostrSend
} }
nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => {
if (!this._nostrSend) { if (!this._nostrSend) {
throw new Error("No nostrSend attached") 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 }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: req.npub, id: req.request_id, appId: ctx.app_id })
return return
case Types.DebitResponse_response_type.INVOICE: 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 return
case Types.DebitResponse_response_type.AUTHORIZE: case Types.DebitResponse_response_type.AUTHORIZE:
await this.handleAuthorization(ctx, req.response.authorize, { npub: req.npub, request_id: req.request_id }) 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 { try {
this.logger("🔍 [DEBIT REQUEST] Paying single invoice") this.logger("🔍 [DEBIT REQUEST] Paying single invoice")
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice)
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 debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } 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) { } catch (e: any) {
this.logger("❌ [DEBIT REQUEST] Error in single invoice payment") 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 }) 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", { this.logger("🔍 [DEBIT REQUEST] Handling authorization", {
npub, npub,
request_id, request_id,
@ -122,15 +123,15 @@ export class DebitManager {
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id)
this.validateAccessRules(access, app, appUser) this.validateAccessRules(access, app, appUser)
this.logger("🔍 [DEBIT REQUEST] Sending debit payment") 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 } 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) { } catch (e: any) {
this.logger("❌ [DEBIT REQUEST] Error in debit authorization") 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 }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id })
throw e throw e
} }
} }
handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => { handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => {
@ -144,7 +145,7 @@ export class DebitManager {
pointerdata pointerdata
}) })
const res = await this.payNdebitInvoice(event, 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') { if (res.status === 'fail' || res.status === 'authOk') {
const e = newNdebitResponse(JSON.stringify(res.debitRes), event) const e = newNdebitResponse(JSON.stringify(res.debitRes), event)
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) 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) this.handleAuthRequired(pointerdata, event, res)
return return
} }
const { op, debitRes } = res const { debitRes } = res
this.notifyPaymentSuccess(appUser, debitRes, op, event) this.notifyPaymentSuccess(debitRes, event)
} }
handleAuthRequired = (data:NdebitData, event: NostrEvent, res: AuthRequiredRes) => { handleAuthRequired = (data: NdebitData, event: NostrEvent, res: AuthRequiredRes) => {
if (!res.appUser.nostr_public_key) { if (!res.appUser.nostr_public_key) {
this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId }) this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId })
return 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 }) 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 }) => { notifyPaymentSuccess = (debitRes: NdebitSuccess, 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 })
}
this.sendDebitResponse(debitRes, event) this.sendDebitResponse(debitRes, event)
} }
@ -286,15 +283,14 @@ export class DebitManager {
} }
await this.validateAccessRules(authorization, app, appUser) await this.validateAccessRules(authorization, app, appUser)
this.logger("🔍 [DEBIT REQUEST] Sending requested debit payment") this.logger("🔍 [DEBIT REQUEST] Sending requested debit payment")
const { op, payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) const { payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11)
return { status: 'invoicePaid', op, app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } return { status: 'invoicePaid', app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } }
} }
sendDebitPayment = async (appId: string, appUserId: string, requestorPub: string, bolt11: string) => { 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 }) 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) await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee)
const op = this.newPaymentOperation(payment, bolt11) return { payment }
return { payment, op }
} }
validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise<boolean> => { validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise<boolean> => {
@ -325,21 +321,5 @@ export class DebitManager {
} }
return true 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
}
}
} }

View file

@ -1,9 +1,9 @@
import * as Types from "../../../proto/autogenerated/ts/types.js"; 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 { Application } from '../storage/entity/Application.js';
import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { ApplicationUser } from '../storage/entity/ApplicationUser.js';
import { UnsignedEvent } from 'nostr-tools'; 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 expirationRuleName = 'expiration'
export const frequencyRuleName = 'frequency' 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 AuthRequiredRes = { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, app: Application, appUser: ApplicationUser }
export type HandleNdebitRes = { status: 'fail', debitRes: NdebitFailure } 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 | AuthRequiredRes
| { status: 'authOk', debitRes: NdebitSuccess } | { status: 'authOk', debitRes: NdebitSuccess }

View file

@ -79,7 +79,7 @@ export default class {
lndSettings: settings.getSettings().lndSettings, lndSettings: settings.getSettings().lndSettings,
lndNodeSettings: settings.getSettings().lndNodeSettings 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.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker)
this.metricsManager = new MetricsManager(this.storage, this.lnd) this.metricsManager = new MetricsManager(this.storage, this.lnd)
@ -103,8 +103,8 @@ export default class {
} }
StartBeacons() { StartBeacons() {
this.applicationManager.StartAppsServiceBeacon(app => { this.applicationManager.StartAppsServiceBeacon((app, fees) => {
this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url }) 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.offerManager.attachNostrSend(f)
this.managementManager.attachNostrSend(f) this.managementManager.attachNostrSend(f)
this.utils.attachNostrSend(f) this.utils.attachNostrSend(f)
this.applicationManager.attachNostrSend(f)
//this.webRTC.attachNostrSend(f) //this.webRTC.attachNostrSend(f)
} }
@ -227,7 +228,7 @@ export default class {
} }
log = getLogger({ appName: userAddress.linkedApplication.name }) log = getLogger({ appName: userAddress.linkedApplication.name })
const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id 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) { if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) {
fee = 0 fee = 0
} }
@ -271,7 +272,7 @@ export default class {
} }
log = getLogger({ appName: userInvoice.linkedApplication.name }) log = getLogger({ appName: userInvoice.linkedApplication.name })
const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id 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) { if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) {
fee = 0 fee = 0
} }
@ -373,8 +374,9 @@ export default class {
getLogger({ appName: app.name })("cannot notify user, not a nostr user") getLogger({ appName: app.name })("cannot notify user, not a nostr user")
return return
} }
const balance = user.user.balance_sats
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } =
{ operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance }
const j = JSON.stringify(message) const j = JSON.stringify(message)
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key }) this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key })
this.SendEncryptedNotification(app, user, op) 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) { if (!app.nostr_public_key) {
getLogger({ appName: app.name })("cannot update beacon, public key not set") getLogger({ appName: app.name })("cannot update beacon, public key not set")
return return
@ -435,8 +437,9 @@ export default class {
async ResetNostr() { async ResetNostr() {
const apps = await this.storage.applicationStorage.GetApplications() const apps = await this.storage.applicationStorage.GetApplications()
const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0]
const fees = this.paymentManager.GetFees()
for (const app of apps) { 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] 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 || "" })), 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, relays: this.settings.getSettings().nostrRelaySettings.relays,
maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength, maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength,
clients: [liquidityProviderInfo] clients: [liquidityProviderInfo],
providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub
} }
this.nostrReset(s) this.nostrReset(s)
} }

View file

@ -50,8 +50,11 @@ export class LiquidityManager {
} }
beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => {
const providerReady = this.liquidityProvider.IsReady()
if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
if (!providerReady) {
throw new Error("cannot use liquidity provider, it is not ready")
}
return 'provider' return 'provider'
} }
@ -63,7 +66,7 @@ export class LiquidityManager {
if (remote > amount) { if (remote > amount) {
return 'lnd' return 'lnd'
} }
const providerCanHandle = await this.liquidityProvider.CanProviderHandle({ action: 'receive', amount }) const providerCanHandle = this.liquidityProvider.IsReady()
if (!providerCanHandle) { if (!providerCanHandle) {
return 'lnd' 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 (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
if (!providerReady) {
throw new Error("cannot use liquidity provider, it is not ready")
}
return 'provider' return 'provider'
} }
const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount }) if (!providerReady) {
if (canHandle) { return 'lnd'
return 'provider'
} }
return 'lnd' const canHandle = await this.liquidityProvider.CanProviderPay(amount, localServiceFee)
if (!canHandle) {
return 'lnd'
}
return 'provider'
} }
afterOutInvoicePaid = async () => { } afterOutInvoicePaid = async () => { }
shouldDrainProvider = async () => { shouldDrainProvider = async () => {
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() const maxW = await this.liquidityProvider.GetMaxWithdrawable()
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
const drainable = Math.min(maxW, remote) const drainable = Math.min(maxW, remote)
if (drainable < 500) { if (drainable < 500) {
@ -121,7 +132,7 @@ export class LiquidityManager {
try { try {
const invoice = await this.lnd.NewInvoice(amt, "liqudity provider drain", defaultInvoiceExpiry, { from: 'system', useProvider: false }) 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 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.feesPaid += fees
this.updateLatestDrain(true, amt) this.updateLatestDrain(true, amt)
} catch (err: any) { } catch (err: any) {
@ -160,7 +171,7 @@ export class LiquidityManager {
if (pendingChannels.pendingOpenChannels.length > 0) { if (pendingChannels.pendingOpenChannels.length > 0) {
return { shouldOpen: false } return { shouldOpen: false }
} }
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() const maxW = await this.liquidityProvider.GetMaxWithdrawable()
if (maxW < threshold) { if (maxW < threshold) {
return { shouldOpen: false } return { shouldOpen: false }
} }

View file

@ -1,15 +1,13 @@
import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js' import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js'
import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js' import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js'
import * as Types from '../../../proto/autogenerated/ts/types.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 { Utils } from '../helpers/utilsWrapper.js'
import { NostrEvent, NostrSend } from '../nostr/handler.js' import { NostrEvent, NostrSend } from '../nostr/handler.js'
import { InvoicePaidCb } from '../lnd/settings.js' import { InvoicePaidCb } from '../lnd/settings.js'
import Storage from '../storage/index.js' import Storage from '../storage/index.js'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { LiquiditySettings } from './settings.js' import { LiquiditySettings } from './settings.js'
export type LiquidityRequest = { action: 'spend' | 'receive', amount: number }
export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export type nostrCallback<T> = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void }
export class LiquidityProvider { export class LiquidityProvider {
getSettings: () => LiquiditySettings getSettings: () => LiquiditySettings
@ -28,7 +26,11 @@ export class LiquidityProvider {
queue: ((state: 'ready') => void)[] = [] queue: ((state: 'ready') => void)[] = []
utils: Utils utils: Utils
pendingPayments: Record<string, number> = {} pendingPayments: Record<string, number> = {}
feesCache: Types.CumulativeFees | null = null
lastSeenBeacon = 0
latestReceivedBalance = 0
incrementProviderBalance: (balance: number) => Promise<void> incrementProviderBalance: (balance: number) => Promise<void>
pendingPaymentsAck: Record<string, boolean> = {}
// make the sub process accept client // make the sub process accept client
constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) { constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise<any>) {
this.utils = utils this.utils = utils
@ -68,7 +70,8 @@ export class LiquidityProvider {
} }
IsReady = () => { 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'> => { AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => {
@ -94,6 +97,7 @@ export class LiquidityProvider {
return return
} }
this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0) this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0)
this.lastSeenBeacon = Date.now()
this.ready = true this.ready = true
this.queue.forEach(q => q('ready')) this.queue.forEach(q => q('ready'))
this.log("subbing to user operations") this.log("subbing to user operations")
@ -107,6 +111,10 @@ export class LiquidityProvider {
try { try {
await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider')
this.incrementProviderBalance(res.operation.amount) 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) { } catch (err: any) {
this.log("error processing incoming invoice", err.message) this.log("error processing incoming invoice", err.message)
} }
@ -122,62 +130,77 @@ export class LiquidityProvider {
} }
return res 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('providerBalance', res.balance)
this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable) this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable)
return res return res
} }
GetLatestMaxWithdrawable = async () => { GetFees = () => {
if (!this.IsReady()) { if (!this.feesCache) {
return 0 throw new Error("fees not cached")
} }
const res = await this.GetUserState() return this.feesCache
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return 0
}
return res.max_withdrawable
} }
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()) { if (!this.IsReady()) {
return 0 return 0
} }
const res = await this.GetUserState() return this.latestReceivedBalance
if (res.status === 'ERROR') {
this.log("error getting user info", res.reason)
return 0
}
return res.balance
} }
GetPendingBalance = async () => { GetPendingBalance = async () => {
return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0) return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0)
} }
CalculateExpectedFeeLimit = (amount: number, info: Types.UserInfo) => { GetServiceFee = (amount: number, f?: Types.CumulativeFees) => {
const serviceFeeRate = info.service_fee_bps / 10000 const fees = f ? f : this.GetFees()
const serviceFeeRate = fees.serviceFeeBps / 10000
const serviceFee = Math.ceil(serviceFeeRate * amount) const serviceFee = Math.ceil(serviceFeeRate * amount)
const networkMaxFeeRate = info.network_max_fee_bps / 10000 return Math.max(serviceFee, fees.networkFeeFixed)
const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + info.network_max_fee_fixed)
return serviceFee + networkFeeLimit
} }
CanProviderHandle = async (req: LiquidityRequest) => { CanProviderPay = async (amount: number, localServiceFee: number): Promise<boolean> => {
if (!this.IsReady()) { if (!this.IsReady()) {
this.log("provider is not ready")
return false return false
} }
const maxW = await this.GetLatestMaxWithdrawable() const maxW = this.GetMaxWithdrawable()
if (req.action === 'spend') { if (maxW < amount) {
return maxW > req.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 return true
} }
AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => {
try { try {
if (!this.IsReady()) { 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 }) const res = await this.client.NewInvoice({ amountSats: amount, memo, expiry })
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
@ -196,20 +219,29 @@ export class LiquidityProvider {
PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => { PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => {
try { try {
if (!this.IsReady()) { 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() const fees = this.GetFees()
if (userInfo.status === 'ERROR') { const providerServiceFee = this.GetServiceFee(decodedAmount, fees)
throw new Error(userInfo.reason) this.pendingPayments[invoice] = decodedAmount + providerServiceFee
} const timeout = setTimeout(() => {
this.pendingPayments[invoice] = decodedAmount + this.CalculateExpectedFeeLimit(decodedAmount, userInfo) if (!this.pendingPaymentsAck[invoice]) {
const res = await this.client.PayInvoice({ invoice, amount: 0 }) 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') { if (res.status === 'ERROR') {
this.log("error paying invoice", res.reason) this.log("error paying invoice", res.reason)
throw new Error(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.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] })
this.latestReceivedBalance = res.latest_balance
this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true })
return res return res
} catch (err) { } catch (err) {
@ -221,7 +253,7 @@ export class LiquidityProvider {
GetPaymentState = async (invoice: string) => { GetPaymentState = async (invoice: string) => {
if (!this.IsReady()) { 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 }) const res = await this.client.GetPaymentState({ invoice })
if (res.status === 'ERROR') { if (res.status === 'ERROR') {
@ -233,7 +265,7 @@ export class LiquidityProvider {
GetOperations = async () => { GetOperations = async () => {
if (!this.IsReady()) { 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({ const res = await this.client.GetUserOperations({
latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 },
@ -265,7 +297,26 @@ export class LiquidityProvider {
setSetIfConfigured = () => { setSetIfConfigured = () => {
if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) { if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) {
this.configured = true 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
} }
} }

View file

@ -37,6 +37,8 @@ interface UserOperationInfo {
}; };
internal?: boolean; internal?: boolean;
} }
export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment } export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment }
const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]` const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]`
const defaultLnAddressMetadata = (text: string, id: string) => `[["text/plain", "${text}"],["text/identifier", "${id}"]]` 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) => { checkPendingProviderPayment = async (log: PubLogger, p: UserInvoicePayment) => {
const state = await this.lnd.liquidProvider.GetPaymentState(p.invoice) const state = await this.lnd.liquidProvider.GetPaymentState(p.invoice)
if (state.paid_at_unix < 0) { 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) log("found a failed provider payment, refunding", fullAmount, "sats to user", p.user.user_id)
await this.storage.StartTransaction(async tx => { await this.storage.StartTransaction(async tx => {
await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx) await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx)
@ -97,18 +99,16 @@ export default class {
return return
} else if (state.paid_at_unix > 0) { } else if (state.paid_at_unix > 0) {
log("provider payment succeeded", p.serial_id, "updating payment info") log("provider payment succeeded", p.serial_id, "updating payment info")
const routingFeeLimit = p.routing_fees
const serviceFee = p.service_fees const serviceFee = p.service_fees
const actualFee = state.network_fee + state.service_fee const networkFee = state.service_fee
await this.storage.StartTransaction(async tx => { await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, serviceFee, true)
if (routingFeeLimit - actualFee > 0) { const remainingFee = serviceFee - networkFee
this.log("refund pending provider payment routing fee", routingFeeLimit, actualFee, "sats") if (remainingFee < 0) {
await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) this.log("WARNING: provider fee was higher than expected,", remainingFee, "were lost")
} }
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 && remainingFee > 0) {
if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && serviceFee > 0) { await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees")
await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees")
} }
const user = await this.storage.userStorage.GetUser(p.user.user_id) 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 }) 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) log(ERROR, "lnd payment not found for pending payment hash ", decoded.paymentHash)
return return
} }
switch (payment.status) { switch (payment.status) {
case Payment_PaymentStatus.UNKNOWN: case Payment_PaymentStatus.UNKNOWN:
log("pending payment in unknown state", p.serial_id, "no action will be performed") log("pending payment in unknown state", p.serial_id, "no action will be performed")
@ -134,24 +133,22 @@ export default class {
return return
case Payment_PaymentStatus.SUCCEEDED: case Payment_PaymentStatus.SUCCEEDED:
log("pending payment succeeded", p.serial_id, "updating payment info") log("pending payment succeeded", p.serial_id, "updating payment info")
const routingFeeLimit = p.routing_fees
const serviceFee = p.service_fees const serviceFee = p.service_fees
const actualFee = Number(payment.feeSat) const networkFee = Number(payment.feeSat)
await this.storage.StartTransaction(async tx => {
if (routingFeeLimit - actualFee > 0) { await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, p.service_fees, true, undefined)
this.log("refund pending payment routing fee", routingFeeLimit, actualFee, "sats") const remainingFee = serviceFee - networkFee
await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) if (remainingFee < 0) { // should not be possible beacuse of the fee limit
} this.log("WARNING: lnd fee was higher than expected,", remainingFee, "were lost")
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 && remainingFee > 0) {
if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && serviceFee > 0) { await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees")
await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees")
} }
const user = await this.storage.userStorage.GetUser(p.user.user_id) 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 }) 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 return
case Payment_PaymentStatus.FAILED: 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) log("found a failed pending payment, refunding", fullAmount, "sats to user", p.user.user_id)
await this.storage.StartTransaction(async tx => { await this.storage.StartTransaction(async tx => {
await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, 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) { switch (action) {
case Types.UserOperationType.INCOMING_TX: case Types.UserOperationType.INCOMING_TX:
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount) 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: case Types.UserOperationType.INCOMING_INVOICE:
if (appUser) { if (appUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount)
} }
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount)
case Types.UserOperationType.OUTGOING_INVOICE: case Types.UserOperationType.INCOMING_USER_TO_USER:
if (appUser) { 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) return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount)
case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER: 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) { if (appUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
} }
@ -235,14 +249,19 @@ export default class {
} }
} }
GetMaxPayableInvoice(balance: number, appUser: boolean): number { GetFees = (): Types.CumulativeFees => {
let maxWithinServiceFee = 0 const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings
if (appUser) { const { feeFixedLimit } = this.settings.getSettings().lndSettings
maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) return { networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
} else { }
maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee)))
} GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) 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<Types.DecodeInvoiceResponse> { async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise<Types.DecodeInvoiceResponse> {
const decoded = await this.lnd.DecodeInvoice(req.invoice) 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<Types.PayInvoiceResponse> { async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, optionals: { swapOperationId?: string, ack?: (op: Types.UserOperation) => void } = {}): Promise<Types.PayInvoiceResponse & { operation: Types.UserOperation }> {
await this.watchDog.PaymentRequested() await this.watchDog.PaymentRequested()
const maybeBanned = await this.storage.userStorage.GetUser(userId) const maybeBanned = await this.storage.userStorage.GetUser(userId)
if (maybeBanned.locked) { if (maybeBanned.locked) {
throw new Error("user is banned, cannot send payment") 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) const decoded = await this.lnd.DecodeInvoice(req.invoice)
if (decoded.numSatoshis !== 0 && req.amount !== 0) { if (decoded.numSatoshis !== 0 && req.amount !== 0) {
throw new Error("invoice has value, do not provide amount the the request") throw new Error("invoice has value, do not provide amount the the request")
@ -266,7 +293,7 @@ export default class {
} }
const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis)
const isAppUserPayment = userId !== linkedApplication.owner.user_id 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) const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice)
if (internalInvoice && internalInvoice.paid_at_unix > 0) { if (internalInvoice && internalInvoice.paid_at_unix > 0) {
throw new Error("this invoice was already paid") throw new Error("this invoice was already paid")
@ -279,23 +306,28 @@ export default class {
if (internalInvoice) { if (internalInvoice) {
paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub) paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub)
} else { } 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) { const feeDiff = serviceFee - paymentInfo.networkFee
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee, "fees") if (isAppUserPayment && feeDiff > 0) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, feeDiff, "fees")
} }
const user = await this.storage.userStorage.GetUser(userId) 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 }) 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 { return {
preimage: paymentInfo.preimage, preimage: paymentInfo.preimage,
amount_paid: paymentInfo.amtPaid, amount_paid: paymentInfo.amtPaid,
operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, operation_id: opId,
network_fee: paymentInfo.networkFee, network_fee: 0,
service_fee: serviceFee, 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) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) {
throw new Error("something went wrong sending payment, please try again later") 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") throw new Error("payment already in progress")
} }
const { amountForLnd, payAmount, serviceFee } = amounts const { amountForLnd, payAmount, serviceFee } = amounts
const totalAmountToDecrement = payAmount + serviceFee const totalAmountToDecrement = payAmount + serviceFee
const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee)
const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount)
const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined
const pendingPayment = await this.storage.StartTransaction(async tx => { const pendingPayment = await this.storage.StartTransaction(async tx => {
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, invoice, tx)
return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, optionals) return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: 0 }, linkedApplication, provider, tx, optionals)
}, "payment started") }, "payment started")
this.log("ready to pay") 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 { 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) 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) 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 } return { preimage: payment.paymentPreimage, amtPaid: payment.valueSat, networkFee: payment.feeSat, serialId: pendingPayment.serial_id }
} catch (err) { } 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) await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, 0, 0, false)
throw err throw err
} }
@ -353,10 +387,8 @@ export default class {
} catch (err) { } catch (err) {
await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "internal_payment_refund:" + internalInvoice.invoice) 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) this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', payAmount, { used: 'internal', from: 'user' }, linkedApplication.app_id)
throw err throw err
} }
} }
async GetTransactionSwapQuote(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuote> { async GetTransactionSwapQuote(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuote> {
@ -375,8 +407,8 @@ export default class {
const swapFee = decoded.numSatoshis - chainTotal const swapFee = decoded.numSatoshis - chainTotal
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isAppUserPayment = ctx.user_id !== app.owner.user_id const isAppUserPayment = ctx.user_id !== app.owner.user_id
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isAppUserPayment) const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isAppUserPayment)
const routingFeeLimit = this.lnd.GetFeeLimitAmount(decoded.numSatoshis) // const routingFeeLimit = this.lnd.GetFeeLimitAmount(decoded.numSatoshis)
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({ const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
app_user_id: ctx.app_user_id, app_user_id: ctx.app_user_id,
swap_quote_id: res.createdResponse.id, swap_quote_id: res.createdResponse.id,
@ -400,7 +432,6 @@ export default class {
transaction_amount_sats: req.transaction_amount_sats, transaction_amount_sats: req.transaction_amount_sats,
chain_fee_sats: minerFee, chain_fee_sats: minerFee,
service_fee_sats: serviceFee, service_fee_sats: serviceFee,
routing_fee_reserve_sats: routingFeeLimit
} }
} }
@ -457,7 +488,7 @@ export default class {
}) })
let payment: Types.PayInvoiceResponse let payment: Types.PayInvoiceResponse
try { 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) { if (!swapResult.ok) {
this.log("invoice payment successful, but swap failed") this.log("invoice payment successful, but swap failed")
await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, swapResult.error) await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, swapResult.error)
@ -475,7 +506,7 @@ export default class {
} }
throw err 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 { return {
txId: swapResult.txId, txId: swapResult.txId,
network_fee: networkFeesTotal, network_fee: networkFeesTotal,
@ -491,7 +522,7 @@ export default class {
} }
const { blockHeight } = await this.lnd.GetInfo() const { blockHeight } = await this.lnd.GetInfo()
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false)
const isAppUserPayment = ctx.user_id !== app.owner.user_id const isAppUserPayment = ctx.user_id !== app.owner.user_id
const txId = crypto.randomBytes(32).toString("hex") 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<Types.PaymentState> { async GetPaymentState(userId: string, req: Types.GetPaymentStateRequest): Promise<Types.PaymentState> {
const user = await this.storage.userStorage.GetUser(userId) const user = await this.storage.userStorage.GetUser(userId)
if (user.locked) { if (user.locked) {
@ -783,7 +831,7 @@ export default class {
return { return {
paid_at_unix: invoice.paid_at_unix, paid_at_unix: invoice.paid_at_unix,
amount: invoice.paid_amount, amount: invoice.paid_amount,
network_fee: invoice.routing_fees, network_fee: 0,
service_fee: invoice.service_fees, service_fee: invoice.service_fees,
} }
} }
@ -822,7 +870,7 @@ export default class {
throw new Error("not enough balance to send payment") throw new Error("not enough balance to send payment")
} }
const isAppUserPayment = fromUser.user_id !== linkedApplication.owner.user_id 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 toDecrement = amount + fee
const paymentEntry = await this.storage.paymentStorage.AddPendingUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication, tx) 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) await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, toDecrement, `${toUserId}:${paymentEntry.serial_id}`, tx)

View file

@ -27,7 +27,7 @@ export class RugPullTracker {
const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst) const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst)
const ready = this.liquidProvider.IsReady() const ready = this.liquidProvider.IsReady()
if (ready) { if (ready) {
const balance = await this.liquidProvider.GetLatestBalance() const balance = this.liquidProvider.GetLatestBalance()
const pendingBalance = await this.liquidProvider.GetPendingBalance() const pendingBalance = await this.liquidProvider.GetPendingBalance()
const trackedBalance = balance + pendingBalance const trackedBalance = balance + pendingBalance
if (!providerTracker) { if (!providerTracker) {

View file

@ -78,9 +78,7 @@ export type LndNodeSettings = {
} }
export type LndSettings = { export type LndSettings = {
lndLogDir: string lndLogDir: string
feeRateLimit: number
feeFixedLimit: number feeFixedLimit: number
feeRateBps: number
mockLnd: boolean mockLnd: boolean
} }
@ -104,12 +102,9 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record<string, string | undefi
} }
export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => { export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => {
const feeRateBps: number = chooseEnvInt('OUTBOUND_MAX_FEE_BPS', dbEnv, 60, addToDb)
return { return {
lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb), lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb),
feeRateBps: feeRateBps, feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb),
feeRateLimit: feeRateBps / 10000,
feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 100, addToDb),
mockLnd: false mockLnd: false
} }
} }

View file

@ -46,7 +46,7 @@ export default class SettingsManager {
toAdd[key] = value toAdd[key] = value
} }
this.settings = this.loadEnvs(dbSettings, addToDb) 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) { for (const key in toAdd) {
await this.storage.settingsStorage.setDbEnvIFNeeded(key, toAdd[key]) await this.storage.settingsStorage.setDbEnvIFNeeded(key, toAdd[key])
} }

View file

@ -301,12 +301,12 @@ export class Unlocker {
GetWalletPassword = () => { GetWalletPassword = () => {
const path = this.settings.getStorageSettings().walletPasswordPath const path = this.settings.getStorageSettings().walletPasswordPath
let password = Buffer.alloc(0) let password: Buffer | null = null
try { try {
password = fs.readFileSync(path) password = fs.readFileSync(path)
} catch { } catch {
} }
if (password.length === 0) { if (!password || password.length === 0) {
this.log("no wallet password configured, using wallet secret") this.log("no wallet password configured, using wallet secret")
const secret = this.GetWalletSecret(false) const secret = this.GetWalletSecret(false)
if (secret === "") { if (secret === "") {

View file

@ -173,7 +173,12 @@ export class Watchdog {
StartCheck = async () => { StartCheck = async () => {
this.latestCheckStart = Date.now() 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() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance) this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance() const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance()

View file

@ -241,12 +241,12 @@ export default class Handler {
ops.outgoingInvoices.forEach(i => { 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 }) 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 totalSpent += i.paid_amount
feesInRange += i.service_fees feesInRange += (i.service_fees - i.routing_fees)
}) })
ops.outgoingTransactions.forEach(tx => { 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 }) 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 totalSpent += tx.paid_amount
feesInRange += tx.service_fees feesInRange += (tx.service_fees - tx.chain_fees)
}) })
ops.userToUser.forEach(op => { ops.userToUser.forEach(op => {

View file

@ -2,7 +2,7 @@
import WebSocket from 'ws' import WebSocket from 'ws'
Object.assign(global, { WebSocket: WebSocket }); Object.assign(global, { WebSocket: WebSocket });
import crypto from 'crypto' 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 { ERROR, getLogger } from '../helpers/logger.js'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js'
@ -26,6 +26,7 @@ export type NostrSettings = {
relays: string[] relays: string[]
clients: ClientInfo[] clients: ClientInfo[]
maxEventContentLength: number maxEventContentLength: number
providerDestinationPub: string
} }
export type NostrEvent = { export type NostrEvent = {
@ -69,9 +70,14 @@ type ProcessMetricsResponse = {
type: 'processMetrics' type: 'processMetrics'
metrics: ProcessMetrics metrics: ProcessMetrics
} }
type BeaconResponse = {
type: 'beacon'
content: string
pub: string
}
export type ChildProcessRequest = SettingsRequest | SendRequest | PingRequest 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) => { const send = (message: ChildProcessResponse) => {
if (process.send) { if (process.send) {
process.send(message, undefined, undefined, err => { process.send(message, undefined, undefined, err => {
@ -218,18 +224,28 @@ export default class Handler {
appIds: appIds, appIds: appIds,
listeningForPubkeys: appIds listeningForPubkeys: appIds
}) })
const subs: Filter[] = [
return relay.subscribe([
{ {
since: Math.ceil(Date.now() / 1000), since: Math.ceil(Date.now() / 1000),
kinds: supportedKinds, kinds: supportedKinds,
'#p': appIds, '#p': appIds,
} }
], { ]
if (this.settings.providerDestinationPub) {
subs.push({
kinds: [30078], '#d': ['Lightning.Pub'],
authors: [this.settings.providerDestinationPub]
})
}
return relay.subscribe(subs, {
oneose: () => { oneose: () => {
this.log("up to date with nostr events") this.log("up to date with nostr events")
}, },
onevent: async (e) => { 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) { if (!supportedKinds.includes(e.kind) || !e.pubkey) {
return return
} }

View file

@ -3,7 +3,7 @@ import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, S
import { Utils } from '../helpers/utilsWrapper.js' import { Utils } from '../helpers/utilsWrapper.js'
import { getLogger, ERROR } from '../helpers/logger.js' import { getLogger, ERROR } from '../helpers/logger.js'
type EventCallback = (event: NostrEvent) => void type EventCallback = (event: NostrEvent) => void
type BeaconCallback = (beacon: { content: string, pub: string }) => void
@ -13,7 +13,7 @@ export default class NostrSubprocess {
utils: Utils utils: Utils
awaitingPongs: (() => void)[] = [] awaitingPongs: (() => void)[] = []
log = getLogger({}) log = getLogger({})
constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback) { constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) {
this.utils = utils this.utils = utils
this.childProcess = fork("./build/src/services/nostr/handler") this.childProcess = fork("./build/src/services/nostr/handler")
this.childProcess.on("error", (error) => { this.childProcess.on("error", (error) => {
@ -43,6 +43,9 @@ export default class NostrSubprocess {
this.awaitingPongs.forEach(resolve => resolve()) this.awaitingPongs.forEach(resolve => resolve())
this.awaitingPongs = [] this.awaitingPongs = []
break break
case 'beacon':
beaconCallback({ content: message.content, pub: message.pub })
break
default: default:
console.error("unknown nostr event response", message) console.error("unknown nostr event response", message)
break; break;

View file

@ -422,8 +422,8 @@ export default class {
async VerifyDbEvent(e: LoggedEvent) { async VerifyDbEvent(e: LoggedEvent) {
switch (e.type) { switch (e.type) {
case "new_invoice": /* case "new_invoice":
return orFail(this.dbs.FindOne<UserReceivingInvoice>('UserReceivingInvoice', { where: { invoice: e.data, user: { user_id: e.userId } } })) return orFail(this.dbs.FindOne<UserReceivingInvoice>('UserReceivingInvoice', { where: { invoice: e.data, user: { user_id: e.userId } } })) */
case 'new_address': case 'new_address':
return orFail(this.dbs.FindOne<UserReceivingAddress>('UserReceivingAddress', { where: { address: e.data, user: { user_id: e.userId } } })) return orFail(this.dbs.FindOne<UserReceivingAddress>('UserReceivingAddress', { where: { address: e.data, user: { user_id: e.userId } } }))
case 'invoice_paid': case 'invoice_paid':

View file

@ -19,7 +19,7 @@ export class TlvStorageFactory extends EventEmitter {
private debug: boolean = false; private debug: boolean = false;
private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') } private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') }
private allowResetMetricsStorages: boolean private allowResetMetricsStorages: boolean
log = getLogger({component: 'TlvStorageFactory'}) log = getLogger({ component: 'TlvStorageFactory' })
constructor(allowResetMetricsStorages: boolean) { constructor(allowResetMetricsStorages: boolean) {
super(); super();
this.allowResetMetricsStorages = allowResetMetricsStorages this.allowResetMetricsStorages = allowResetMetricsStorages
@ -134,10 +134,15 @@ export class TlvStorageFactory extends EventEmitter {
return this.handleOp<Types.WebRtcAnswer>(op) return this.handleOp<Types.WebRtcAnswer>(op)
} }
ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise<void> { async ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise<void> {
const opId = Math.random().toString() const opId = Math.random().toString()
const op: ProcessMetricsTlvOperation = { type: 'processMetrics', opId, metrics, processName } const op: ProcessMetricsTlvOperation = { type: 'processMetrics', opId, metrics, processName }
return this.handleOp<void>(op) try {
return this.handleOp<void>(op)
} catch (error: any) {
this.log(ERROR, 'Error processing metrics', error.message)
}
return
} }

View file

@ -5,7 +5,7 @@ DATABASE_FILE=db.sqlite
JWT_SECRET=bigsecrethere JWT_SECRET=bigsecrethere
ALLOW_BALANCE_MIGRATION=true ALLOW_BALANCE_MIGRATION=true
OUTBOUND_MAX_FEE_BPS=60 OUTBOUND_MAX_FEE_BPS=60
OUTBOUND_MAX_FEE_EXTRA_SATS=100 OUTBOUND_MAX_FEE_EXTRA_SATS=10
INCOMING_CHAIN_FEE_ROOT_BPS=0 INCOMING_CHAIN_FEE_ROOT_BPS=0
OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets
INCOMING_INVOICE_FEE_ROOT_BPS=0 INCOMING_INVOICE_FEE_ROOT_BPS=0

View file

@ -24,11 +24,10 @@ const testSuccessfulExternalPayment = async (T: TestBase) => {
T.d("paid 500 sats invoice from user1") T.d("paid 500 sats invoice from user1")
const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId)
const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id)
expect(u1.balance_sats).to.be.equal(1496) expect(u1.balance_sats).to.be.equal(1490)
T.d("user1 balance is now 1496 (2000 - (500 + 3 fee + 1 routing))") T.d("user1 balance is now 1490 (2000 - (500 + 10fee))")
expect(owner.balance_sats).to.be.equal(3) expect(owner.balance_sats).to.be.equal(9)
T.d("app balance is 3 sats") T.d("app balance is 9 sats")
} }
const testFailedExternalPayment = async (T: TestBase) => { 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") 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") T.d("payment failed as expected, with the expected error message")
const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId)
expect(u1.balance_sats).to.be.equal(1496) expect(u1.balance_sats).to.be.equal(1490)
T.d("user1 balance is still 1496") T.d("user1 balance is still 1490")
const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id)
expect(owner.balance_sats).to.be.equal(3) expect(owner.balance_sats).to.be.equal(9)
T.d("app balance is still 3 sats") T.d("app balance is still 9 sats")
} }
const testSuccesfulReceivedExternalChainPayment = async (T: TestBase) => { const testSuccesfulReceivedExternalChainPayment = async (T: TestBase) => {

View file

@ -23,10 +23,10 @@ const testSuccessfulInternalPayment = async (T: TestBase) => {
const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id)
expect(u2.balance_sats).to.be.equal(1000) expect(u2.balance_sats).to.be.equal(1000)
T.d("user2 balance is 1000") T.d("user2 balance is 1000")
expect(u1.balance_sats).to.be.equal(994) expect(u1.balance_sats).to.be.equal(990)
T.d("user1 balance is 994 cuz he paid 6 sats fee") T.d("user1 balance is 990 cuz he paid 10 sats fee")
expect(owner.balance_sats).to.be.equal(6) expect(owner.balance_sats).to.be.equal(10)
T.d("app balance is 6 sats") T.d("app balance is 10 sats")
} }
const testFailedInternalPayment = async (T: TestBase) => { const testFailedInternalPayment = async (T: TestBase) => {

View file

@ -21,30 +21,30 @@ export default async (T: TestBase) => {
const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => { const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => {
T.d("starting testInboundPaymentFromProvider") 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)) 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 }) 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.expect(userBalance.balance).to.equal(3000)
T.d("user balance is 2000") T.d("user balance is 3000")
const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance()
T.expect(providerBalance).to.equal(2000) T.expect(providerBalance).to.equal(3000)
T.d("provider balance is 2000") T.d("provider balance is 3000")
T.d("testInboundPaymentFromProvider done") T.d("testInboundPaymentFromProvider done")
} }
const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => { const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => {
T.d("starting testOutboundPaymentFromProvider") 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 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 res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 })
const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx) 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() 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") T.d("testOutboundPaymentFromProvider done")
} }

View file

@ -24,8 +24,8 @@ export const setupNetwork = async (): Promise<ChainTools> => {
const lndNodeSettings = LoadLndNodeSettingsFromEnv({}) const lndNodeSettings = LoadLndNodeSettingsFromEnv({})
const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false } const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false }
const alice = new LND(() => ({ lndSettings, lndNodeSettings }), 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 () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const peers = await alice.ListPeers() const peers = await alice.ListPeers()
if (peers.peers.length > 0) { if (peers.peers.length > 0) {

View file

@ -45,7 +45,7 @@ export const initBootstrappedInstance = async (T: TestBase) => {
bootstrapped.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) bootstrapped.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey })
await new Promise<void>(res => { await new Promise<void>(res => {
const interval = setInterval(async () => { const interval = setInterval(async () => {
const canHandle = await bootstrapped.liquidityProvider.CanProviderHandle({ action: 'receive', amount: 2000 }) const canHandle = bootstrapped.liquidityProvider.IsReady()
if (canHandle) { if (canHandle) {
clearInterval(interval) clearInterval(interval)
res() res()

View file

@ -29,16 +29,15 @@ const testSpamExternalPayment = async (T: TestBase) => {
const failedPayments = res.filter(r => !r.success) const failedPayments = res.filter(r => !r.success)
console.log(failedPayments) console.log(failedPayments)
failedPayments.forEach(f => expect(f.err).to.be.equal("not enough balance to decrement")) 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(successfulPayments.length).to.be.equal(3)
expect(failedPayments.length).to.be.equal(7) expect(failedPayments.length).to.be.equal(7)
T.d("3 payments succeeded, 7 failed as expected") T.d("3 payments succeeded, 7 failed as expected")
const u = await T.main.storage.userStorage.GetUser(T.user1.userId) const u = await T.main.storage.userStorage.GetUser(T.user1.userId)
const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id)
expect(u.balance_sats).to.be.equal(488) expect(u.balance_sats).to.be.equal(470)
T.d("user1 balance is now 488 (2000 - (500 + 3 fee + 1 routing) * 3)") T.d("user1 balance is now 470 (2000 - (500 + 10 fee) * 3)")
expect(owner.balance_sats).to.be.equal(9) expect(owner.balance_sats).to.be.equal(27)
T.d("app balance is 9 sats") T.d("app balance is 27 sats")
} }

View file

@ -87,12 +87,12 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise<Te
const lndSettings = LoadLndSettingsFromEnv({}) const lndSettings = LoadLndSettingsFromEnv({})
const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const otherLndSetting = () => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }) const otherLndSetting = () => ({ 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() await externalAccessToOtherLnd.Warmup()
const thirdLndNodeSettings = LoadThirdLndSettingsFromEnv() const thirdLndNodeSettings = LoadThirdLndSettingsFromEnv()
const thirdLndSetting = () => ({ lndSettings, lndNodeSettings: thirdLndNodeSettings }) 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() await externalAccessToThirdLnd.Warmup()