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