Compare commits
4 Commits
4675a2c29d
...
7e5a8310df
Author | SHA1 | Date | |
---|---|---|---|
|
7e5a8310df | ||
|
18700b4201 | ||
|
fdd34b2eb3 | ||
|
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
|
- this function is called when an optimistic action is retried
|
||||||
- it's passed the original `invoiceId` and the `newInvoiceId`
|
- 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`
|
- 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
|
- this is only used for p2p wrapped zaps currently
|
||||||
- `describe`: returns a description as a string of the action
|
- `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
|
- 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
|
#### Function arguments
|
||||||
|
|
||||||
@ -179,6 +180,7 @@ All functions have the following signature: `function(args: Object, context: Obj
|
|||||||
`context` contains the following fields:
|
`context` contains the following fields:
|
||||||
- `me`: the user performing the action (undefined if anonymous)
|
- `me`: the user performing the action (undefined if anonymous)
|
||||||
- `cost`: the cost of the action in msats as a `BigInt`
|
- `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)
|
- `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)
|
- `models`: the current prisma client (for anything that doesn't need to be done atomically with the payment)
|
||||||
- `lnd`: the current lnd client
|
- `lnd`: the current lnd client
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
|
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
|
||||||
import { datePivot } from '@/lib/time'
|
import { datePivot } from '@/lib/time'
|
||||||
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
|
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 { Prisma } from '@prisma/client'
|
||||||
import * as ITEM_CREATE from './itemCreate'
|
import * as ITEM_CREATE from './itemCreate'
|
||||||
import * as ITEM_UPDATE from './itemUpdate'
|
import * as ITEM_UPDATE from './itemUpdate'
|
||||||
@ -14,8 +14,7 @@ import * as TERRITORY_BILLING from './territoryBilling'
|
|||||||
import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
|
import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
|
||||||
import * as DONATE from './donate'
|
import * as DONATE from './donate'
|
||||||
import * as BOOST from './boost'
|
import * as BOOST from './boost'
|
||||||
import wrapInvoice from 'wallets/wrap'
|
import { createWrappedInvoice } from 'wallets/server'
|
||||||
import { createInvoice as createUserInvoice } from 'wallets/server'
|
|
||||||
|
|
||||||
export const paidActions = {
|
export const paidActions = {
|
||||||
ITEM_CREATE,
|
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.me = me ? await models.user.findUnique({ where: { id: me.id } }) : undefined
|
||||||
context.cost = await paidAction.getCost(args, context)
|
context.cost = await paidAction.getCost(args, context)
|
||||||
|
context.sybilFeePercent = await paidAction.getSybilFeePercent?.(args, context)
|
||||||
|
|
||||||
if (!me) {
|
if (!me) {
|
||||||
if (!paidAction.anonable) {
|
if (!paidAction.anonable) {
|
||||||
@ -229,8 +229,7 @@ const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
|
|||||||
export async function createLightningInvoice (actionType, args, context) {
|
export async function createLightningInvoice (actionType, args, context) {
|
||||||
// if the action has an invoiceable peer, we'll create a peer invoice
|
// if the action has an invoiceable peer, we'll create a peer invoice
|
||||||
// wrap it, and return the wrapped invoice
|
// wrap it, and return the wrapped invoice
|
||||||
const { cost, models, lnd, me } = context
|
const { cost, models, lnd, sybilFeePercent, me } = context
|
||||||
const userId = await paidActions[actionType]?.invoiceablePeer?.(args, context)
|
|
||||||
|
|
||||||
// count pending invoices and bail if we're over the limit
|
// count pending invoices and bail if we're over the limit
|
||||||
const pendingInvoices = await models.invoice.count({
|
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')
|
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) {
|
if (userId) {
|
||||||
let logger, bolt11
|
|
||||||
try {
|
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 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
|
const { invoice, wrappedInvoice, wallet, maxFee } = await createWrappedInvoice(userId, {
|
||||||
msats: cost * BigInt(7) / BigInt(10),
|
msats: cost,
|
||||||
|
feePercent: sybilFeePercent,
|
||||||
description,
|
description,
|
||||||
expiry: INVOICE_EXPIRE_SECS
|
expiry: INVOICE_EXPIRE_SECS
|
||||||
}, { models })
|
}, { models, me, lnd })
|
||||||
|
|
||||||
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 })
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bolt11,
|
bolt11: invoice,
|
||||||
wrappedBolt11: wrappedInvoice.request,
|
wrappedBolt11: wrappedInvoice,
|
||||||
wallet,
|
wallet,
|
||||||
maxFee
|
maxFee
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger?.error('invalid invoice: ' + e.message, { bolt11 })
|
|
||||||
console.error('failed to create stacker invoice, falling back to SN invoice', e)
|
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)
|
return satsToMsats(sats)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function invoiceablePeer ({ id }, { models }) {
|
export async function getInvoiceablePeer ({ id }, { models }) {
|
||||||
const item = await models.item.findUnique({
|
const item = await models.item.findUnique({
|
||||||
where: { id: parseInt(id) },
|
where: { id: parseInt(id) },
|
||||||
include: {
|
include: {
|
||||||
@ -27,8 +27,12 @@ export async function invoiceablePeer ({ id }, { models }) {
|
|||||||
return item.user.wallets.length > 0 && item.itemForwards.length === 0 ? item.userId : null
|
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 }) {
|
export async function getSybilFeePercent () {
|
||||||
const feeMsats = 3n * (cost / BigInt(10)) // 30% fee
|
return 30n
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, sybilFeePercent, tx }) {
|
||||||
|
const feeMsats = cost * sybilFeePercent / 100n
|
||||||
const zapMsats = cost - feeMsats
|
const zapMsats = cost - feeMsats
|
||||||
itemId = parseInt(itemId)
|
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
|
// 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 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
|
// timer permission check
|
||||||
if (!adminEdit && !myBio && !timer && !isJob(item)) {
|
if (!adminEdit && !myBio && !timer && !isJob(item)) {
|
||||||
|
@ -6,7 +6,7 @@ import Dropdown from 'react-bootstrap/Dropdown'
|
|||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
import { abbrNum, numWithUnits } from '@/lib/format'
|
import { abbrNum, numWithUnits } from '@/lib/format'
|
||||||
import { newComments, commentsViewedAt } from '@/lib/new-comments'
|
import { newComments, commentsViewedAt } from '@/lib/new-comments'
|
||||||
import { timeSince } from '@/lib/time'
|
import { datePivot, timeSince } from '@/lib/time'
|
||||||
import { DeleteDropdownItem } from './delete'
|
import { DeleteDropdownItem } from './delete'
|
||||||
import styles from './item.module.css'
|
import styles from './item.module.css'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
@ -34,10 +34,9 @@ export default function ItemInfo ({
|
|||||||
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true,
|
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true,
|
||||||
setDisableRetry, disableRetry
|
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 { me } = useMe()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [canEdit, setCanEdit] = useState(item.mine && !item.bio && (Date.now() < editThreshold))
|
|
||||||
const [hasNewComments, setHasNewComments] = useState(false)
|
const [hasNewComments, setHasNewComments] = useState(false)
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const sub = item?.sub || root?.sub
|
const sub = item?.sub || root?.sub
|
||||||
@ -48,12 +47,18 @@ export default function ItemInfo ({
|
|||||||
}
|
}
|
||||||
}, [item])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const authorEdit = item.mine && !item.bio
|
|
||||||
const invParams = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
|
const invParams = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
|
||||||
const hmacEdit = !!invParams && !me && Number(item.user.id) === USER_ID.anon
|
setAnonEdit(!!invParams && !me && Number(item.user.id) === USER_ID.anon)
|
||||||
setCanEdit((authorEdit || hmacEdit) && (Date.now() < editThreshold))
|
}, [])
|
||||||
}, [me, item.id, item.mine, editThreshold])
|
|
||||||
|
// 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
|
// territory founders can pin any post in their territory
|
||||||
// and OPs can pin any root reply in their post
|
// and OPs can pin any root reply in their post
|
||||||
@ -152,7 +157,7 @@ export default function ItemInfo ({
|
|||||||
<>
|
<>
|
||||||
<EditInfo
|
<EditInfo
|
||||||
item={item} edit={edit} canEdit={canEdit}
|
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} />
|
<PaymentInfo item={item} disableRetry={disableRetry} setDisableRetry={setDisableRetry} />
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
|
@ -79,6 +79,7 @@ export default function Login ({ providers, callbackUrl, multiAuth, error, text,
|
|||||||
case 'Email':
|
case 'Email':
|
||||||
return (
|
return (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
|
key={provider.id}
|
||||||
placement='bottom'
|
placement='bottom'
|
||||||
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
|
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
|
||||||
trigger={['hover', 'focus']}
|
trigger={['hover', 'focus']}
|
||||||
|
@ -265,6 +265,7 @@ function LogoutObstacle ({ onClose }) {
|
|||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
const { removeLocalWallets } = useWallets()
|
const { removeLocalWallets } = useWallets()
|
||||||
const { multiAuthSignout } = useAccounts()
|
const { multiAuthSignout } = useAccounts()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='d-flex m-auto flex-column w-fit-content'>
|
<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
|
// only signout if multiAuth did not find a next available account
|
||||||
if (switchSuccess) {
|
if (switchSuccess) {
|
||||||
onClose()
|
onClose()
|
||||||
|
// reload whatever page we're on to avoid any bugs
|
||||||
|
router.reload()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
@ -494,26 +494,6 @@ export const lud18PayerDataSchema = (k1) => object({
|
|||||||
identifier: string()
|
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({
|
export const deviceSyncSchema = object().shape({
|
||||||
passphrase: string().required('required')
|
passphrase: string().required('required')
|
||||||
.test(async (value, context) => {
|
.test(async (value, context) => {
|
||||||
@ -533,3 +513,79 @@ export const deviceSyncSchema = object().shape({
|
|||||||
return true
|
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 secret = process.env.NEXTAUTH_SECRET
|
||||||
const jwt = await encodeJWT({ token, secret })
|
const jwt = await encodeJWT({ token, secret })
|
||||||
const me = await prisma.user.findUnique({ where: { id: token.id } })
|
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 })
|
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 }))
|
cookies.push(cookie.serialize(`multi_auth.${userId}`, '', { ...cookieOptions, expires: 0, maxAge: 0 }))
|
||||||
|
|
||||||
// update multi_auth cookie and check if there are more accounts available
|
// update multi_auth cookie and check if there are more accounts available
|
||||||
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
const oldMultiAuth = req.cookies.multi_auth ? b64Decode(req.cookies.multi_auth) : undefined
|
||||||
const newMultiAuth = oldMultiAuth.filter(({ id }) => id !== Number(userId))
|
const newMultiAuth = oldMultiAuth?.filter(({ id }) => id !== Number(userId))
|
||||||
if (newMultiAuth.length === 0) {
|
if (!oldMultiAuth || newMultiAuth?.length === 0) {
|
||||||
// no next account available. cleanup: remove multi_auth + pointer cookie
|
// 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', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||||
cookies.push(cookie.serialize('multi_auth.user-id', '', { ...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 { walletLogger } from '@/api/resolvers/wallet'
|
||||||
import walletDefs from 'wallets/server'
|
import walletDefs from 'wallets/server'
|
||||||
import { parsePaymentRequest } from 'ln-service'
|
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 { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||||
import { withTimeout } from '@/lib/time'
|
import { withTimeout } from '@/lib/time'
|
||||||
import { canReceive } from './common'
|
import { canReceive } from './common'
|
||||||
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
||||||
|
import wrapInvoice from './wrap'
|
||||||
|
|
||||||
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
|
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')
|
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 (
|
async function walletCreateInvoice (
|
||||||
{
|
{
|
||||||
msats,
|
msats,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createHodlInvoice, parsePaymentRequest } from 'ln-service'
|
import { createHodlInvoice, parsePaymentRequest } from 'ln-service'
|
||||||
import { estimateRouteFee, getBlockHeight } from '../api/lnd'
|
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 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
|
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
|
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
|
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 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 MAX_FEE_ESTIMATE_PERCENT = 3n // 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
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The wrapInvoice function is used to wrap an outgoing invoice with the necessary parameters for an incoming hold invoice.
|
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 args {object} {
|
||||||
@param options {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 {
|
@returns {
|
||||||
invoice: the wrapped incoming invoice,
|
invoice: the wrapped incoming invoice,
|
||||||
maxFee: number
|
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 {
|
try {
|
||||||
console.group('wrapInvoice', description)
|
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)
|
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
|
// validate outgoing amount
|
||||||
if (inv.mtokens) {
|
if (inv.mtokens) {
|
||||||
outgoingMsat = toPositiveNumber(inv.mtokens)
|
outgoingMsat = toPositiveBigInt(inv.mtokens)
|
||||||
if (outgoingMsat < MIN_OUTGOING_MSATS) {
|
if (outgoingMsat < MIN_OUTGOING_MSATS) {
|
||||||
throw new Error(`Invoice amount is too low: ${outgoingMsat}`)
|
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
|
// validate incoming amount
|
||||||
if (msats) {
|
if (msats) {
|
||||||
msats = toPositiveNumber(msats)
|
msats = toPositiveBigInt(msats)
|
||||||
if (outgoingMsat * ZAP_SYBIL_FEE_MULT > 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')
|
throw new Error('Sybil fee is too low')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -162,7 +179,7 @@ export default async function wrapInvoice (bolt11, { msats, description, descrip
|
|||||||
|
|
||||||
// validate the fee budget
|
// validate the fee budget
|
||||||
const minEstFees = toPositiveNumber(routingFeeMsat)
|
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) {
|
if (minEstFees > outgoingMaxFeeMsat) {
|
||||||
throw new Error('Estimated fees are too high')
|
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 }) {
|
async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, models, lnd, boss }) {
|
||||||
try {
|
try {
|
||||||
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
|
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
|
||||||
const result = await paidActions[dbInvoice.actionType].perform(args,
|
const context = { tx, cost: BigInt(lndInvoice.received_mtokens) }
|
||||||
{ models, tx, lnd, cost: BigInt(lndInvoice.received_mtokens), me: dbInvoice.user })
|
context.sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
|
||||||
|
|
||||||
|
const result = await paidActions[dbInvoice.actionType].perform(args, context)
|
||||||
await tx.invoice.update({
|
await tx.invoice.update({
|
||||||
where: { id: dbInvoice.id },
|
where: { id: dbInvoice.id },
|
||||||
data: {
|
data: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user