fixies
This commit is contained in:
parent
fd7a5eb1d6
commit
e2dec7d9b3
14 changed files with 138 additions and 49 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
#LND_CERT_PATH=~/.lnd/tls.cert
|
#LND_CERT_PATH=~/.lnd/tls.cert
|
||||||
#LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
#LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||||
#LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log
|
#LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log
|
||||||
|
#BTC_NETWORK=mainnet
|
||||||
|
|
||||||
#BOOTSTRAP_PEER
|
#BOOTSTRAP_PEER
|
||||||
# A trusted peer that will hold a node-level account until channel automation becomes affordable
|
# A trusted peer that will hold a node-level account until channel automation becomes affordable
|
||||||
|
|
@ -49,8 +50,9 @@
|
||||||
#LIGHTNING
|
#LIGHTNING
|
||||||
# Maximum amount in network fees passed to LND when it pays an external invoice
|
# Maximum amount in network fees passed to LND when it pays an external invoice
|
||||||
# BPS are basis points, 100 BPS = 1%
|
# BPS are basis points, 100 BPS = 1%
|
||||||
#OUTBOUND_MAX_FEE_BPS=60
|
#OUTBOUND_MAX_FEE_BPS=60 // deprecated
|
||||||
#OUTBOUND_MAX_FEE_EXTRA_SATS=100
|
#OUTBOUND_MAX_FEE_EXTRA_SATS=100 // deprecated use OUTBOUND_FEE_FLOOR_SATS instead
|
||||||
|
#OUTBOUND_FEE_FLOOR_SATS=10
|
||||||
# If the back-end doesn't have adequate channel capacity, buy one from an LSP
|
# If the back-end doesn't have adequate channel capacity, buy one from an LSP
|
||||||
# Will execute when it costs less than 1% of balance and uses a trusted peer
|
# Will execute when it costs less than 1% of balance and uses a trusted peer
|
||||||
#BOOTSTRAP=1
|
#BOOTSTRAP=1
|
||||||
|
|
|
||||||
|
|
@ -1169,7 +1169,7 @@ The nostr server will send back a message response, and inside the body there wi
|
||||||
- __invitation_link__: _string_
|
- __invitation_link__: _string_
|
||||||
|
|
||||||
### CumulativeFees
|
### CumulativeFees
|
||||||
- __networkFeeFixed__: _number_
|
- __outboundFeeFloor__: _number_
|
||||||
- __serviceFeeBps__: _number_
|
- __serviceFeeBps__: _number_
|
||||||
|
|
||||||
### DebitAuthorization
|
### DebitAuthorization
|
||||||
|
|
@ -1601,6 +1601,7 @@ The nostr server will send back a message response, and inside the body there wi
|
||||||
- __swap_operation_id__: _string_
|
- __swap_operation_id__: _string_
|
||||||
|
|
||||||
### SwapsList
|
### SwapsList
|
||||||
|
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_
|
||||||
- __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_
|
- __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_
|
||||||
|
|
||||||
### TransactionSwapQuote
|
### TransactionSwapQuote
|
||||||
|
|
|
||||||
|
|
@ -230,8 +230,8 @@ type CreateOneTimeInviteLinkResponse struct {
|
||||||
Invitation_link string `json:"invitation_link"`
|
Invitation_link string `json:"invitation_link"`
|
||||||
}
|
}
|
||||||
type CumulativeFees struct {
|
type CumulativeFees struct {
|
||||||
Networkfeefixed int64 `json:"networkFeeFixed"`
|
Outboundfeefloor int64 `json:"outboundFeeFloor"`
|
||||||
Servicefeebps int64 `json:"serviceFeeBps"`
|
Servicefeebps int64 `json:"serviceFeeBps"`
|
||||||
}
|
}
|
||||||
type DebitAuthorization struct {
|
type DebitAuthorization struct {
|
||||||
Authorized bool `json:"authorized"`
|
Authorized bool `json:"authorized"`
|
||||||
|
|
@ -662,7 +662,8 @@ type SwapOperation struct {
|
||||||
Swap_operation_id string `json:"swap_operation_id"`
|
Swap_operation_id string `json:"swap_operation_id"`
|
||||||
}
|
}
|
||||||
type SwapsList struct {
|
type SwapsList struct {
|
||||||
Swaps []SwapOperation `json:"swaps"`
|
Quotes []TransactionSwapQuote `json:"quotes"`
|
||||||
|
Swaps []SwapOperation `json:"swaps"`
|
||||||
}
|
}
|
||||||
type TransactionSwapQuote struct {
|
type TransactionSwapQuote struct {
|
||||||
Chain_fee_sats int64 `json:"chain_fee_sats"`
|
Chain_fee_sats int64 `json:"chain_fee_sats"`
|
||||||
|
|
|
||||||
|
|
@ -1303,21 +1303,21 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CumulativeFees = {
|
export type CumulativeFees = {
|
||||||
networkFeeFixed: number
|
outboundFeeFloor: number
|
||||||
serviceFeeBps: number
|
serviceFeeBps: number
|
||||||
}
|
}
|
||||||
export const CumulativeFeesOptionalFields: [] = []
|
export const CumulativeFeesOptionalFields: [] = []
|
||||||
export type CumulativeFeesOptions = OptionsBaseMessage & {
|
export type CumulativeFeesOptions = OptionsBaseMessage & {
|
||||||
checkOptionalsAreSet?: []
|
checkOptionalsAreSet?: []
|
||||||
networkFeeFixed_CustomCheck?: (v: number) => boolean
|
outboundFeeFloor_CustomCheck?: (v: number) => boolean
|
||||||
serviceFeeBps_CustomCheck?: (v: number) => boolean
|
serviceFeeBps_CustomCheck?: (v: number) => boolean
|
||||||
}
|
}
|
||||||
export const CumulativeFeesValidate = (o?: CumulativeFees, opts: CumulativeFeesOptions = {}, path: string = 'CumulativeFees::root.'): Error | null => {
|
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 (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
|
||||||
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
|
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
|
||||||
|
|
||||||
if (typeof o.networkFeeFixed !== 'number') return new Error(`${path}.networkFeeFixed: is not a number`)
|
if (typeof o.outboundFeeFloor !== 'number') return new Error(`${path}.outboundFeeFloor: is not a number`)
|
||||||
if (opts.networkFeeFixed_CustomCheck && !opts.networkFeeFixed_CustomCheck(o.networkFeeFixed)) return new Error(`${path}.networkFeeFixed: custom check failed`)
|
if (opts.outboundFeeFloor_CustomCheck && !opts.outboundFeeFloor_CustomCheck(o.outboundFeeFloor)) return new Error(`${path}.outboundFeeFloor: custom check failed`)
|
||||||
|
|
||||||
if (typeof o.serviceFeeBps !== 'number') return new Error(`${path}.serviceFeeBps: is not a number`)
|
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`)
|
if (opts.serviceFeeBps_CustomCheck && !opts.serviceFeeBps_CustomCheck(o.serviceFeeBps)) return new Error(`${path}.serviceFeeBps: custom check failed`)
|
||||||
|
|
@ -3887,11 +3887,14 @@ export const SwapOperationValidate = (o?: SwapOperation, opts: SwapOperationOpti
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SwapsList = {
|
export type SwapsList = {
|
||||||
|
quotes: TransactionSwapQuote[]
|
||||||
swaps: SwapOperation[]
|
swaps: SwapOperation[]
|
||||||
}
|
}
|
||||||
export const SwapsListOptionalFields: [] = []
|
export const SwapsListOptionalFields: [] = []
|
||||||
export type SwapsListOptions = OptionsBaseMessage & {
|
export type SwapsListOptions = OptionsBaseMessage & {
|
||||||
checkOptionalsAreSet?: []
|
checkOptionalsAreSet?: []
|
||||||
|
quotes_ItemOptions?: TransactionSwapQuoteOptions
|
||||||
|
quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean
|
||||||
swaps_ItemOptions?: SwapOperationOptions
|
swaps_ItemOptions?: SwapOperationOptions
|
||||||
swaps_CustomCheck?: (v: SwapOperation[]) => boolean
|
swaps_CustomCheck?: (v: SwapOperation[]) => boolean
|
||||||
}
|
}
|
||||||
|
|
@ -3899,6 +3902,13 @@ export const SwapsListValidate = (o?: SwapsList, opts: SwapsListOptions = {}, pa
|
||||||
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
|
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
|
||||||
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
|
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
|
||||||
|
|
||||||
|
if (!Array.isArray(o.quotes)) return new Error(`${path}.quotes: is not an array`)
|
||||||
|
for (let index = 0; index < o.quotes.length; index++) {
|
||||||
|
const quotesErr = TransactionSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`)
|
||||||
|
if (quotesErr !== null) return quotesErr
|
||||||
|
}
|
||||||
|
if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`)
|
||||||
|
|
||||||
if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`)
|
if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`)
|
||||||
for (let index = 0; index < o.swaps.length; index++) {
|
for (let index = 0; index < o.swaps.length; index++) {
|
||||||
const swapsErr = SwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`)
|
const swapsErr = SwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`)
|
||||||
|
|
|
||||||
|
|
@ -853,10 +853,11 @@ message SwapOperation {
|
||||||
|
|
||||||
message SwapsList {
|
message SwapsList {
|
||||||
repeated SwapOperation swaps = 1;
|
repeated SwapOperation swaps = 1;
|
||||||
|
repeated TransactionSwapQuote quotes = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CumulativeFees {
|
message CumulativeFees {
|
||||||
int64 networkFeeFixed = 2;
|
int64 outboundFeeFloor = 2;
|
||||||
int64 serviceFeeBps = 3;
|
int64 serviceFeeBps = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import ws from 'ws';
|
||||||
import { getLogger, PubLogger, ERROR } from '../helpers/logger.js';
|
import { getLogger, PubLogger, ERROR } from '../helpers/logger.js';
|
||||||
import SettingsManager from '../main/settingsManager.js';
|
import SettingsManager from '../main/settingsManager.js';
|
||||||
import * as Types from '../../../proto/autogenerated/ts/types.js';
|
import * as Types from '../../../proto/autogenerated/ts/types.js';
|
||||||
|
import { BTCNetwork } from '../main/settings.js';
|
||||||
|
|
||||||
type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string }
|
type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string }
|
||||||
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
|
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
|
||||||
|
|
@ -43,7 +44,6 @@ type TransactionSwapResponse = {
|
||||||
}
|
}
|
||||||
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
|
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
|
||||||
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
|
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
|
||||||
const network = Networks.bitcoinMainnet
|
|
||||||
export class Swaps {
|
export class Swaps {
|
||||||
reverseSwaps: ReverseSwaps
|
reverseSwaps: ReverseSwaps
|
||||||
submarineSwaps: SubmarineSwaps
|
submarineSwaps: SubmarineSwaps
|
||||||
|
|
@ -52,6 +52,8 @@ export class Swaps {
|
||||||
this.submarineSwaps = new SubmarineSwaps(settings)
|
this.submarineSwaps = new SubmarineSwaps(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stop = () => { }
|
||||||
|
|
||||||
GetKeys = (privateKey: string) => {
|
GetKeys = (privateKey: string) => {
|
||||||
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
|
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
|
||||||
return keys
|
return keys
|
||||||
|
|
@ -230,17 +232,11 @@ export class ReverseSwaps {
|
||||||
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/reverse`
|
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/reverse`
|
||||||
const req: any = {
|
const req: any = {
|
||||||
onchainAmount: txAmount,
|
onchainAmount: txAmount,
|
||||||
// invoiceAmount,
|
|
||||||
to: 'BTC',
|
to: 'BTC',
|
||||||
from: 'BTC',
|
from: 'BTC',
|
||||||
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
|
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
|
||||||
preimageHash: createHash('sha256').update(preimage).digest('hex'),
|
preimageHash: createHash('sha256').update(preimage).digest('hex'),
|
||||||
}
|
}
|
||||||
/* if (amount.type === Types.TransactionSwapRequest_amount_type.INVOICE_AMOUNT_SATS) {
|
|
||||||
req.invoiceAmount = amount.invoice_amount_sats
|
|
||||||
} else if (amount.type === Types.TransactionSwapRequest_amount_type.TRANSACTION_AMOUNT_SATS) {
|
|
||||||
req.onchainAmount = amount.transaction_amount_sats
|
|
||||||
} */
|
|
||||||
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
|
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
|
||||||
if (!createdResponseRes.ok) {
|
if (!createdResponseRes.ok) {
|
||||||
return createdResponseRes
|
return createdResponseRes
|
||||||
|
|
@ -262,11 +258,21 @@ export class ReverseSwaps {
|
||||||
webSocket.on('open', () => {
|
webSocket.on('open', () => {
|
||||||
webSocket.send(JSON.stringify(subReq))
|
webSocket.send(JSON.stringify(subReq))
|
||||||
})
|
})
|
||||||
let txId = ""
|
let txId = "", isDone = false
|
||||||
const done = () => {
|
const done = () => {
|
||||||
|
isDone = true
|
||||||
webSocket.close()
|
webSocket.close()
|
||||||
swapDone({ ok: true, txId })
|
swapDone({ ok: true, txId })
|
||||||
}
|
}
|
||||||
|
webSocket.on('error', (err) => {
|
||||||
|
this.log(ERROR, 'Error in WebSocket', err.message)
|
||||||
|
})
|
||||||
|
webSocket.on('close', () => {
|
||||||
|
if (!isDone) {
|
||||||
|
this.log(ERROR, 'WebSocket closed before swap was done');
|
||||||
|
swapDone({ ok: false, error: 'WebSocket closed before swap was done' })
|
||||||
|
}
|
||||||
|
})
|
||||||
webSocket.on('message', async (rawMsg) => {
|
webSocket.on('message', async (rawMsg) => {
|
||||||
try {
|
try {
|
||||||
const result = await this.handleSwapTransactionMessage(rawMsg, data, done)
|
const result = await this.handleSwapTransactionMessage(rawMsg, data, done)
|
||||||
|
|
@ -275,6 +281,7 @@ export class ReverseSwaps {
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
|
this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
|
||||||
|
isDone = true
|
||||||
webSocket.close()
|
webSocket.close()
|
||||||
swapDone({ ok: false, error: err.message })
|
swapDone({ ok: false, error: err.message })
|
||||||
return
|
return
|
||||||
|
|
@ -336,7 +343,7 @@ export class ReverseSwaps {
|
||||||
this.log(ERROR, 'No swap output found in lockup transaction');
|
this.log(ERROR, 'No swap output found in lockup transaction');
|
||||||
return { ok: false, error: 'No swap output found in lockup transaction' }
|
return { ok: false, error: 'No swap output found in lockup transaction' }
|
||||||
}
|
}
|
||||||
|
const network = getNetwork(this.settings.getSettings().lndSettings.network)
|
||||||
// Create a claim transaction to be signed cooperatively via a key path spend
|
// Create a claim transaction to be signed cooperatively via a key path spend
|
||||||
const claimTx = constructClaimTransaction(
|
const claimTx = constructClaimTransaction(
|
||||||
[
|
[
|
||||||
|
|
@ -437,3 +444,16 @@ const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, da
|
||||||
return { ok: false, error: err.message }
|
return { ok: false, error: err.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNetwork = (network: BTCNetwork): Network => {
|
||||||
|
switch (network) {
|
||||||
|
case 'mainnet':
|
||||||
|
return Networks.bitcoinMainnet
|
||||||
|
case 'testnet':
|
||||||
|
return Networks.bitcoinTestnet
|
||||||
|
case 'regtest':
|
||||||
|
return Networks.bitcoinRegtest
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid network: ${network}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,14 +69,14 @@ export default class {
|
||||||
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
|
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing
|
||||||
}
|
}
|
||||||
const nostrSettings = this.settings.getSettings().nostrRelaySettings
|
const nostrSettings = this.settings.getSettings().nostrRelaySettings
|
||||||
const { max, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats)
|
const { max, outboundFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats)
|
||||||
return {
|
return {
|
||||||
userId: ctx.user_id,
|
userId: ctx.user_id,
|
||||||
balance: user.balance_sats,
|
balance: user.balance_sats,
|
||||||
max_withdrawable: max,
|
max_withdrawable: max,
|
||||||
user_identifier: appUser.identifier,
|
user_identifier: appUser.identifier,
|
||||||
network_max_fee_bps: 0,
|
network_max_fee_bps: 0,
|
||||||
network_max_fee_fixed: networkFeeFixed,
|
network_max_fee_fixed: outboundFeeFloor,
|
||||||
service_fee_bps: serviceFeeBps,
|
service_fee_bps: serviceFeeBps,
|
||||||
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||||
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }),
|
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }),
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ export default class {
|
||||||
|
|
||||||
const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] })
|
const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] })
|
||||||
log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString })
|
log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString })
|
||||||
const { max, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats)
|
const { max, outboundFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats)
|
||||||
return {
|
return {
|
||||||
identifier: u.identifier,
|
identifier: u.identifier,
|
||||||
info: {
|
info: {
|
||||||
|
|
@ -176,7 +176,7 @@ export default class {
|
||||||
max_withdrawable: max,
|
max_withdrawable: max,
|
||||||
user_identifier: u.identifier,
|
user_identifier: u.identifier,
|
||||||
network_max_fee_bps: 0,
|
network_max_fee_bps: 0,
|
||||||
network_max_fee_fixed: networkFeeFixed,
|
network_max_fee_fixed: outboundFeeFloor,
|
||||||
service_fee_bps: serviceFeeBps,
|
service_fee_bps: serviceFeeBps,
|
||||||
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||||
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
|
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
|
||||||
|
|
@ -225,14 +225,14 @@ export default class {
|
||||||
const app = await this.storage.applicationStorage.GetApplication(appId)
|
const app = await this.storage.applicationStorage.GetApplication(appId)
|
||||||
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
|
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
|
||||||
const nostrSettings = this.settings.getSettings().nostrRelaySettings
|
const nostrSettings = this.settings.getSettings().nostrRelaySettings
|
||||||
const { max, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats)
|
const { max, outboundFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats)
|
||||||
return {
|
return {
|
||||||
max_withdrawable: max, identifier: req.user_identifier, info: {
|
max_withdrawable: max, identifier: req.user_identifier, info: {
|
||||||
userId: user.user.user_id, balance: user.user.balance_sats,
|
userId: user.user.user_id, balance: user.user.balance_sats,
|
||||||
max_withdrawable: max,
|
max_withdrawable: max,
|
||||||
user_identifier: user.identifier,
|
user_identifier: user.identifier,
|
||||||
network_max_fee_bps: 0,
|
network_max_fee_bps: 0,
|
||||||
network_max_fee_fixed: networkFeeFixed,
|
network_max_fee_fixed: outboundFeeFloor,
|
||||||
service_fee_bps: serviceFeeBps,
|
service_fee_bps: serviceFeeBps,
|
||||||
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }),
|
||||||
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
|
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,8 @@ export default class {
|
||||||
NewBlockHandler = async (height: number) => {
|
NewBlockHandler = async (height: number) => {
|
||||||
let confirmed: (PendingTx & { confs: number; })[]
|
let confirmed: (PendingTx & { confs: number; })[]
|
||||||
let log = getLogger({})
|
let log = getLogger({})
|
||||||
|
this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height)
|
||||||
|
.catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err))
|
||||||
try {
|
try {
|
||||||
const balanceEvents = await this.paymentManager.GetLndBalance()
|
const balanceEvents = await this.paymentManager.GetLndBalance()
|
||||||
await this.metricsManager.NewBlockCb(height, balanceEvents)
|
await this.metricsManager.NewBlockCb(height, balanceEvents)
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export class LiquidityProvider {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
this.feesCache = {
|
this.feesCache = {
|
||||||
networkFeeFixed: res.network_max_fee_fixed,
|
outboundFeeFloor: res.network_max_fee_fixed,
|
||||||
serviceFeeBps: res.service_fee_bps
|
serviceFeeBps: res.service_fee_bps
|
||||||
}
|
}
|
||||||
this.latestReceivedBalance = res.balance
|
this.latestReceivedBalance = res.balance
|
||||||
|
|
@ -152,11 +152,11 @@ export class LiquidityProvider {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
const balance = this.latestReceivedBalance
|
const balance = this.latestReceivedBalance
|
||||||
const { networkFeeFixed, serviceFeeBps } = this.feesCache
|
const { outboundFeeFloor, serviceFeeBps } = this.feesCache
|
||||||
const div = 1 + (serviceFeeBps / 10000)
|
const div = 1 + (serviceFeeBps / 10000)
|
||||||
const maxWithoutFixed = Math.floor(balance / div)
|
const maxWithoutFixed = Math.floor(balance / div)
|
||||||
const fee = balance - maxWithoutFixed
|
const fee = balance - maxWithoutFixed
|
||||||
return balance - Math.max(fee, networkFeeFixed)
|
return balance - Math.max(fee, outboundFeeFloor)
|
||||||
}
|
}
|
||||||
|
|
||||||
GetLatestBalance = () => {
|
GetLatestBalance = () => {
|
||||||
|
|
@ -174,7 +174,7 @@ export class LiquidityProvider {
|
||||||
const fees = f ? f : this.GetFees()
|
const fees = f ? f : this.GetFees()
|
||||||
const serviceFeeRate = fees.serviceFeeBps / 10000
|
const serviceFeeRate = fees.serviceFeeBps / 10000
|
||||||
const serviceFee = Math.ceil(serviceFeeRate * amount)
|
const serviceFee = Math.ceil(serviceFeeRate * amount)
|
||||||
return Math.max(serviceFee, fees.networkFeeFixed)
|
return Math.max(serviceFee, fees.outboundFeeFloor)
|
||||||
}
|
}
|
||||||
|
|
||||||
CanProviderPay = async (amount: number, localServiceFee: number): Promise<boolean> => {
|
CanProviderPay = async (amount: number, localServiceFee: number): Promise<boolean> => {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ export default class {
|
||||||
liquidityManager: LiquidityManager
|
liquidityManager: LiquidityManager
|
||||||
utils: Utils
|
utils: Utils
|
||||||
swaps: Swaps
|
swaps: Swaps
|
||||||
|
invoiceLock: InvoiceLock
|
||||||
constructor(storage: Storage, lnd: LND, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
|
constructor(storage: Storage, lnd: LND, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
|
||||||
this.storage = storage
|
this.storage = storage
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
|
|
@ -65,9 +66,13 @@ export default class {
|
||||||
this.swaps = new Swaps(settings)
|
this.swaps = new Swaps(settings)
|
||||||
this.addressPaidCb = addressPaidCb
|
this.addressPaidCb = addressPaidCb
|
||||||
this.invoicePaidCb = invoicePaidCb
|
this.invoicePaidCb = invoicePaidCb
|
||||||
|
this.invoiceLock = new InvoiceLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Stop() {
|
Stop() {
|
||||||
this.watchDog.Stop()
|
this.watchDog.Stop()
|
||||||
|
this.swaps.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPendingPayments = async () => {
|
checkPendingPayments = async () => {
|
||||||
|
|
@ -192,7 +197,7 @@ export default class {
|
||||||
throw new Error("Sending a transaction is not supported")
|
throw new Error("Sending a transaction is not supported")
|
||||||
case Types.UserOperationType.OUTGOING_INVOICE:
|
case Types.UserOperationType.OUTGOING_INVOICE:
|
||||||
const fee = this.getInvoicePaymentServiceFee(amount, appUser)
|
const fee = this.getInvoicePaymentServiceFee(amount, appUser)
|
||||||
return Math.max(fee, this.settings.getSettings().lndSettings.feeFixedLimit)
|
return Math.max(fee, this.settings.getSettings().lndSettings.outboundFeeFloor)
|
||||||
case Types.UserOperationType.OUTGOING_USER_TO_USER:
|
case Types.UserOperationType.OUTGOING_USER_TO_USER:
|
||||||
if (appUser) {
|
if (appUser) {
|
||||||
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
|
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
|
||||||
|
|
@ -251,17 +256,17 @@ export default class {
|
||||||
|
|
||||||
GetFees = (): Types.CumulativeFees => {
|
GetFees = (): Types.CumulativeFees => {
|
||||||
const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings
|
const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings
|
||||||
const { feeFixedLimit } = this.settings.getSettings().lndSettings
|
const { outboundFeeFloor } = this.settings.getSettings().lndSettings
|
||||||
return { networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
|
return { outboundFeeFloor, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
|
||||||
}
|
}
|
||||||
|
|
||||||
GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
|
GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
|
||||||
const { networkFeeFixed, serviceFeeBps } = this.GetFees()
|
const { outboundFeeFloor, serviceFeeBps } = this.GetFees()
|
||||||
const div = 1 + (serviceFeeBps / 10000)
|
const div = 1 + (serviceFeeBps / 10000)
|
||||||
const maxWithoutFixed = Math.floor(balance / div)
|
const maxWithoutFixed = Math.floor(balance / div)
|
||||||
const fee = balance - maxWithoutFixed
|
const fee = balance - maxWithoutFixed
|
||||||
const max = balance - Math.max(fee, networkFeeFixed)
|
const max = balance - Math.max(fee, outboundFeeFloor)
|
||||||
return { max, networkFeeFixed, serviceFeeBps }
|
return { max, outboundFeeFloor, serviceFeeBps }
|
||||||
}
|
}
|
||||||
async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise<Types.DecodeInvoiceResponse> {
|
async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise<Types.DecodeInvoiceResponse> {
|
||||||
const decoded = await this.lnd.DecodeInvoice(req.invoice)
|
const decoded = await this.lnd.DecodeInvoice(req.invoice)
|
||||||
|
|
@ -277,10 +282,10 @@ export default class {
|
||||||
throw new Error("user is banned, cannot send payment")
|
throw new Error("user is banned, cannot send payment")
|
||||||
}
|
}
|
||||||
if (req.expected_fees) {
|
if (req.expected_fees) {
|
||||||
const { networkFeeFixed, serviceFeeBps } = req.expected_fees
|
const { outboundFeeFloor, serviceFeeBps } = req.expected_fees
|
||||||
const serviceFixed = this.settings.getSettings().lndSettings.feeFixedLimit
|
const serviceFixed = this.settings.getSettings().lndSettings.outboundFeeFloor
|
||||||
const serviceBps = this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps
|
const serviceBps = this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps
|
||||||
if (serviceFixed !== networkFeeFixed || serviceBps !== serviceFeeBps) {
|
if (serviceFixed !== outboundFeeFloor || serviceBps !== serviceFeeBps) {
|
||||||
throw new Error("fees do not match the expected fees")
|
throw new Error("fees do not match the expected fees")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -303,10 +308,20 @@ export default class {
|
||||||
throw new Error("this invoice was already paid")
|
throw new Error("this invoice was already paid")
|
||||||
}
|
}
|
||||||
let paymentInfo = { preimage: "", amtPaid: 0, networkFee: 0, serialId: 0 }
|
let paymentInfo = { preimage: "", amtPaid: 0, networkFee: 0, serialId: 0 }
|
||||||
if (internalInvoice) {
|
if (this.invoiceLock.isLocked(req.invoice)) {
|
||||||
paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub)
|
throw new Error("this invoice is already being paid")
|
||||||
} else {
|
}
|
||||||
paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, { ...optionals, debitNpub: req.debit_npub })
|
this.invoiceLock.lock(req.invoice)
|
||||||
|
try {
|
||||||
|
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, { ...optionals, debitNpub: req.debit_npub })
|
||||||
|
}
|
||||||
|
this.invoiceLock.unlock(req.invoice)
|
||||||
|
} catch (err) {
|
||||||
|
this.invoiceLock.unlock(req.invoice)
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
const feeDiff = serviceFee - paymentInfo.networkFee
|
const feeDiff = serviceFee - paymentInfo.networkFee
|
||||||
if (isAppUserPayment && feeDiff > 0) {
|
if (isAppUserPayment && feeDiff > 0) {
|
||||||
|
|
@ -456,7 +471,7 @@ export default class {
|
||||||
async PayAddressWithSwap(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
|
async PayAddressWithSwap(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
|
||||||
this.log("paying external address")
|
this.log("paying external address")
|
||||||
if (!req.swap_operation_id) {
|
if (!req.swap_operation_id) {
|
||||||
throw new Error("request a swap quote before payng an external address")
|
throw new Error("request a swap quote before paying an external address")
|
||||||
}
|
}
|
||||||
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
|
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
|
||||||
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(req.swap_operation_id, ctx.app_user_id)
|
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(req.swap_operation_id, ctx.app_user_id)
|
||||||
|
|
@ -547,6 +562,7 @@ export default class {
|
||||||
|
|
||||||
async ListSwaps(ctx: Types.UserContext): Promise<Types.SwapsList> {
|
async ListSwaps(ctx: Types.UserContext): Promise<Types.SwapsList> {
|
||||||
const swaps = await this.storage.paymentStorage.ListCompletedSwaps(ctx.app_user_id)
|
const swaps = await this.storage.paymentStorage.ListCompletedSwaps(ctx.app_user_id)
|
||||||
|
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(ctx.app_user_id)
|
||||||
return {
|
return {
|
||||||
swaps: swaps.map(s => {
|
swaps: swaps.map(s => {
|
||||||
const p = s.payment
|
const p = s.payment
|
||||||
|
|
@ -558,6 +574,17 @@ export default class {
|
||||||
address_paid: s.swap.address_paid,
|
address_paid: s.swap.address_paid,
|
||||||
failure_reason: s.swap.failure_reason,
|
failure_reason: s.swap.failure_reason,
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
quotes: pendingSwaps.map(s => {
|
||||||
|
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, s.invoice_amount, true)
|
||||||
|
return {
|
||||||
|
swap_operation_id: s.swap_operation_id,
|
||||||
|
invoice_amount_sats: s.invoice_amount,
|
||||||
|
transaction_amount_sats: s.transaction_amount,
|
||||||
|
chain_fee_sats: s.chain_fee_sats,
|
||||||
|
service_fee_sats: serviceFee,
|
||||||
|
swap_fee_sats: s.swap_fee_sats,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -948,3 +975,16 @@ export default class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InvoiceLock {
|
||||||
|
locked: Record<string, boolean> = {}
|
||||||
|
lock(invoice: string) {
|
||||||
|
this.locked[invoice] = true
|
||||||
|
}
|
||||||
|
unlock(invoice: string) {
|
||||||
|
delete this.locked[invoice]
|
||||||
|
}
|
||||||
|
isLocked(invoice: string) {
|
||||||
|
return this.locked[invoice]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -76,11 +76,13 @@ export type LndNodeSettings = {
|
||||||
lndCertPath: string // cold setting
|
lndCertPath: string // cold setting
|
||||||
lndMacaroonPath: string // cold setting
|
lndMacaroonPath: string // cold setting
|
||||||
}
|
}
|
||||||
|
const networks = ['mainnet', 'testnet', 'regtest'] as const
|
||||||
|
export type BTCNetwork = (typeof networks)[number]
|
||||||
export type LndSettings = {
|
export type LndSettings = {
|
||||||
lndLogDir: string
|
lndLogDir: string
|
||||||
feeFixedLimit: number
|
outboundFeeFloor: number
|
||||||
mockLnd: boolean
|
mockLnd: boolean
|
||||||
|
network: BTCNetwork
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveHome = (filepath: string) => {
|
const resolveHome = (filepath: string) => {
|
||||||
|
|
@ -109,10 +111,14 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record<string, string | undefi
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => {
|
export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => {
|
||||||
|
const network = chooseEnv('BTC_NETWORK', dbEnv, 'mainnet', addToDb) as BTCNetwork
|
||||||
|
const limitOld = chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb)
|
||||||
|
const outboundFeeFloor = chooseEnvInt('OUTBOUND_FEE_FLOOR_SATS', dbEnv, limitOld, addToDb)
|
||||||
return {
|
return {
|
||||||
lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb),
|
lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb),
|
||||||
feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb),
|
outboundFeeFloor,
|
||||||
mockLnd: false
|
mockLnd: false,
|
||||||
|
network: networks.includes(network) ? network : 'mainnet'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ export class TransactionSwap {
|
||||||
@Column()
|
@Column()
|
||||||
ephemeral_public_key: string
|
ephemeral_public_key: string
|
||||||
|
|
||||||
|
// the private key is used on to perform a swap, it does not hold any funds once the swap is completed
|
||||||
|
// the swap should only last a few seconds, so it is not a security risk to store the private key in the database
|
||||||
|
// the key is stored here mostly for recovery purposes, in case something goes wrong with the swap
|
||||||
@Column()
|
@Column()
|
||||||
ephemeral_private_key: string
|
ephemeral_private_key: string
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,10 @@ export default class {
|
||||||
return this.dbs.Delete<TransactionSwap>('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId)
|
return this.dbs.Delete<TransactionSwap>('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ListPendingTransactionSwaps(appUserId: string, txId?: string) {
|
||||||
|
return this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId)
|
||||||
|
}
|
||||||
|
|
||||||
async ListCompletedSwaps(appUserId: string, txId?: string) {
|
async ListCompletedSwaps(appUserId: string, txId?: string) {
|
||||||
const completed = await this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: true, app_user_id: appUserId } }, txId)
|
const completed = await this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: true, app_user_id: appUserId } }, txId)
|
||||||
const payments = await this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()) } }, txId)
|
const payments = await this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()) } }, txId)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue