configurable sybil fee (#1577)
* configurable sybil fee * document getSybilFeePercent * fixes * remove null check * refine at the margins --------- Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
		
							parent
							
								
									fdd34b2eb3
								
							
						
					
					
						commit
						18700b4201
					
				@ -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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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