Compare commits
4 Commits
4675a2c29d
...
7e5a8310df
Author | SHA1 | Date |
---|---|---|
Riccardo Balbo | 7e5a8310df | |
Riccardo Balbo | 18700b4201 | |
ekzyis | fdd34b2eb3 | |
ekzyis | 406ae81693 |
|
@ -167,10 +167,11 @@ All functions have the following signature: `function(args: Object, context: Obj
|
|||
- this function is called when an optimistic action is retried
|
||||
- it's passed the original `invoiceId` and the `newInvoiceId`
|
||||
- this function should update the rows created in `perform` to contain the new `newInvoiceId` and remark the row as `PENDING`
|
||||
- `invoiceablePeer`: returns the userId of the peer that's capable of generating an invoice so they can be paid for the action
|
||||
- `getInvoiceablePeer`: returns the userId of the peer that's capable of generating an invoice so they can be paid for the action
|
||||
- this is only used for p2p wrapped zaps currently
|
||||
- `describe`: returns a description as a string of the action
|
||||
- for actions that require generating an invoice, and for stackers that don't hide invoice descriptions, this is used in the invoice description
|
||||
- `getSybilFeePercent` (required if `getInvoiceablePeer` is implemented): returns the action sybil fee percent as a `BigInt` (eg. 30n for 30%)
|
||||
|
||||
#### Function arguments
|
||||
|
||||
|
@ -179,6 +180,7 @@ All functions have the following signature: `function(args: Object, context: Obj
|
|||
`context` contains the following fields:
|
||||
- `me`: the user performing the action (undefined if anonymous)
|
||||
- `cost`: the cost of the action in msats as a `BigInt`
|
||||
- `sybilFeePercent`: the sybil fee percent as a `BigInt` (eg. 30n for 30%)
|
||||
- `tx`: the current transaction (for anything that needs to be done atomically with the payment)
|
||||
- `models`: the current prisma client (for anything that doesn't need to be done atomically with the payment)
|
||||
- `lnd`: the current lnd client
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
|
||||
import { createHmac, walletLogger } from '@/api/resolvers/wallet'
|
||||
import { createHmac } from '@/api/resolvers/wallet'
|
||||
import { Prisma } from '@prisma/client'
|
||||
import * as ITEM_CREATE from './itemCreate'
|
||||
import * as ITEM_UPDATE from './itemUpdate'
|
||||
|
@ -14,8 +14,7 @@ import * as TERRITORY_BILLING from './territoryBilling'
|
|||
import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
|
||||
import * as DONATE from './donate'
|
||||
import * as BOOST from './boost'
|
||||
import wrapInvoice from 'wallets/wrap'
|
||||
import { createInvoice as createUserInvoice } from 'wallets/server'
|
||||
import { createWrappedInvoice } from 'wallets/server'
|
||||
|
||||
export const paidActions = {
|
||||
ITEM_CREATE,
|
||||
|
@ -44,6 +43,7 @@ export default async function performPaidAction (actionType, args, context) {
|
|||
|
||||
context.me = me ? await models.user.findUnique({ where: { id: me.id } }) : undefined
|
||||
context.cost = await paidAction.getCost(args, context)
|
||||
context.sybilFeePercent = await paidAction.getSybilFeePercent?.(args, context)
|
||||
|
||||
if (!me) {
|
||||
if (!paidAction.anonable) {
|
||||
|
@ -229,8 +229,7 @@ const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
|
|||
export async function createLightningInvoice (actionType, args, context) {
|
||||
// if the action has an invoiceable peer, we'll create a peer invoice
|
||||
// wrap it, and return the wrapped invoice
|
||||
const { cost, models, lnd, me } = context
|
||||
const userId = await paidActions[actionType]?.invoiceablePeer?.(args, context)
|
||||
const { cost, models, lnd, sybilFeePercent, me } = context
|
||||
|
||||
// count pending invoices and bail if we're over the limit
|
||||
const pendingInvoices = await models.invoice.count({
|
||||
|
@ -248,33 +247,29 @@ export async function createLightningInvoice (actionType, args, context) {
|
|||
throw new Error('You have too many pending paid actions, cancel some or wait for them to expire')
|
||||
}
|
||||
|
||||
const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, context)
|
||||
if (userId) {
|
||||
let logger, bolt11
|
||||
try {
|
||||
if (!sybilFeePercent) {
|
||||
throw new Error('sybil fee percent is not set for an invoiceable peer action')
|
||||
}
|
||||
|
||||
const description = await paidActions[actionType].describe(args, context)
|
||||
const { invoice, wallet } = await createUserInvoice(userId, {
|
||||
// this is the amount the stacker will receive, the other 3/10ths is the sybil fee
|
||||
msats: cost * BigInt(7) / BigInt(10),
|
||||
|
||||
const { invoice, wrappedInvoice, wallet, maxFee } = await createWrappedInvoice(userId, {
|
||||
msats: cost,
|
||||
feePercent: sybilFeePercent,
|
||||
description,
|
||||
expiry: INVOICE_EXPIRE_SECS
|
||||
}, { models })
|
||||
|
||||
logger = walletLogger({ wallet, models })
|
||||
bolt11 = invoice
|
||||
|
||||
// the sender (me) decides if the wrapped invoice has a description
|
||||
// whereas the recipient decides if their invoice has a description
|
||||
const { invoice: wrappedInvoice, maxFee } = await wrapInvoice(
|
||||
bolt11, { msats: cost, description }, { me, lnd })
|
||||
}, { models, me, lnd })
|
||||
|
||||
return {
|
||||
bolt11,
|
||||
wrappedBolt11: wrappedInvoice.request,
|
||||
bolt11: invoice,
|
||||
wrappedBolt11: wrappedInvoice,
|
||||
wallet,
|
||||
maxFee
|
||||
}
|
||||
} catch (e) {
|
||||
logger?.error('invalid invoice: ' + e.message, { bolt11 })
|
||||
console.error('failed to create stacker invoice, falling back to SN invoice', e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export async function getCost ({ sats }) {
|
|||
return satsToMsats(sats)
|
||||
}
|
||||
|
||||
export async function invoiceablePeer ({ id }, { models }) {
|
||||
export async function getInvoiceablePeer ({ id }, { models }) {
|
||||
const item = await models.item.findUnique({
|
||||
where: { id: parseInt(id) },
|
||||
include: {
|
||||
|
@ -27,8 +27,12 @@ export async function invoiceablePeer ({ id }, { models }) {
|
|||
return item.user.wallets.length > 0 && item.itemForwards.length === 0 ? item.userId : null
|
||||
}
|
||||
|
||||
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, tx }) {
|
||||
const feeMsats = 3n * (cost / BigInt(10)) // 30% fee
|
||||
export async function getSybilFeePercent () {
|
||||
return 30n
|
||||
}
|
||||
|
||||
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, sybilFeePercent, tx }) {
|
||||
const feeMsats = cost * sybilFeePercent / 100n
|
||||
const zapMsats = cost - feeMsats
|
||||
itemId = parseInt(itemId)
|
||||
|
||||
|
|
|
@ -1375,7 +1375,7 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
|
|||
|
||||
// prevent update if it's not explicitly allowed, not their bio, not their job and older than 10 minutes
|
||||
const myBio = user.bioId === old.id
|
||||
const timer = Date.now() < new Date(old.invoicePaidAt ?? old.createdAt).getTime() + 10 * 60_000
|
||||
const timer = Date.now() < datePivot(new Date(old.invoicePaidAt ?? old.createdAt), { minutes: 10 })
|
||||
|
||||
// timer permission check
|
||||
if (!adminEdit && !myBio && !timer && !isJob(item)) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import Dropdown from 'react-bootstrap/Dropdown'
|
|||
import Countdown from './countdown'
|
||||
import { abbrNum, numWithUnits } from '@/lib/format'
|
||||
import { newComments, commentsViewedAt } from '@/lib/new-comments'
|
||||
import { timeSince } from '@/lib/time'
|
||||
import { datePivot, timeSince } from '@/lib/time'
|
||||
import { DeleteDropdownItem } from './delete'
|
||||
import styles from './item.module.css'
|
||||
import { useMe } from './me'
|
||||
|
@ -34,10 +34,9 @@ export default function ItemInfo ({
|
|||
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true,
|
||||
setDisableRetry, disableRetry
|
||||
}) {
|
||||
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
|
||||
const editThreshold = datePivot(new Date(item.invoice?.confirmedAt ?? item.createdAt), { minutes: 10 })
|
||||
const { me } = useMe()
|
||||
const router = useRouter()
|
||||
const [canEdit, setCanEdit] = useState(item.mine && !item.bio && (Date.now() < editThreshold))
|
||||
const [hasNewComments, setHasNewComments] = useState(false)
|
||||
const root = useRoot()
|
||||
const sub = item?.sub || root?.sub
|
||||
|
@ -48,12 +47,18 @@ export default function ItemInfo ({
|
|||
}
|
||||
}, [item])
|
||||
|
||||
// allow anon edits if they have the correct hmac for the item invoice
|
||||
// (the server will verify the hmac)
|
||||
const [anonEdit, setAnonEdit] = useState(false)
|
||||
useEffect(() => {
|
||||
const authorEdit = item.mine && !item.bio
|
||||
const invParams = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
|
||||
const hmacEdit = !!invParams && !me && Number(item.user.id) === USER_ID.anon
|
||||
setCanEdit((authorEdit || hmacEdit) && (Date.now() < editThreshold))
|
||||
}, [me, item.id, item.mine, editThreshold])
|
||||
setAnonEdit(!!invParams && !me && Number(item.user.id) === USER_ID.anon)
|
||||
}, [])
|
||||
|
||||
// deleted items can never be edited and every item has a 10 minute edit window
|
||||
// except bios, they can always be edited but they should never show the countdown
|
||||
const noEdit = !!item.deletedAt || (Date.now() >= editThreshold) || item.bio
|
||||
const canEdit = !noEdit && ((me && item.mine) || anonEdit)
|
||||
|
||||
// territory founders can pin any post in their territory
|
||||
// and OPs can pin any root reply in their post
|
||||
|
@ -152,7 +157,7 @@ export default function ItemInfo ({
|
|||
<>
|
||||
<EditInfo
|
||||
item={item} edit={edit} canEdit={canEdit}
|
||||
setCanEdit={setCanEdit} toggleEdit={toggleEdit} editText={editText} editThreshold={editThreshold}
|
||||
setCanEdit={setAnonEdit} toggleEdit={toggleEdit} editText={editText} editThreshold={editThreshold}
|
||||
/>
|
||||
<PaymentInfo item={item} disableRetry={disableRetry} setDisableRetry={setDisableRetry} />
|
||||
<ActionDropdown>
|
||||
|
|
|
@ -79,6 +79,7 @@ export default function Login ({ providers, callbackUrl, multiAuth, error, text,
|
|||
case 'Email':
|
||||
return (
|
||||
<OverlayTrigger
|
||||
key={provider.id}
|
||||
placement='bottom'
|
||||
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
|
||||
trigger={['hover', 'focus']}
|
||||
|
|
|
@ -265,6 +265,7 @@ function LogoutObstacle ({ onClose }) {
|
|||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||
const { removeLocalWallets } = useWallets()
|
||||
const { multiAuthSignout } = useAccounts()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className='d-flex m-auto flex-column w-fit-content'>
|
||||
|
@ -283,6 +284,8 @@ function LogoutObstacle ({ onClose }) {
|
|||
// only signout if multiAuth did not find a next available account
|
||||
if (switchSuccess) {
|
||||
onClose()
|
||||
// reload whatever page we're on to avoid any bugs
|
||||
router.reload()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
|
@ -494,26 +494,6 @@ export const lud18PayerDataSchema = (k1) => object({
|
|||
identifier: string()
|
||||
})
|
||||
|
||||
// check if something is _really_ a number.
|
||||
// returns true for every number in this range: [-Infinity, ..., 0, ..., Infinity]
|
||||
export const isNumber = x => typeof x === 'number' && !Number.isNaN(x)
|
||||
|
||||
export const toNumber = (x, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) => {
|
||||
if (typeof x === 'undefined') {
|
||||
throw new Error('value is required')
|
||||
}
|
||||
const n = Number(x)
|
||||
if (isNumber(n)) {
|
||||
if (x < min || x > max) {
|
||||
throw new Error(`value ${x} must be between ${min} and ${max}`)
|
||||
}
|
||||
return n
|
||||
}
|
||||
throw new Error(`value ${x} is not a number`)
|
||||
}
|
||||
|
||||
export const toPositiveNumber = (x) => toNumber(x, 0)
|
||||
|
||||
export const deviceSyncSchema = object().shape({
|
||||
passphrase: string().required('required')
|
||||
.test(async (value, context) => {
|
||||
|
@ -533,3 +513,79 @@ export const deviceSyncSchema = object().shape({
|
|||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// check if something is _really_ a number.
|
||||
// returns true for every number in this range: [-Infinity, ..., 0, ..., Infinity]
|
||||
export const isNumber = x => typeof x === 'number' && !Number.isNaN(x)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any | bigint} x
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
* @returns {number}
|
||||
*/
|
||||
export const toNumber = (x, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) => {
|
||||
if (typeof x === 'undefined') {
|
||||
throw new Error('value is required')
|
||||
}
|
||||
if (typeof x === 'bigint') {
|
||||
if (x < BigInt(min) || x > BigInt(max)) {
|
||||
throw new Error(`value ${x} must be between ${min} and ${max}`)
|
||||
}
|
||||
return Number(x)
|
||||
} else {
|
||||
const n = Number(x)
|
||||
if (isNumber(n)) {
|
||||
if (x < min || x > max) {
|
||||
throw new Error(`value ${x} must be between ${min} and ${max}`)
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
throw new Error(`value ${x} is not a number`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any | bigint} x
|
||||
* @returns {number}
|
||||
*/
|
||||
export const toPositiveNumber = (x) => toNumber(x, 0)
|
||||
|
||||
/**
|
||||
* @param {any} x
|
||||
* @param {bigint | number} [min]
|
||||
* @param {bigint | number} [max]
|
||||
* @returns {bigint}
|
||||
*/
|
||||
export const toBigInt = (x, min, max) => {
|
||||
if (typeof x === 'undefined') throw new Error('value is required')
|
||||
|
||||
const n = BigInt(x)
|
||||
if (min !== undefined && n < BigInt(min)) {
|
||||
throw new Error(`value ${x} must be at least ${min}`)
|
||||
}
|
||||
|
||||
if (max !== undefined && n > BigInt(max)) {
|
||||
throw new Error(`value ${x} must be at most ${max}`)
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number|bigint} x
|
||||
* @returns {bigint}
|
||||
*/
|
||||
export const toPositiveBigInt = (x) => {
|
||||
return toBigInt(x, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number|bigint} x
|
||||
* @returns {number|bigint}
|
||||
*/
|
||||
export const toPositive = (x) => {
|
||||
if (typeof x === 'bigint') return toPositiveBigInt(x)
|
||||
return toPositiveNumber(x)
|
||||
}
|
||||
|
|
|
@ -97,6 +97,8 @@ function getCallbacks (req, res) {
|
|||
const secret = process.env.NEXTAUTH_SECRET
|
||||
const jwt = await encodeJWT({ token, secret })
|
||||
const me = await prisma.user.findUnique({ where: { id: token.id } })
|
||||
// we set multi_auth cookies on login/signup with only one user so the rest of the code doesn't
|
||||
// have to consider the case where they aren't set yet because account switching wasn't used yet
|
||||
setMultiAuthCookies(req, res, { ...me, jwt })
|
||||
}
|
||||
|
||||
|
|
|
@ -36,9 +36,9 @@ export default (req, res) => {
|
|||
cookies.push(cookie.serialize(`multi_auth.${userId}`, '', { ...cookieOptions, expires: 0, maxAge: 0 }))
|
||||
|
||||
// update multi_auth cookie and check if there are more accounts available
|
||||
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
||||
const newMultiAuth = oldMultiAuth.filter(({ id }) => id !== Number(userId))
|
||||
if (newMultiAuth.length === 0) {
|
||||
const oldMultiAuth = req.cookies.multi_auth ? b64Decode(req.cookies.multi_auth) : undefined
|
||||
const newMultiAuth = oldMultiAuth?.filter(({ id }) => id !== Number(userId))
|
||||
if (!oldMultiAuth || newMultiAuth?.length === 0) {
|
||||
// no next account available. cleanup: remove multi_auth + pointer cookie
|
||||
cookies.push(cookie.serialize('multi_auth', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||
cookies.push(cookie.serialize('multi_auth.user-id', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||
|
|
|
@ -14,11 +14,12 @@ import * as webln from 'wallets/webln'
|
|||
import { walletLogger } from '@/api/resolvers/wallet'
|
||||
import walletDefs from 'wallets/server'
|
||||
import { parsePaymentRequest } from 'ln-service'
|
||||
import { toPositiveNumber } from '@/lib/validate'
|
||||
import { toPositiveBigInt, toPositiveNumber } from '@/lib/validate'
|
||||
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||
import { withTimeout } from '@/lib/time'
|
||||
import { canReceive } from './common'
|
||||
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
||||
import wrapInvoice from './wrap'
|
||||
|
||||
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
|
||||
|
||||
|
@ -96,6 +97,37 @@ export async function createInvoice (userId, { msats, description, descriptionHa
|
|||
throw new Error('no wallet to receive available')
|
||||
}
|
||||
|
||||
export async function createWrappedInvoice (userId,
|
||||
{ msats, feePercent, description, descriptionHash, expiry = 360 },
|
||||
{ models, me, lnd }) {
|
||||
let logger, bolt11
|
||||
try {
|
||||
const { invoice, wallet } = await createInvoice(userId, {
|
||||
// this is the amount the stacker will receive, the other (feePercent)% is our fee
|
||||
msats: toPositiveBigInt(msats) * (100n - feePercent) / 100n,
|
||||
description,
|
||||
descriptionHash,
|
||||
expiry
|
||||
}, { models })
|
||||
|
||||
logger = walletLogger({ wallet, models })
|
||||
bolt11 = invoice
|
||||
|
||||
const { invoice: wrappedInvoice, maxFee } =
|
||||
await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd })
|
||||
|
||||
return {
|
||||
invoice,
|
||||
wrappedInvoice: wrappedInvoice.request,
|
||||
wallet,
|
||||
maxFee
|
||||
}
|
||||
} catch (e) {
|
||||
logger?.error('invalid invoice: ' + e.message, { bolt11 })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function walletCreateInvoice (
|
||||
{
|
||||
msats,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createHodlInvoice, parsePaymentRequest } from 'ln-service'
|
||||
import { estimateRouteFee, getBlockHeight } from '../api/lnd'
|
||||
import { toPositiveNumber } from '@/lib/validate'
|
||||
import { toBigInt, toPositiveBigInt, toPositiveNumber } from '@/lib/validate'
|
||||
|
||||
const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice
|
||||
const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice
|
||||
|
@ -9,20 +9,26 @@ const INCOMING_EXPIRATION_BUFFER_MSECS = 300_000 // the buffer enforce for the i
|
|||
const MAX_OUTGOING_CLTV_DELTA = 500 // the maximum cltv delta we'll allow for the outgoing invoice
|
||||
export const MIN_SETTLEMENT_CLTV_DELTA = 80 // the minimum blocks we'll leave for settling the incoming invoice
|
||||
const FEE_ESTIMATE_TIMEOUT_SECS = 5 // the timeout for the fee estimate request
|
||||
const MAX_FEE_ESTIMATE_PERCENT = 0.025 // the maximum fee relative to outgoing we'll allow for the fee estimate
|
||||
const ZAP_SYBIL_FEE_MULT = 10 / 7 // the fee for the zap sybil service
|
||||
const MAX_FEE_ESTIMATE_PERCENT = 3n // the maximum fee relative to outgoing we'll allow for the fee estimate
|
||||
|
||||
/*
|
||||
The wrapInvoice function is used to wrap an outgoing invoice with the necessary parameters for an incoming hold invoice.
|
||||
|
||||
@param bolt11 {string} the bolt11 invoice to wrap
|
||||
@param options {object}
|
||||
@param args {object} {
|
||||
bolt11: {string} the bolt11 invoice to wrap
|
||||
feePercent: {bigint} the fee percent to use for the incoming invoice
|
||||
}
|
||||
@param options {object} {
|
||||
msats: {bigint} the amount in msats to use for the incoming invoice
|
||||
description: {string} the description to use for the incoming invoice
|
||||
descriptionHash: {string} the description hash to use for the incoming invoice
|
||||
}
|
||||
@returns {
|
||||
invoice: the wrapped incoming invoice,
|
||||
maxFee: number
|
||||
}
|
||||
*/
|
||||
export default async function wrapInvoice (bolt11, { msats, description, descriptionHash }, { me, lnd }) {
|
||||
export default async function wrapInvoice ({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) {
|
||||
try {
|
||||
console.group('wrapInvoice', description)
|
||||
|
||||
|
@ -38,9 +44,17 @@ export default async function wrapInvoice (bolt11, { msats, description, descrip
|
|||
|
||||
console.log('invoice', inv.id, inv.mtokens, inv.expires_at, inv.cltv_delta, inv.destination)
|
||||
|
||||
// validate fee percent
|
||||
if (feePercent) {
|
||||
// assert the fee percent is in the range 0-100
|
||||
feePercent = toBigInt(feePercent, 0n, 100n)
|
||||
} else {
|
||||
throw new Error('Fee percent is missing')
|
||||
}
|
||||
|
||||
// validate outgoing amount
|
||||
if (inv.mtokens) {
|
||||
outgoingMsat = toPositiveNumber(inv.mtokens)
|
||||
outgoingMsat = toPositiveBigInt(inv.mtokens)
|
||||
if (outgoingMsat < MIN_OUTGOING_MSATS) {
|
||||
throw new Error(`Invoice amount is too low: ${outgoingMsat}`)
|
||||
}
|
||||
|
@ -53,8 +67,11 @@ export default async function wrapInvoice (bolt11, { msats, description, descrip
|
|||
|
||||
// validate incoming amount
|
||||
if (msats) {
|
||||
msats = toPositiveNumber(msats)
|
||||
if (outgoingMsat * ZAP_SYBIL_FEE_MULT > msats) {
|
||||
msats = toPositiveBigInt(msats)
|
||||
// outgoing amount should be smaller than the incoming amount
|
||||
// by a factor of exactly 100n / (100n - feePercent)
|
||||
const incomingMsats = outgoingMsat * 100n / (100n - feePercent)
|
||||
if (incomingMsats > msats) {
|
||||
throw new Error('Sybil fee is too low')
|
||||
}
|
||||
} else {
|
||||
|
@ -162,7 +179,7 @@ export default async function wrapInvoice (bolt11, { msats, description, descrip
|
|||
|
||||
// validate the fee budget
|
||||
const minEstFees = toPositiveNumber(routingFeeMsat)
|
||||
const outgoingMaxFeeMsat = Math.ceil(msats * MAX_FEE_ESTIMATE_PERCENT)
|
||||
const outgoingMaxFeeMsat = Math.ceil(toPositiveNumber(msats * MAX_FEE_ESTIMATE_PERCENT) / 100)
|
||||
if (minEstFees > outgoingMaxFeeMsat) {
|
||||
throw new Error('Estimated fees are too high')
|
||||
}
|
||||
|
|
|
@ -112,8 +112,10 @@ async function transitionInvoice (jobName, { invoiceId, fromState, toState, tran
|
|||
async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, models, lnd, boss }) {
|
||||
try {
|
||||
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
|
||||
const result = await paidActions[dbInvoice.actionType].perform(args,
|
||||
{ models, tx, lnd, cost: BigInt(lndInvoice.received_mtokens), me: dbInvoice.user })
|
||||
const context = { tx, cost: BigInt(lndInvoice.received_mtokens) }
|
||||
context.sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
|
||||
|
||||
const result = await paidActions[dbInvoice.actionType].perform(args, context)
|
||||
await tx.invoice.update({
|
||||
where: { id: dbInvoice.id },
|
||||
data: {
|
||||
|
|
Loading…
Reference in New Issue