From ffc156df2bbc21eb8fcdc77bce4db17de8ee1984 Mon Sep 17 00:00:00 2001 From: Keyan <34140557+huumn@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:33:17 -0500 Subject: [PATCH] Msats to sats floor (#1307) * make wallet invoice creation tests make full sat invoice * handle rounded/floored msats for receiving wallets * msats flooring to sats function --- api/paidAction/index.js | 2 +- lib/format.js | 3 +++ wallets/cln/server.js | 2 +- wallets/lightning-address/server.js | 5 +++++ wallets/lnbits/server.js | 2 +- wallets/lnd/server.js | 2 +- wallets/server.js | 16 +++++++++++++++- wallets/wrap.js | 22 ++++++++++++++++------ worker/autowithdraw.js | 17 ++++++++--------- 9 files changed, 51 insertions(+), 20 deletions(-) diff --git a/api/paidAction/index.js b/api/paidAction/index.js index 563ef6c0..dea06403 100644 --- a/api/paidAction/index.js +++ b/api/paidAction/index.js @@ -231,7 +231,7 @@ export async function createLightningInvoice (actionType, args, context) { }, { models }) const { invoice: wrappedInvoice, maxFee } = await wrapInvoice( - bolt11, { description }, { lnd }) + bolt11, { msats: cost, description }, { lnd }) return { bolt11, diff --git a/lib/format.js b/lib/format.js index 37ba9fc0..2ac67e26 100644 --- a/lib/format.js +++ b/lib/format.js @@ -52,6 +52,7 @@ export const msatsToSats = msats => { if (msats === null || msats === undefined) { return null } + // implicitly floors the result return Number(BigInt(msats) / 1000n) } @@ -62,6 +63,8 @@ export const satsToMsats = sats => { return BigInt(sats) * 1000n } +export const msatsSatsFloor = msats => satsToMsats(msatsToSats(msats)) + export const msatsToSatsDecimal = msats => { if (msats === null || msats === undefined) { return null diff --git a/wallets/cln/server.js b/wallets/cln/server.js index 9f25b111..e8779ef9 100644 --- a/wallets/cln/server.js +++ b/wallets/cln/server.js @@ -3,7 +3,7 @@ import { createInvoice as clnCreateInvoice } from '@/lib/cln' export * from 'wallets/cln' export const testConnectServer = async ({ socket, rune, cert }) => { - return await createInvoice({ msats: 1, expiry: 1, description: '' }, { socket, rune, cert }) + return await createInvoice({ msats: 1000, expiry: 1, description: '' }, { socket, rune, cert }) } export const createInvoice = async ( diff --git a/wallets/lightning-address/server.js b/wallets/lightning-address/server.js index b5d7b519..cc5ccb37 100644 --- a/wallets/lightning-address/server.js +++ b/wallets/lightning-address/server.js @@ -1,3 +1,4 @@ +import { msatsSatsFloor } from '@/lib/format' import { lnAddrOptions } from '@/lib/lnurl' export * from 'wallets/lightning-address' @@ -12,6 +13,10 @@ export const createInvoice = async ( ) => { const { callback, commentAllowed } = await lnAddrOptions(address) const callbackUrl = new URL(callback) + + // most lnurl providers suck nards so we have to floor to nearest sat + msats = msatsSatsFloor(msats) + callbackUrl.searchParams.append('amount', msats) if (commentAllowed >= description?.length) { diff --git a/wallets/lnbits/server.js b/wallets/lnbits/server.js index a9a403b9..f0ff5a7d 100644 --- a/wallets/lnbits/server.js +++ b/wallets/lnbits/server.js @@ -1,7 +1,7 @@ export * from 'wallets/lnbits' export async function testConnectServer ({ url, invoiceKey }) { - return await createInvoice({ msats: 1, expiry: 1 }, { url, invoiceKey }) + return await createInvoice({ msats: 1000, expiry: 1 }, { url, invoiceKey }) } export async function createInvoice ( diff --git a/wallets/lnd/server.js b/wallets/lnd/server.js index 00be1a93..7f830029 100644 --- a/wallets/lnd/server.js +++ b/wallets/lnd/server.js @@ -4,7 +4,7 @@ import { authenticatedLndGrpc, createInvoice as lndCreateInvoice } from 'ln-serv export * from 'wallets/lnd' export const testConnectServer = async ({ cert, macaroon, socket }) => { - return await createInvoice({ msats: 1, expiry: 1 }, { cert, macaroon, socket }) + return await createInvoice({ msats: 1000, expiry: 1 }, { cert, macaroon, socket }) } export const createInvoice = async ( diff --git a/wallets/server.js b/wallets/server.js index b0a208c0..946631d3 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -53,7 +53,21 @@ export async function createInvoice (userId, { msats, description, descriptionHa const bolt11 = await parsePaymentRequest({ request: invoice }) if (BigInt(bolt11.mtokens) !== BigInt(msats)) { - throw new Error('invoice has incorrect amount') + if (BigInt(bolt11.mtokens) > BigInt(msats)) { + throw new Error(`invoice is for an amount greater than requested ${bolt11.mtokens} > ${msats}`) + } + if (BigInt(bolt11.mtokens) === 0n) { + throw new Error('invoice is for 0 msats') + } + if (BigInt(msats) - BigInt(bolt11.mtokens) >= 1000n) { + throw new Error(`invoice has a different satoshi amount ${bolt11.mtokens} !== ${msats}`) + } + + await addWalletLog({ + wallet, + level: 'INFO', + message: `wallet does not support msats so we floored ${msats} msats to nearest sat ${BigInt(bolt11.mtokens)} msats` + }, { models }) } return { invoice, wallet } diff --git a/wallets/wrap.js b/wallets/wrap.js index 1149a1a5..f3f0574d 100644 --- a/wallets/wrap.js +++ b/wallets/wrap.js @@ -19,10 +19,10 @@ const ZAP_SYBIL_FEE_MULT = 10 / 9 // the fee for the zap sybil service @param options {object} @returns { invoice: the wrapped incoming invoice, - outgoingMaxFeeMsat: number + maxFee: number } */ -export default async function wrapInvoice (bolt11, { description, descriptionHash }, { lnd }) { +export default async function wrapInvoice (bolt11, { msats, description, descriptionHash }, { lnd }) { try { console.group('wrapInvoice', description) @@ -38,7 +38,7 @@ export default async function wrapInvoice (bolt11, { description, descriptionHas console.log('invoice', inv.mtokens, inv.expires_at, inv.cltv_delta) - // validate amount + // validate outgoing amount if (inv.mtokens) { outgoingMsat = toPositiveNumber(inv.mtokens) if (outgoingMsat < MIN_OUTGOING_MSATS) { @@ -48,7 +48,17 @@ export default async function wrapInvoice (bolt11, { description, descriptionHas throw new Error(`Invoice amount is too high: ${outgoingMsat}`) } } else { - throw new Error('Invoice amount is missing') + throw new Error('Outgoing invoice is missing amount') + } + + // validate incoming amount + if (msats) { + msats = toPositiveNumber(msats) + if (outgoingMsat * ZAP_SYBIL_FEE_MULT > msats) { + throw new Error('Sybil fee is too low') + } + } else { + throw new Error('Incoming invoice amount is missing') } // validate features @@ -145,13 +155,13 @@ export default async function wrapInvoice (bolt11, { description, descriptionHas // validate the fee budget const minEstFees = toPositiveNumber(routingFeeMsat) - const outgoingMaxFeeMsat = Math.ceil(outgoingMsat * MAX_FEE_ESTIMATE_PERCENT) + const outgoingMaxFeeMsat = Math.ceil(msats * MAX_FEE_ESTIMATE_PERCENT) if (minEstFees > outgoingMaxFeeMsat) { throw new Error('Estimated fees are too high') } // calculate the incoming invoice amount, without fees - wrapped.mtokens = String(Math.ceil(outgoingMsat * ZAP_SYBIL_FEE_MULT)) + wrapped.mtokens = String(msats) console.log('outgoingMaxFeeMsat', outgoingMaxFeeMsat, 'wrapped', wrapped) return { diff --git a/worker/autowithdraw.js b/worker/autowithdraw.js index 2f79540b..0805619e 100644 --- a/worker/autowithdraw.js +++ b/worker/autowithdraw.js @@ -1,4 +1,4 @@ -import { msatsToSats, satsToMsats } from '@/lib/format' +import { msatsSatsFloor, msatsToSats, satsToMsats } from '@/lib/format' import { createWithdrawal } from '@/api/resolvers/wallet' import { createInvoice } from 'wallets/server' @@ -12,14 +12,13 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) { // excess must be greater than 10% of threshold if (excess < Number(threshold) * 0.1) return - const maxFeeMsats = Math.ceil(excess * (user.autoWithdrawMaxFeePercent / 100.0)) - const msats = excess - maxFeeMsats + // floor fee to nearest sat but still denominated in msats + const maxFeeMsats = msatsSatsFloor(Math.ceil(excess * (user.autoWithdrawMaxFeePercent / 100.0))) + // msats will be floored by createInvoice if it needs to be + const msats = BigInt(excess) - maxFeeMsats // must be >= 1 sat - if (msats < 1000) return - - // maxFee is expected to be in sats, ie "msatsFeePaying" is always divisible by 1000 - const maxFee = msatsToSats(maxFeeMsats) + if (msats < 1000n) return // check that // 1. the user doesn't have an autowithdraw pending @@ -33,7 +32,7 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) { OR ( status <> 'CONFIRMED' AND now() < created_at + interval '1 hour' AND - "msatsFeePaying" >= ${satsToMsats(maxFee)} + "msatsFeePaying" >= ${maxFeeMsats} )) )` @@ -41,6 +40,6 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) { const { invoice, wallet } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models }) return await createWithdrawal(null, - { invoice, maxFee }, + { invoice, maxFee: msatsToSats(maxFeeMsats) }, { me: { id }, models, lnd, walletId: wallet.id }) }