Compare commits

...

3 Commits

Author SHA1 Message Date
Keyan ffc156df2b
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
2024-08-16 19:33:17 -05:00
ekzyis ab80873a57
Fix all wallets deleted on logout (#1308)
Every wallet returned by the useWallet hook has sendPayment set even if it doesn't support payments since wallet.sendPayment is wrapped with a useCallback hook.
2024-08-16 15:50:11 -05:00
ekzyis 3cd9991aa9
Hide wallet name in logs for only a single wallet (#1305) 2024-08-16 09:02:09 -05:00
12 changed files with 62 additions and 24 deletions

View File

@ -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,

View File

@ -1,13 +1,13 @@
import { timeSince } from '@/lib/time'
import styles from './log-message.module.css'
export default function LogMessage ({ wallet, level, message, ts }) {
export default function LogMessage ({ showWallet, wallet, level, message, ts }) {
level = level.toLowerCase()
const levelClassName = ['ok', 'success'].includes(level) ? 'text-success' : level === 'error' ? 'text-danger' : level === 'info' ? 'text-info' : ''
return (
<tr className={styles.line}>
<td className={styles.timestamp}>{timeSince(new Date(ts))}</td>
<td className={styles.wallet}>[{wallet}]</td>
{showWallet ? <td className={styles.wallet}>[{wallet}]</td> : <td className='mx-1' />}
<td className={`${styles.level} ${levelClassName}`}>{level === 'success' ? 'ok' : level}</td>
<td>{message}</td>
</tr>

View File

@ -30,7 +30,13 @@ export function WalletLogs ({ wallet, embedded }) {
{logs.length === 0 && <div className='w-100 text-center'>empty</div>}
<table>
<tbody>
{logs.map((log, i) => <LogMessage key={i} {...log} />)}
{logs.map((log, i) => (
<LogMessage
key={i}
showWallet={!wallet}
{...log}
/>
))}
</tbody>
</table>
<div className='w-100 text-center'>------ start of logs ------</div>

View File

@ -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

View File

@ -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 (

View File

@ -105,6 +105,7 @@ export function useWallet (name) {
return {
...wallet,
canSend: !!wallet.sendPayment,
sendPayment,
config,
save,
@ -375,7 +376,7 @@ export function useWallets () {
const resetClient = useCallback(async (wallet) => {
for (const w of wallets) {
if (w.sendPayment) {
if (w.canSend) {
await w.delete()
}
await w.deleteLogs()

View File

@ -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) {

View File

@ -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 (

View File

@ -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 (

View File

@ -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 }

View File

@ -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 {

View File

@ -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 })
}