diff --git a/env.example b/env.example index a853c5e8..bc3c040b 100644 --- a/env.example +++ b/env.example @@ -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 diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index b402cfec..abd101e9 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -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 diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 9dc27c9e..a4ac016b 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -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"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index a306e5ac..7e72d13d 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -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}]`) diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 40f83e7a..f2aa653b 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -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; } diff --git a/src/services/lnd/swaps.ts b/src/services/lnd/swaps.ts index 33627d2c..4dec5cdf 100644 --- a/src/services/lnd/swaps.ts +++ b/src/services/lnd/swaps.ts @@ -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(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 (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}`) + } } \ No newline at end of file diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index bf75246c..b8b1dc1d 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -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] }), diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 3cff29a5..a2ad96d2 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -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] }), diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 9afa2ba5..4d061721 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -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) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 963610a6..f625558f 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -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 => { diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 238f11bc..e24efd48 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -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 { 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 { 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 { 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 = {} + lock(invoice: string) { + this.locked[invoice] = true + } + unlock(invoice: string) { + delete this.locked[invoice] + } + isLocked(invoice: string) { + return this.locked[invoice] + } +} \ No newline at end of file diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index d186f1c3..c49225a7 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -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, 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' } } diff --git a/src/services/storage/entity/TransactionSwap.ts b/src/services/storage/entity/TransactionSwap.ts index 5e3c0e51..e3351d08 100644 --- a/src/services/storage/entity/TransactionSwap.ts +++ b/src/services/storage/entity/TransactionSwap.ts @@ -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 diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index aa7d5b1d..47205463 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -494,6 +494,10 @@ export default class { return this.dbs.Delete('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId) } + async ListPendingTransactionSwaps(appUserId: string, txId?: string) { + return this.dbs.Find('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId) + } + async ListCompletedSwaps(appUserId: string, txId?: string) { const completed = await this.dbs.Find('TransactionSwap', { where: { used: true, app_user_id: appUserId } }, txId) const payments = await this.dbs.Find('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()) } }, txId)