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_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
|
||||
#LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log
|
||||
#BTC_NETWORK=mainnet
|
||||
|
||||
#BOOTSTRAP_PEER
|
||||
# A trusted peer that will hold a node-level account until channel automation becomes affordable
|
||||
|
|
@ -49,8 +50,9 @@
|
|||
#LIGHTNING
|
||||
# Maximum amount in network fees passed to LND when it pays an external invoice
|
||||
# BPS are basis points, 100 BPS = 1%
|
||||
#OUTBOUND_MAX_FEE_BPS=60
|
||||
#OUTBOUND_MAX_FEE_EXTRA_SATS=100
|
||||
#OUTBOUND_MAX_FEE_BPS=60 // deprecated
|
||||
#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
|
||||
# Will execute when it costs less than 1% of balance and uses a trusted peer
|
||||
#BOOTSTRAP=1
|
||||
|
|
|
|||
|
|
@ -1169,7 +1169,7 @@ The nostr server will send back a message response, and inside the body there wi
|
|||
- __invitation_link__: _string_
|
||||
|
||||
### CumulativeFees
|
||||
- __networkFeeFixed__: _number_
|
||||
- __outboundFeeFloor__: _number_
|
||||
- __serviceFeeBps__: _number_
|
||||
|
||||
### DebitAuthorization
|
||||
|
|
@ -1601,6 +1601,7 @@ The nostr server will send back a message response, and inside the body there wi
|
|||
- __swap_operation_id__: _string_
|
||||
|
||||
### SwapsList
|
||||
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_
|
||||
- __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_
|
||||
|
||||
### TransactionSwapQuote
|
||||
|
|
|
|||
|
|
@ -230,8 +230,8 @@ type CreateOneTimeInviteLinkResponse struct {
|
|||
Invitation_link string `json:"invitation_link"`
|
||||
}
|
||||
type CumulativeFees struct {
|
||||
Networkfeefixed int64 `json:"networkFeeFixed"`
|
||||
Servicefeebps int64 `json:"serviceFeeBps"`
|
||||
Outboundfeefloor int64 `json:"outboundFeeFloor"`
|
||||
Servicefeebps int64 `json:"serviceFeeBps"`
|
||||
}
|
||||
type DebitAuthorization struct {
|
||||
Authorized bool `json:"authorized"`
|
||||
|
|
@ -662,7 +662,8 @@ type SwapOperation struct {
|
|||
Swap_operation_id string `json:"swap_operation_id"`
|
||||
}
|
||||
type SwapsList struct {
|
||||
Swaps []SwapOperation `json:"swaps"`
|
||||
Quotes []TransactionSwapQuote `json:"quotes"`
|
||||
Swaps []SwapOperation `json:"swaps"`
|
||||
}
|
||||
type TransactionSwapQuote struct {
|
||||
Chain_fee_sats int64 `json:"chain_fee_sats"`
|
||||
|
|
|
|||
|
|
@ -1303,21 +1303,21 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL
|
|||
}
|
||||
|
||||
export type CumulativeFees = {
|
||||
networkFeeFixed: number
|
||||
outboundFeeFloor: number
|
||||
serviceFeeBps: number
|
||||
}
|
||||
export const CumulativeFeesOptionalFields: [] = []
|
||||
export type CumulativeFeesOptions = OptionsBaseMessage & {
|
||||
checkOptionalsAreSet?: []
|
||||
networkFeeFixed_CustomCheck?: (v: number) => boolean
|
||||
outboundFeeFloor_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.outboundFeeFloor !== 'number') return new Error(`${path}.outboundFeeFloor: is not a number`)
|
||||
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 (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 = {
|
||||
quotes: TransactionSwapQuote[]
|
||||
swaps: SwapOperation[]
|
||||
}
|
||||
export const SwapsListOptionalFields: [] = []
|
||||
export type SwapsListOptions = OptionsBaseMessage & {
|
||||
checkOptionalsAreSet?: []
|
||||
quotes_ItemOptions?: TransactionSwapQuoteOptions
|
||||
quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean
|
||||
swaps_ItemOptions?: SwapOperationOptions
|
||||
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 (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`)
|
||||
for (let index = 0; index < o.swaps.length; index++) {
|
||||
const swapsErr = SwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`)
|
||||
|
|
|
|||
|
|
@ -853,10 +853,11 @@ message SwapOperation {
|
|||
|
||||
message SwapsList {
|
||||
repeated SwapOperation swaps = 1;
|
||||
repeated TransactionSwapQuote quotes = 2;
|
||||
}
|
||||
|
||||
message CumulativeFees {
|
||||
int64 networkFeeFixed = 2;
|
||||
int64 outboundFeeFloor = 2;
|
||||
int64 serviceFeeBps = 3;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import ws from 'ws';
|
|||
import { getLogger, PubLogger, ERROR } from '../helpers/logger.js';
|
||||
import SettingsManager from '../main/settingsManager.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 InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
|
||||
|
|
@ -43,7 +44,6 @@ type TransactionSwapResponse = {
|
|||
}
|
||||
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
|
||||
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
|
||||
const network = Networks.bitcoinMainnet
|
||||
export class Swaps {
|
||||
reverseSwaps: ReverseSwaps
|
||||
submarineSwaps: SubmarineSwaps
|
||||
|
|
@ -52,6 +52,8 @@ export class Swaps {
|
|||
this.submarineSwaps = new SubmarineSwaps(settings)
|
||||
}
|
||||
|
||||
Stop = () => { }
|
||||
|
||||
GetKeys = (privateKey: string) => {
|
||||
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
|
||||
return keys
|
||||
|
|
@ -230,17 +232,11 @@ export class ReverseSwaps {
|
|||
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/reverse`
|
||||
const req: any = {
|
||||
onchainAmount: txAmount,
|
||||
// invoiceAmount,
|
||||
to: 'BTC',
|
||||
from: 'BTC',
|
||||
claimPublicKey: Buffer.from(keys.publicKey).toString('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)
|
||||
if (!createdResponseRes.ok) {
|
||||
return createdResponseRes
|
||||
|
|
@ -262,11 +258,21 @@ export class ReverseSwaps {
|
|||
webSocket.on('open', () => {
|
||||
webSocket.send(JSON.stringify(subReq))
|
||||
})
|
||||
let txId = ""
|
||||
let txId = "", isDone = false
|
||||
const done = () => {
|
||||
isDone = true
|
||||
webSocket.close()
|
||||
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) => {
|
||||
try {
|
||||
const result = await this.handleSwapTransactionMessage(rawMsg, data, done)
|
||||
|
|
@ -275,6 +281,7 @@ export class ReverseSwaps {
|
|||
}
|
||||
} catch (err: any) {
|
||||
this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
|
||||
isDone = true
|
||||
webSocket.close()
|
||||
swapDone({ ok: false, error: err.message })
|
||||
return
|
||||
|
|
@ -336,7 +343,7 @@ export class ReverseSwaps {
|
|||
this.log(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
|
||||
const claimTx = constructClaimTransaction(
|
||||
[
|
||||
|
|
@ -436,4 +443,17 @@ const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, da
|
|||
log(ERROR, 'Error getting request', 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
|
||||
}
|
||||
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 {
|
||||
userId: ctx.user_id,
|
||||
balance: user.balance_sats,
|
||||
max_withdrawable: max,
|
||||
user_identifier: appUser.identifier,
|
||||
network_max_fee_bps: 0,
|
||||
network_max_fee_fixed: networkFeeFixed,
|
||||
network_max_fee_fixed: outboundFeeFloor,
|
||||
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] }),
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ 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)
|
||||
const { max, outboundFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats)
|
||||
return {
|
||||
identifier: u.identifier,
|
||||
info: {
|
||||
|
|
@ -176,7 +176,7 @@ export default class {
|
|||
max_withdrawable: max,
|
||||
user_identifier: u.identifier,
|
||||
network_max_fee_bps: 0,
|
||||
network_max_fee_fixed: networkFeeFixed,
|
||||
network_max_fee_fixed: outboundFeeFloor,
|
||||
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] }),
|
||||
|
|
@ -225,14 +225,14 @@ export default class {
|
|||
const app = await this.storage.applicationStorage.GetApplication(appId)
|
||||
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
|
||||
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 {
|
||||
max_withdrawable: max, identifier: req.user_identifier, info: {
|
||||
userId: user.user.user_id, balance: user.user.balance_sats,
|
||||
max_withdrawable: max,
|
||||
user_identifier: user.identifier,
|
||||
network_max_fee_bps: 0,
|
||||
network_max_fee_fixed: networkFeeFixed,
|
||||
network_max_fee_fixed: outboundFeeFloor,
|
||||
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] }),
|
||||
|
|
|
|||
|
|
@ -170,7 +170,8 @@ export default class {
|
|||
NewBlockHandler = async (height: number) => {
|
||||
let confirmed: (PendingTx & { confs: number; })[]
|
||||
let log = getLogger({})
|
||||
|
||||
this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height)
|
||||
.catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err))
|
||||
try {
|
||||
const balanceEvents = await this.paymentManager.GetLndBalance()
|
||||
await this.metricsManager.NewBlockCb(height, balanceEvents)
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export class LiquidityProvider {
|
|||
return res
|
||||
}
|
||||
this.feesCache = {
|
||||
networkFeeFixed: res.network_max_fee_fixed,
|
||||
outboundFeeFloor: res.network_max_fee_fixed,
|
||||
serviceFeeBps: res.service_fee_bps
|
||||
}
|
||||
this.latestReceivedBalance = res.balance
|
||||
|
|
@ -152,11 +152,11 @@ export class LiquidityProvider {
|
|||
return 0
|
||||
}
|
||||
const balance = this.latestReceivedBalance
|
||||
const { networkFeeFixed, serviceFeeBps } = this.feesCache
|
||||
const { outboundFeeFloor, serviceFeeBps } = this.feesCache
|
||||
const div = 1 + (serviceFeeBps / 10000)
|
||||
const maxWithoutFixed = Math.floor(balance / div)
|
||||
const fee = balance - maxWithoutFixed
|
||||
return balance - Math.max(fee, networkFeeFixed)
|
||||
return balance - Math.max(fee, outboundFeeFloor)
|
||||
}
|
||||
|
||||
GetLatestBalance = () => {
|
||||
|
|
@ -174,7 +174,7 @@ export class LiquidityProvider {
|
|||
const fees = f ? f : this.GetFees()
|
||||
const serviceFeeRate = fees.serviceFeeBps / 10000
|
||||
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> => {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export default class {
|
|||
liquidityManager: LiquidityManager
|
||||
utils: Utils
|
||||
swaps: Swaps
|
||||
invoiceLock: InvoiceLock
|
||||
constructor(storage: Storage, lnd: LND, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
|
||||
this.storage = storage
|
||||
this.settings = settings
|
||||
|
|
@ -65,9 +66,13 @@ export default class {
|
|||
this.swaps = new Swaps(settings)
|
||||
this.addressPaidCb = addressPaidCb
|
||||
this.invoicePaidCb = invoicePaidCb
|
||||
this.invoiceLock = new InvoiceLock()
|
||||
}
|
||||
|
||||
|
||||
Stop() {
|
||||
this.watchDog.Stop()
|
||||
this.swaps.Stop()
|
||||
}
|
||||
|
||||
checkPendingPayments = async () => {
|
||||
|
|
@ -192,7 +197,7 @@ export default class {
|
|||
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)
|
||||
return Math.max(fee, this.settings.getSettings().lndSettings.outboundFeeFloor)
|
||||
case Types.UserOperationType.OUTGOING_USER_TO_USER:
|
||||
if (appUser) {
|
||||
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
|
||||
|
|
@ -251,17 +256,17 @@ export default class {
|
|||
|
||||
GetFees = (): Types.CumulativeFees => {
|
||||
const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings
|
||||
const { feeFixedLimit } = this.settings.getSettings().lndSettings
|
||||
return { networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
|
||||
const { outboundFeeFloor } = this.settings.getSettings().lndSettings
|
||||
return { outboundFeeFloor, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
|
||||
}
|
||||
|
||||
GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
|
||||
const { networkFeeFixed, serviceFeeBps } = this.GetFees()
|
||||
const { outboundFeeFloor, 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 }
|
||||
const max = balance - Math.max(fee, outboundFeeFloor)
|
||||
return { max, outboundFeeFloor, serviceFeeBps }
|
||||
}
|
||||
async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise<Types.DecodeInvoiceResponse> {
|
||||
const decoded = await this.lnd.DecodeInvoice(req.invoice)
|
||||
|
|
@ -277,10 +282,10 @@ export default class {
|
|||
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 { outboundFeeFloor, serviceFeeBps } = req.expected_fees
|
||||
const serviceFixed = this.settings.getSettings().lndSettings.outboundFeeFloor
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -303,10 +308,20 @@ export default class {
|
|||
throw new Error("this invoice was already paid")
|
||||
}
|
||||
let paymentInfo = { preimage: "", amtPaid: 0, networkFee: 0, serialId: 0 }
|
||||
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 })
|
||||
if (this.invoiceLock.isLocked(req.invoice)) {
|
||||
throw new Error("this invoice is already being paid")
|
||||
}
|
||||
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
|
||||
if (isAppUserPayment && feeDiff > 0) {
|
||||
|
|
@ -456,7 +471,7 @@ export default class {
|
|||
async PayAddressWithSwap(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
|
||||
this.log("paying external address")
|
||||
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 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> {
|
||||
const swaps = await this.storage.paymentStorage.ListCompletedSwaps(ctx.app_user_id)
|
||||
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(ctx.app_user_id)
|
||||
return {
|
||||
swaps: swaps.map(s => {
|
||||
const p = s.payment
|
||||
|
|
@ -558,6 +574,17 @@ export default class {
|
|||
address_paid: s.swap.address_paid,
|
||||
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
|
||||
lndMacaroonPath: string // cold setting
|
||||
}
|
||||
const networks = ['mainnet', 'testnet', 'regtest'] as const
|
||||
export type BTCNetwork = (typeof networks)[number]
|
||||
export type LndSettings = {
|
||||
lndLogDir: string
|
||||
feeFixedLimit: number
|
||||
outboundFeeFloor: number
|
||||
mockLnd: boolean
|
||||
|
||||
network: BTCNetwork
|
||||
}
|
||||
|
||||
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 => {
|
||||
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 {
|
||||
lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb),
|
||||
feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb),
|
||||
mockLnd: false
|
||||
outboundFeeFloor,
|
||||
mockLnd: false,
|
||||
network: networks.includes(network) ? network : 'mainnet'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ export class TransactionSwap {
|
|||
@Column()
|
||||
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()
|
||||
ephemeral_private_key: string
|
||||
|
||||
|
|
|
|||
|
|
@ -494,6 +494,10 @@ export default class {
|
|||
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) {
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue