cowboy credits (aka nov-5 (aka jan-3)) (#1678)
* wip adding cowboy credits * invite gift paid action * remove balance limit * remove p2p zap withdrawal notifications * credits typedefs * squash migrations * remove wallet limit stuff * CCs in item detail * comments with meCredits * begin including CCs in item stats/notifications * buy credits ui/mutation * fix old /settings/wallets paths * bios don't get sats * fix settings * make invites work with credits * restore migration from master * inform backend of send wallets on zap * satistics header * default receive options to true and squash migrations * fix paidAction query * add nav for credits * fix forever stacked count * ek suggested fixes * fix lint * fix freebies wrt CCs * add back disable freebies * trigger cowboy hat job on CC depletion * fix meMsats+meMcredits * Update api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * remove expireBoost migration that doesn't work --------- Co-authored-by: ekzyis <ek@stacker.news>
This commit is contained in:
		
							parent
							
								
									47debbcb06
								
							
						
					
					
						commit
						146b60278c
					
				@ -194,6 +194,12 @@ All functions have the following signature: `function(args: Object, context: Obj
 | 
				
			|||||||
- `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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Recording Cowboy Credits
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To avoid adding sats and credits together everywhere to show an aggregate sat value, in most cases we denormalize a `sats` field that carries the "sats value", the combined sats + credits of something, and a `credits` field that carries only the earned `credits`. For example, the `Item` table has an `msats` field that carries the sum of the `mcredits` and `msats` earned and a `mcredits` field that carries the value of the `mcredits` earned. So, the sats value an item earned is `item.msats` BUT the real sats earned is `item.msats - item.mcredits`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The ONLY exception to this are for the `users` table where we store a stacker's rewards sats and credits balances separately.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## `IMPORTANT: transaction isolation`
 | 
					## `IMPORTANT: transaction isolation`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
We use a `read committed` isolation level for actions. This means paid actions need to be mindful of concurrency issues. Specifically, reading data from the database and then writing it back in `read committed` is a common source of consistency bugs (aka serialization anamolies).
 | 
					We use a `read committed` isolation level for actions. This means paid actions need to be mindful of concurrency issues. Specifically, reading data from the database and then writing it back in `read committed` is a common source of consistency bugs (aka serialization anamolies).
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ export const anonable = false
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										32
									
								
								api/paidAction/buyCredits.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								api/paidAction/buyCredits.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
 | 
				
			||||||
 | 
					import { satsToMsats } from '@/lib/format'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const anonable = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const paymentMethods = [
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getCost ({ credits }) {
 | 
				
			||||||
 | 
					  return satsToMsats(credits)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function perform ({ credits }, { me, cost, tx }) {
 | 
				
			||||||
 | 
					  await tx.user.update({
 | 
				
			||||||
 | 
					    where: { id: me.id },
 | 
				
			||||||
 | 
					    data: {
 | 
				
			||||||
 | 
					      mcredits: {
 | 
				
			||||||
 | 
					        increment: cost
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    credits
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function describe () {
 | 
				
			||||||
 | 
					  return 'SN: buy fee credits'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -5,6 +5,7 @@ export const anonable = true
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ export const anonable = false
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,7 @@ 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 * as RECEIVE from './receive'
 | 
					import * as RECEIVE from './receive'
 | 
				
			||||||
 | 
					import * as BUY_CREDITS from './buyCredits'
 | 
				
			||||||
import * as INVITE_GIFT from './inviteGift'
 | 
					import * as INVITE_GIFT from './inviteGift'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const paidActions = {
 | 
					export const paidActions = {
 | 
				
			||||||
@ -33,6 +34,7 @@ export const paidActions = {
 | 
				
			|||||||
  TERRITORY_UNARCHIVE,
 | 
					  TERRITORY_UNARCHIVE,
 | 
				
			||||||
  DONATE,
 | 
					  DONATE,
 | 
				
			||||||
  RECEIVE,
 | 
					  RECEIVE,
 | 
				
			||||||
 | 
					  BUY_CREDITS,
 | 
				
			||||||
  INVITE_GIFT
 | 
					  INVITE_GIFT
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -96,7 +98,8 @@ export default async function performPaidAction (actionType, args, incomingConte
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      // additional payment methods that logged in users can use
 | 
					      // additional payment methods that logged in users can use
 | 
				
			||||||
      if (me) {
 | 
					      if (me) {
 | 
				
			||||||
        if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT) {
 | 
					        if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT ||
 | 
				
			||||||
 | 
					          paymentMethod === PAID_ACTION_PAYMENT_METHODS.REWARD_SATS) {
 | 
				
			||||||
          try {
 | 
					          try {
 | 
				
			||||||
            return await performNoInvoiceAction(actionType, args, contextWithPaymentMethod)
 | 
					            return await performNoInvoiceAction(actionType, args, contextWithPaymentMethod)
 | 
				
			||||||
          } catch (e) {
 | 
					          } catch (e) {
 | 
				
			||||||
@ -141,6 +144,13 @@ async function performNoInvoiceAction (actionType, args, incomingContext) {
 | 
				
			|||||||
    const context = { ...incomingContext, tx }
 | 
					    const context = { ...incomingContext, tx }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (paymentMethod === 'FEE_CREDIT') {
 | 
					    if (paymentMethod === 'FEE_CREDIT') {
 | 
				
			||||||
 | 
					      await tx.user.update({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          id: me?.id ?? USER_ID.anon
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        data: { mcredits: { decrement: cost } }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    } else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.REWARD_SATS) {
 | 
				
			||||||
      await tx.user.update({
 | 
					      await tx.user.update({
 | 
				
			||||||
        where: {
 | 
					        where: {
 | 
				
			||||||
          id: me?.id ?? USER_ID.anon
 | 
					          id: me?.id ?? USER_ID.anon
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,8 @@ import { notifyInvite } from '@/lib/webPush'
 | 
				
			|||||||
export const anonable = false
 | 
					export const anonable = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function getCost ({ id }, { models, me }) {
 | 
					export async function getCost ({ id }, { models, me }) {
 | 
				
			||||||
@ -36,7 +37,7 @@ export async function perform ({ id, userId }, { me, cost, tx }) {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    data: {
 | 
					    data: {
 | 
				
			||||||
      msats: {
 | 
					      mcredits: {
 | 
				
			||||||
        increment: cost
 | 
					        increment: cost
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      inviteId: id,
 | 
					      inviteId: id,
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ export const anonable = true
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
 | 
					  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
@ -29,7 +30,7 @@ export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio },
 | 
				
			|||||||
  // sub allows freebies (or is a bio or a comment), cost is less than baseCost, not anon,
 | 
					  // sub allows freebies (or is a bio or a comment), cost is less than baseCost, not anon,
 | 
				
			||||||
  // cost must be greater than user's balance, and user has not disabled freebies
 | 
					  // cost must be greater than user's balance, and user has not disabled freebies
 | 
				
			||||||
  const freebie = (parentId || bio) && cost <= baseCost && !!me &&
 | 
					  const freebie = (parentId || bio) && cost <= baseCost && !!me &&
 | 
				
			||||||
    cost > me?.msats && !me?.disableFreebies
 | 
					    me?.msats < cost && !me?.disableFreebies && me?.mcredits < cost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return freebie ? BigInt(0) : BigInt(cost)
 | 
					  return freebie ? BigInt(0) : BigInt(cost)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ export const anonable = true
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,9 @@
 | 
				
			|||||||
import { BALANCE_LIMIT_MSATS, PAID_ACTION_TERMINAL_STATES, USER_ID, SN_ADMIN_IDS } from '@/lib/constants'
 | 
					import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
 | 
				
			||||||
import { msatsToSats, numWithUnits } from '@/lib/format'
 | 
					 | 
				
			||||||
import { datePivot } from '@/lib/time'
 | 
					import { datePivot } from '@/lib/time'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
 | 
					const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
 | 
				
			||||||
const MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES = 10
 | 
					const MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES = 10
 | 
				
			||||||
const MAX_PENDING_DIRECT_INVOICES_PER_USER = 100
 | 
					const MAX_PENDING_DIRECT_INVOICES_PER_USER = 100
 | 
				
			||||||
const USER_IDS_BALANCE_NO_LIMIT = [...SN_ADMIN_IDS, USER_ID.anon, USER_ID.ad]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function assertBelowMaxPendingInvoices (context) {
 | 
					export async function assertBelowMaxPendingInvoices (context) {
 | 
				
			||||||
  const { models, me } = context
 | 
					  const { models, me } = context
 | 
				
			||||||
@ -56,47 +54,3 @@ export async function assertBelowMaxPendingDirectPayments (userId, context) {
 | 
				
			|||||||
    throw new Error('Receiver has too many direct payments')
 | 
					    throw new Error('Receiver has too many direct payments')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function assertBelowBalanceLimit (context) {
 | 
					 | 
				
			||||||
  const { me, tx } = context
 | 
					 | 
				
			||||||
  if (!me || USER_IDS_BALANCE_NO_LIMIT.includes(me.id)) return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // we need to prevent this invoice (and any other pending invoices and withdrawls)
 | 
					 | 
				
			||||||
  // from causing the user's balance to exceed the balance limit
 | 
					 | 
				
			||||||
  const pendingInvoices = await tx.invoice.aggregate({
 | 
					 | 
				
			||||||
    where: {
 | 
					 | 
				
			||||||
      userId: me.id,
 | 
					 | 
				
			||||||
      // p2p invoices are never in state PENDING
 | 
					 | 
				
			||||||
      actionState: 'PENDING',
 | 
					 | 
				
			||||||
      actionType: 'RECEIVE'
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    _sum: {
 | 
					 | 
				
			||||||
      msatsRequested: true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Get pending withdrawals total
 | 
					 | 
				
			||||||
  const pendingWithdrawals = await tx.withdrawl.aggregate({
 | 
					 | 
				
			||||||
    where: {
 | 
					 | 
				
			||||||
      userId: me.id,
 | 
					 | 
				
			||||||
      status: null
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    _sum: {
 | 
					 | 
				
			||||||
      msatsPaying: true,
 | 
					 | 
				
			||||||
      msatsFeePaying: true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Calculate total pending amount
 | 
					 | 
				
			||||||
  const pendingMsats = (pendingInvoices._sum.msatsRequested ?? 0n) +
 | 
					 | 
				
			||||||
    ((pendingWithdrawals._sum.msatsPaying ?? 0n) + (pendingWithdrawals._sum.msatsFeePaying ?? 0n))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Check balance limit
 | 
					 | 
				
			||||||
  if (pendingMsats + me.msats > BALANCE_LIMIT_MSATS) {
 | 
					 | 
				
			||||||
    throw new Error(
 | 
					 | 
				
			||||||
      `pending invoices and withdrawals must not cause balance to exceed ${
 | 
					 | 
				
			||||||
        numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))
 | 
					 | 
				
			||||||
      }`
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ export const anonable = false
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@ import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
 | 
				
			|||||||
import { toPositiveBigInt, numWithUnits, msatsToSats, satsToMsats } from '@/lib/format'
 | 
					import { toPositiveBigInt, numWithUnits, msatsToSats, satsToMsats } from '@/lib/format'
 | 
				
			||||||
import { notifyDeposit } from '@/lib/webPush'
 | 
					import { notifyDeposit } from '@/lib/webPush'
 | 
				
			||||||
import { getInvoiceableWallets } from '@/wallets/server'
 | 
					import { getInvoiceableWallets } from '@/wallets/server'
 | 
				
			||||||
import { assertBelowBalanceLimit } from './lib/assert'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const anonable = false
 | 
					export const anonable = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -19,13 +18,16 @@ export async function getCost ({ msats }) {
 | 
				
			|||||||
export async function getInvoiceablePeer (_, { me, models, cost, paymentMethod }) {
 | 
					export async function getInvoiceablePeer (_, { me, models, cost, paymentMethod }) {
 | 
				
			||||||
  if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P && !me?.proxyReceive) return null
 | 
					  if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P && !me?.proxyReceive) return null
 | 
				
			||||||
  if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.DIRECT && !me?.directReceive) return null
 | 
					  if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.DIRECT && !me?.directReceive) return null
 | 
				
			||||||
  if ((cost + me.msats) <= satsToMsats(me.autoWithdrawThreshold)) return null
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const wallets = await getInvoiceableWallets(me.id, { models })
 | 
					  const wallets = await getInvoiceableWallets(me.id, { models })
 | 
				
			||||||
  if (wallets.length === 0) {
 | 
					  if (wallets.length === 0) {
 | 
				
			||||||
    return null
 | 
					    return null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (cost < satsToMsats(me.receiveCreditsBelowSats)) {
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return me.id
 | 
					  return me.id
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -39,7 +41,7 @@ export async function perform ({
 | 
				
			|||||||
  lud18Data,
 | 
					  lud18Data,
 | 
				
			||||||
  noteStr
 | 
					  noteStr
 | 
				
			||||||
}, { me, tx }) {
 | 
					}, { me, tx }) {
 | 
				
			||||||
  const invoice = await tx.invoice.update({
 | 
					  return await tx.invoice.update({
 | 
				
			||||||
    where: { id: invoiceId },
 | 
					    where: { id: invoiceId },
 | 
				
			||||||
    data: {
 | 
					    data: {
 | 
				
			||||||
      comment,
 | 
					      comment,
 | 
				
			||||||
@ -48,11 +50,6 @@ export async function perform ({
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    include: { invoiceForward: true }
 | 
					    include: { invoiceForward: true }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!invoice.invoiceForward) {
 | 
					 | 
				
			||||||
    // if the invoice is not p2p, assert that the user's balance limit is not exceeded
 | 
					 | 
				
			||||||
    await assertBelowBalanceLimit({ me, tx })
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function describe ({ description }, { me, cost, paymentMethod, sybilFeePercent }) {
 | 
					export async function describe ({ description }, { me, cost, paymentMethod, sybilFeePercent }) {
 | 
				
			||||||
@ -73,7 +70,7 @@ export async function onPaid ({ invoice }, { tx }) {
 | 
				
			|||||||
  await tx.user.update({
 | 
					  await tx.user.update({
 | 
				
			||||||
    where: { id: invoice.userId },
 | 
					    where: { id: invoice.userId },
 | 
				
			||||||
    data: {
 | 
					    data: {
 | 
				
			||||||
      msats: {
 | 
					      mcredits: {
 | 
				
			||||||
        increment: invoice.msatsReceived
 | 
					        increment: invoice.msatsReceived
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ export const anonable = false
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ export const anonable = false
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ export const anonable = false
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ export const anonable = false
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,9 @@ import { getInvoiceableWallets } from '@/wallets/server'
 | 
				
			|||||||
export const anonable = true
 | 
					export const anonable = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const paymentMethods = [
 | 
					export const paymentMethods = [
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
					 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.P2P,
 | 
					  PAID_ACTION_PAYMENT_METHODS.P2P,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
 | 
				
			||||||
 | 
					  PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
 | 
					  PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
 | 
				
			||||||
  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
					  PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
@ -16,16 +17,38 @@ export async function getCost ({ sats }) {
 | 
				
			|||||||
  return satsToMsats(sats)
 | 
					  return satsToMsats(sats)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function getInvoiceablePeer ({ id }, { models }) {
 | 
					export async function getInvoiceablePeer ({ id, sats, hasSendWallet }, { models, me, cost }) {
 | 
				
			||||||
 | 
					  // if the zap is dust, or if me doesn't have a send wallet but has enough sats/credits to pay for it
 | 
				
			||||||
 | 
					  // then we don't invoice the peer
 | 
				
			||||||
 | 
					  if (sats < me?.sendCreditsBelowSats ||
 | 
				
			||||||
 | 
					    (me && !hasSendWallet && (me.mcredits >= cost || me.msats >= cost))) {
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const item = await models.item.findUnique({
 | 
					  const item = await models.item.findUnique({
 | 
				
			||||||
    where: { id: parseInt(id) },
 | 
					    where: { id: parseInt(id) },
 | 
				
			||||||
    include: { itemForwards: true }
 | 
					    include: {
 | 
				
			||||||
 | 
					      itemForwards: true,
 | 
				
			||||||
 | 
					      user: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // bios don't get sats
 | 
				
			||||||
 | 
					  if (item.bio) {
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const wallets = await getInvoiceableWallets(item.userId, { models })
 | 
					  const wallets = await getInvoiceableWallets(item.userId, { models })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // request peer invoice if they have an attached wallet and have not forwarded the item
 | 
					  // request peer invoice if they have an attached wallet and have not forwarded the item
 | 
				
			||||||
  return wallets.length > 0 && item.itemForwards.length === 0 ? item.userId : null
 | 
					  // and the receiver doesn't want to receive credits
 | 
				
			||||||
 | 
					  if (wallets.length > 0 &&
 | 
				
			||||||
 | 
					    item.itemForwards.length === 0 &&
 | 
				
			||||||
 | 
					    sats >= item.user.receiveCreditsBelowSats) {
 | 
				
			||||||
 | 
					    return item.userId
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function getSybilFeePercent () {
 | 
					export async function getSybilFeePercent () {
 | 
				
			||||||
@ -90,32 +113,38 @@ export async function onPaid ({ invoice, actIds }, { tx }) {
 | 
				
			|||||||
  const sats = msatsToSats(msats)
 | 
					  const sats = msatsToSats(msats)
 | 
				
			||||||
  const itemAct = acts.find(act => act.act === 'TIP')
 | 
					  const itemAct = acts.find(act => act.act === 'TIP')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // give user and all forwards the sats
 | 
					  if (invoice?.invoiceForward) {
 | 
				
			||||||
  await tx.$executeRaw`
 | 
					    // only the op got sats and we need to add it to their stackedMsats
 | 
				
			||||||
    WITH forwardees AS (
 | 
					    // because the sats were p2p
 | 
				
			||||||
      SELECT "userId", ((${itemAct.msats}::BIGINT * pct) / 100)::BIGINT AS msats
 | 
					    await tx.user.update({
 | 
				
			||||||
      FROM "ItemForward"
 | 
					      where: { id: itemAct.item.userId },
 | 
				
			||||||
      WHERE "itemId" = ${itemAct.itemId}::INTEGER
 | 
					      data: { stackedMsats: { increment: itemAct.msats } }
 | 
				
			||||||
    ), total_forwarded AS (
 | 
					    })
 | 
				
			||||||
      SELECT COALESCE(SUM(msats), 0) as msats
 | 
					  } else {
 | 
				
			||||||
      FROM forwardees
 | 
					    // splits only use mcredits
 | 
				
			||||||
    ), recipients AS (
 | 
					    await tx.$executeRaw`
 | 
				
			||||||
      SELECT "userId", msats, msats AS "stackedMsats" FROM forwardees
 | 
					      WITH forwardees AS (
 | 
				
			||||||
      UNION
 | 
					        SELECT "userId", ((${itemAct.msats}::BIGINT * pct) / 100)::BIGINT AS mcredits
 | 
				
			||||||
      SELECT ${itemAct.item.userId}::INTEGER as "userId",
 | 
					        FROM "ItemForward"
 | 
				
			||||||
        CASE WHEN ${!!invoice?.invoiceForward}::BOOLEAN
 | 
					        WHERE "itemId" = ${itemAct.itemId}::INTEGER
 | 
				
			||||||
          THEN 0::BIGINT
 | 
					      ), total_forwarded AS (
 | 
				
			||||||
          ELSE ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT
 | 
					        SELECT COALESCE(SUM(mcredits), 0) as mcredits
 | 
				
			||||||
        END as msats,
 | 
					        FROM forwardees
 | 
				
			||||||
        ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT as "stackedMsats"
 | 
					      ), recipients AS (
 | 
				
			||||||
      ORDER BY "userId" ASC -- order to prevent deadlocks
 | 
					        SELECT "userId", mcredits FROM forwardees
 | 
				
			||||||
    )
 | 
					        UNION
 | 
				
			||||||
    UPDATE users
 | 
					        SELECT ${itemAct.item.userId}::INTEGER as "userId",
 | 
				
			||||||
    SET
 | 
					          ${itemAct.msats}::BIGINT - (SELECT mcredits FROM total_forwarded)::BIGINT as mcredits
 | 
				
			||||||
      msats = users.msats + recipients.msats,
 | 
					        ORDER BY "userId" ASC -- order to prevent deadlocks
 | 
				
			||||||
      "stackedMsats" = users."stackedMsats" + recipients."stackedMsats"
 | 
					      )
 | 
				
			||||||
    FROM recipients
 | 
					      UPDATE users
 | 
				
			||||||
    WHERE users.id = recipients."userId"`
 | 
					      SET
 | 
				
			||||||
 | 
					        mcredits = users.mcredits + recipients.mcredits,
 | 
				
			||||||
 | 
					        "stackedMsats" = users."stackedMsats" + recipients.mcredits,
 | 
				
			||||||
 | 
					        "stackedMcredits" = users."stackedMcredits" + recipients.mcredits
 | 
				
			||||||
 | 
					      FROM recipients
 | 
				
			||||||
 | 
					      WHERE users.id = recipients."userId"`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt
 | 
					  // perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt
 | 
				
			||||||
  // NOTE: for the rows that might be updated by a concurrent zap, we use UPDATE for implicit locking
 | 
					  // NOTE: for the rows that might be updated by a concurrent zap, we use UPDATE for implicit locking
 | 
				
			||||||
@ -135,6 +164,7 @@ export async function onPaid ({ invoice, actIds }, { tx }) {
 | 
				
			|||||||
      "weightedVotes" = "weightedVotes" + (zapper.trust * zap.log_sats),
 | 
					      "weightedVotes" = "weightedVotes" + (zapper.trust * zap.log_sats),
 | 
				
			||||||
      upvotes = upvotes + zap.first_vote,
 | 
					      upvotes = upvotes + zap.first_vote,
 | 
				
			||||||
      msats = "Item".msats + ${msats}::BIGINT,
 | 
					      msats = "Item".msats + ${msats}::BIGINT,
 | 
				
			||||||
 | 
					      mcredits = "Item".mcredits + ${invoice?.invoiceForward ? 0n : msats}::BIGINT,
 | 
				
			||||||
      "lastZapAt" = now()
 | 
					      "lastZapAt" = now()
 | 
				
			||||||
    FROM zap, zapper
 | 
					    FROM zap, zapper
 | 
				
			||||||
    WHERE "Item".id = ${itemAct.itemId}::INTEGER
 | 
					    WHERE "Item".id = ${itemAct.itemId}::INTEGER
 | 
				
			||||||
@ -165,7 +195,8 @@ export async function onPaid ({ invoice, actIds }, { tx }) {
 | 
				
			|||||||
        SELECT * FROM "Item" WHERE id = ${itemAct.itemId}::INTEGER
 | 
					        SELECT * FROM "Item" WHERE id = ${itemAct.itemId}::INTEGER
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      UPDATE "Item"
 | 
					      UPDATE "Item"
 | 
				
			||||||
      SET "commentMsats" = "Item"."commentMsats" + ${msats}::BIGINT
 | 
					      SET "commentMsats" = "Item"."commentMsats" + ${msats}::BIGINT,
 | 
				
			||||||
 | 
					        "commentMcredits" = "Item"."commentMcredits" + ${invoice?.invoiceForward ? 0n : msats}::BIGINT
 | 
				
			||||||
      FROM zapped
 | 
					      FROM zapped
 | 
				
			||||||
      WHERE "Item".path @> zapped.path AND "Item".id <> zapped.id`
 | 
					      WHERE "Item".path @> zapped.path AND "Item".id <> zapped.id`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -83,7 +83,7 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    poor: async (invite, args, { me, models }) => {
 | 
					    poor: async (invite, args, { me, models }) => {
 | 
				
			||||||
      const user = await models.user.findUnique({ where: { id: invite.userId } })
 | 
					      const user = await models.user.findUnique({ where: { id: invite.userId } })
 | 
				
			||||||
      return msatsToSats(user.msats) < invite.gift
 | 
					      return msatsToSats(user.msats) < invite.gift && msatsToSats(user.mcredits) < invite.gift
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    description: (invite, args, { me }) => {
 | 
					    description: (invite, args, { me }) => {
 | 
				
			||||||
      return invite.userId === me?.id ? invite.description : undefined
 | 
					      return invite.userId === me?.id ? invite.description : undefined
 | 
				
			||||||
 | 
				
			|||||||
@ -150,6 +150,7 @@ export async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ..
 | 
				
			|||||||
    return await models.$queryRawUnsafe(`
 | 
					    return await models.$queryRawUnsafe(`
 | 
				
			||||||
      SELECT "Item".*, to_jsonb(users.*) || jsonb_build_object('meMute', "Mute"."mutedId" IS NOT NULL) as user,
 | 
					      SELECT "Item".*, to_jsonb(users.*) || jsonb_build_object('meMute', "Mute"."mutedId" IS NOT NULL) as user,
 | 
				
			||||||
        COALESCE("ItemAct"."meMsats", 0) as "meMsats", COALESCE("ItemAct"."mePendingMsats", 0) as "mePendingMsats",
 | 
					        COALESCE("ItemAct"."meMsats", 0) as "meMsats", COALESCE("ItemAct"."mePendingMsats", 0) as "mePendingMsats",
 | 
				
			||||||
 | 
					        COALESCE("ItemAct"."meMcredits", 0) as "meMcredits", COALESCE("ItemAct"."mePendingMcredits", 0) as "mePendingMcredits",
 | 
				
			||||||
        COALESCE("ItemAct"."meDontLikeMsats", 0) as "meDontLikeMsats", b."itemId" IS NOT NULL AS "meBookmark",
 | 
					        COALESCE("ItemAct"."meDontLikeMsats", 0) as "meDontLikeMsats", b."itemId" IS NOT NULL AS "meBookmark",
 | 
				
			||||||
        "ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward",
 | 
					        "ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", "ItemForward"."itemId" IS NOT NULL AS "meForward",
 | 
				
			||||||
        to_jsonb("Sub".*) || jsonb_build_object('meMuteSub', "MuteSub"."userId" IS NOT NULL)
 | 
					        to_jsonb("Sub".*) || jsonb_build_object('meMuteSub', "MuteSub"."userId" IS NOT NULL)
 | 
				
			||||||
@ -167,10 +168,14 @@ export async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ..
 | 
				
			|||||||
      LEFT JOIN "SubSubscription" ON "Sub"."name" = "SubSubscription"."subName" AND "SubSubscription"."userId" = ${me.id}
 | 
					      LEFT JOIN "SubSubscription" ON "Sub"."name" = "SubSubscription"."subName" AND "SubSubscription"."userId" = ${me.id}
 | 
				
			||||||
      LEFT JOIN LATERAL (
 | 
					      LEFT JOIN LATERAL (
 | 
				
			||||||
        SELECT "itemId",
 | 
					        SELECT "itemId",
 | 
				
			||||||
          sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM 'FAILED' AND (act = 'FEE' OR act = 'TIP')) AS "meMsats",
 | 
					          sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM 'FAILED' AND "InvoiceForward".id IS NOT NULL AND (act = 'FEE' OR act = 'TIP')) AS "meMsats",
 | 
				
			||||||
          sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS NOT DISTINCT FROM 'PENDING' AND (act = 'FEE' OR act = 'TIP') AND "Item"."userId" <> ${me.id}) AS "mePendingMsats",
 | 
					          sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM 'FAILED' AND "InvoiceForward".id IS NULL AND (act = 'FEE' OR act = 'TIP')) AS "meMcredits",
 | 
				
			||||||
 | 
					          sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS NOT DISTINCT FROM 'PENDING' AND "InvoiceForward".id IS NOT NULL AND (act = 'FEE' OR act = 'TIP')) AS "mePendingMsats",
 | 
				
			||||||
 | 
					          sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS NOT DISTINCT FROM 'PENDING' AND "InvoiceForward".id IS NULL AND (act = 'FEE' OR act = 'TIP')) AS "mePendingMcredits",
 | 
				
			||||||
          sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM 'FAILED' AND act = 'DONT_LIKE_THIS') AS "meDontLikeMsats"
 | 
					          sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM 'FAILED' AND act = 'DONT_LIKE_THIS') AS "meDontLikeMsats"
 | 
				
			||||||
        FROM "ItemAct"
 | 
					        FROM "ItemAct"
 | 
				
			||||||
 | 
					        LEFT JOIN "Invoice" ON "Invoice".id = "ItemAct"."invoiceId"
 | 
				
			||||||
 | 
					        LEFT JOIN "InvoiceForward" ON "InvoiceForward"."invoiceId" = "Invoice"."id"
 | 
				
			||||||
        WHERE "ItemAct"."userId" = ${me.id}
 | 
					        WHERE "ItemAct"."userId" = ${me.id}
 | 
				
			||||||
        AND "ItemAct"."itemId" = "Item".id
 | 
					        AND "ItemAct"."itemId" = "Item".id
 | 
				
			||||||
        GROUP BY "ItemAct"."itemId"
 | 
					        GROUP BY "ItemAct"."itemId"
 | 
				
			||||||
@ -940,7 +945,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      return await performPaidAction('POLL_VOTE', { id }, { me, models, lnd })
 | 
					      return await performPaidAction('POLL_VOTE', { id }, { me, models, lnd })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    act: async (parent, { id, sats, act = 'TIP' }, { me, models, lnd, headers }) => {
 | 
					    act: async (parent, { id, sats, act = 'TIP', hasSendWallet }, { me, models, lnd, headers }) => {
 | 
				
			||||||
      assertApiKeyNotPermitted({ me })
 | 
					      assertApiKeyNotPermitted({ me })
 | 
				
			||||||
      await validateSchema(actSchema, { sats, act })
 | 
					      await validateSchema(actSchema, { sats, act })
 | 
				
			||||||
      await assertGofacYourself({ models, headers })
 | 
					      await assertGofacYourself({ models, headers })
 | 
				
			||||||
@ -974,7 +979,7 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (act === 'TIP') {
 | 
					      if (act === 'TIP') {
 | 
				
			||||||
        return await performPaidAction('ZAP', { id, sats }, { me, models, lnd })
 | 
					        return await performPaidAction('ZAP', { id, sats, hasSendWallet }, { me, models, lnd })
 | 
				
			||||||
      } else if (act === 'DONT_LIKE_THIS') {
 | 
					      } else if (act === 'DONT_LIKE_THIS') {
 | 
				
			||||||
        return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd })
 | 
					        return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd })
 | 
				
			||||||
      } else if (act === 'BOOST') {
 | 
					      } else if (act === 'BOOST') {
 | 
				
			||||||
@ -1049,11 +1054,17 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  Item: {
 | 
					  Item: {
 | 
				
			||||||
    sats: async (item, args, { models }) => {
 | 
					    sats: async (item, args, { models }) => {
 | 
				
			||||||
      return msatsToSats(BigInt(item.msats) + BigInt(item.mePendingMsats || 0))
 | 
					      return msatsToSats(BigInt(item.msats) + BigInt(item.mePendingMsats || 0) + BigInt(item.mePendingMcredits || 0))
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    credits: async (item, args, { models }) => {
 | 
				
			||||||
 | 
					      return msatsToSats(BigInt(item.mcredits) + BigInt(item.mePendingMcredits || 0))
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    commentSats: async (item, args, { models }) => {
 | 
					    commentSats: async (item, args, { models }) => {
 | 
				
			||||||
      return msatsToSats(item.commentMsats)
 | 
					      return msatsToSats(item.commentMsats)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    commentCredits: async (item, args, { models }) => {
 | 
				
			||||||
 | 
					      return msatsToSats(item.commentMcredits)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    isJob: async (item, args, { models }) => {
 | 
					    isJob: async (item, args, { models }) => {
 | 
				
			||||||
      return item.subName === 'jobs'
 | 
					      return item.subName === 'jobs'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -1170,8 +1181,8 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    meSats: async (item, args, { me, models }) => {
 | 
					    meSats: async (item, args, { me, models }) => {
 | 
				
			||||||
      if (!me) return 0
 | 
					      if (!me) return 0
 | 
				
			||||||
      if (typeof item.meMsats !== 'undefined') {
 | 
					      if (typeof item.meMsats !== 'undefined' && typeof item.meMcredits !== 'undefined') {
 | 
				
			||||||
        return msatsToSats(item.meMsats)
 | 
					        return msatsToSats(BigInt(item.meMsats) + BigInt(item.meMcredits))
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const { _sum: { msats } } = await models.itemAct.aggregate({
 | 
					      const { _sum: { msats } } = await models.itemAct.aggregate({
 | 
				
			||||||
@ -1197,6 +1208,38 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      return (msats && msatsToSats(msats)) || 0
 | 
					      return (msats && msatsToSats(msats)) || 0
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    meCredits: async (item, args, { me, models }) => {
 | 
				
			||||||
 | 
					      if (!me) return 0
 | 
				
			||||||
 | 
					      if (typeof item.meMcredits !== 'undefined') {
 | 
				
			||||||
 | 
					        return msatsToSats(item.meMcredits)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { _sum: { msats } } = await models.itemAct.aggregate({
 | 
				
			||||||
 | 
					        _sum: {
 | 
				
			||||||
 | 
					          msats: true
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          itemId: Number(item.id),
 | 
				
			||||||
 | 
					          userId: me.id,
 | 
				
			||||||
 | 
					          invoiceActionState: {
 | 
				
			||||||
 | 
					            not: 'FAILED'
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          invoice: {
 | 
				
			||||||
 | 
					            invoiceForward: { is: null }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          OR: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              act: 'TIP'
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              act: 'FEE'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return (msats && msatsToSats(msats)) || 0
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    meDontLikeSats: async (item, args, { me, models }) => {
 | 
					    meDontLikeSats: async (item, args, { me, models }) => {
 | 
				
			||||||
      if (!me) return 0
 | 
					      if (!me) return 0
 | 
				
			||||||
      if (typeof item.meDontLikeMsats !== 'undefined') {
 | 
					      if (typeof item.meDontLikeMsats !== 'undefined') {
 | 
				
			||||||
 | 
				
			|||||||
@ -247,7 +247,7 @@ export default {
 | 
				
			|||||||
            WHERE "Withdrawl"."userId" = $1
 | 
					            WHERE "Withdrawl"."userId" = $1
 | 
				
			||||||
            AND "Withdrawl".status = 'CONFIRMED'
 | 
					            AND "Withdrawl".status = 'CONFIRMED'
 | 
				
			||||||
            AND "Withdrawl".created_at < $2
 | 
					            AND "Withdrawl".created_at < $2
 | 
				
			||||||
            AND ("InvoiceForward"."id" IS NULL OR "Invoice"."actionType" = 'ZAP')
 | 
					            AND "InvoiceForward"."id" IS NULL
 | 
				
			||||||
            GROUP BY "Withdrawl".id
 | 
					            GROUP BY "Withdrawl".id
 | 
				
			||||||
            ORDER BY "sortTime" DESC
 | 
					            ORDER BY "sortTime" DESC
 | 
				
			||||||
            LIMIT ${LIMIT})`
 | 
					            LIMIT ${LIMIT})`
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,8 @@ function paidActionType (actionType) {
 | 
				
			|||||||
      return 'PollVotePaidAction'
 | 
					      return 'PollVotePaidAction'
 | 
				
			||||||
    case 'RECEIVE':
 | 
					    case 'RECEIVE':
 | 
				
			||||||
      return 'ReceivePaidAction'
 | 
					      return 'ReceivePaidAction'
 | 
				
			||||||
 | 
					    case 'BUY_CREDITS':
 | 
				
			||||||
 | 
					      return 'BuyCreditsPaidAction'
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
      throw new Error('Unknown action type')
 | 
					      throw new Error('Unknown action type')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -1024,7 +1024,13 @@ export default {
 | 
				
			|||||||
      if (!me || me.id !== user.id) {
 | 
					      if (!me || me.id !== user.id) {
 | 
				
			||||||
        return 0
 | 
					        return 0
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return msatsToSats(user.msats)
 | 
					      return msatsToSats(user.msats + user.mcredits)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    credits: async (user, args, { models, me }) => {
 | 
				
			||||||
 | 
					      if (!me || me.id !== user.id) {
 | 
				
			||||||
 | 
					        return 0
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return msatsToSats(user.mcredits)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    authMethods,
 | 
					    authMethods,
 | 
				
			||||||
    hasInvites: async (user, args, { models }) => {
 | 
					    hasInvites: async (user, args, { models }) => {
 | 
				
			||||||
@ -1106,7 +1112,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (!when || when === 'forever') {
 | 
					      if (!when || when === 'forever') {
 | 
				
			||||||
        // forever
 | 
					        // forever
 | 
				
			||||||
        return (user.stackedMsats && msatsToSats(user.stackedMsats)) || 0
 | 
					        return ((user.stackedMsats && msatsToSats(user.stackedMsats)) || 0)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const range = whenRange(when, from, to)
 | 
					      const range = whenRange(when, from, to)
 | 
				
			||||||
 | 
				
			|||||||
@ -583,6 +583,9 @@ const resolvers = {
 | 
				
			|||||||
      await models.walletLog.deleteMany({ where: { userId: me.id, wallet } })
 | 
					      await models.walletLog.deleteMany({ where: { userId: me.id, wallet } })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return true
 | 
					      return true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    buyCredits: async (parent, { credits }, { me, models, lnd }) => {
 | 
				
			||||||
 | 
					      return await performPaidAction('BUY_CREDITS', { credits }, { models, me, lnd })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -644,6 +647,9 @@ const resolvers = {
 | 
				
			|||||||
      }))?.withdrawl?.msatsPaid
 | 
					      }))?.withdrawl?.msatsPaid
 | 
				
			||||||
      return msats ? msatsToSats(msats) : null
 | 
					      return msats ? msatsToSats(msats) : null
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    invoiceForward: async (invoice, args, { models }) => {
 | 
				
			||||||
 | 
					      return !!invoice.invoiceForward || !!(await models.invoiceForward.findUnique({ where: { invoiceId: Number(invoice.id) } }))
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    nostr: async (invoice, args, { models }) => {
 | 
					    nostr: async (invoice, args, { models }) => {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        return JSON.parse(invoice.desc)
 | 
					        return JSON.parse(invoice.desc)
 | 
				
			||||||
 | 
				
			|||||||
@ -60,7 +60,7 @@ export default gql`
 | 
				
			|||||||
      hash: String, hmac: String): ItemPaidAction!
 | 
					      hash: String, hmac: String): ItemPaidAction!
 | 
				
			||||||
    updateNoteId(id: ID!, noteId: String!): Item!
 | 
					    updateNoteId(id: ID!, noteId: String!): Item!
 | 
				
			||||||
    upsertComment(id: ID, text: String!, parentId: ID, boost: Int, hash: String, hmac: String): ItemPaidAction!
 | 
					    upsertComment(id: ID, text: String!, parentId: ID, boost: Int, hash: String, hmac: String): ItemPaidAction!
 | 
				
			||||||
    act(id: ID!, sats: Int, act: String, idempotent: Boolean): ItemActPaidAction!
 | 
					    act(id: ID!, sats: Int, act: String, hasSendWallet: Boolean): ItemActPaidAction!
 | 
				
			||||||
    pollVote(id: ID!): PollVotePaidAction!
 | 
					    pollVote(id: ID!): PollVotePaidAction!
 | 
				
			||||||
    toggleOutlaw(id: ID!): Item!
 | 
					    toggleOutlaw(id: ID!): Item!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -127,10 +127,13 @@ export default gql`
 | 
				
			|||||||
    bountyPaidTo: [Int]
 | 
					    bountyPaidTo: [Int]
 | 
				
			||||||
    noteId: String
 | 
					    noteId: String
 | 
				
			||||||
    sats: Int!
 | 
					    sats: Int!
 | 
				
			||||||
 | 
					    credits: Int!
 | 
				
			||||||
    commentSats: Int!
 | 
					    commentSats: Int!
 | 
				
			||||||
 | 
					    commentCredits: Int!
 | 
				
			||||||
    lastCommentAt: Date
 | 
					    lastCommentAt: Date
 | 
				
			||||||
    upvotes: Int!
 | 
					    upvotes: Int!
 | 
				
			||||||
    meSats: Int!
 | 
					    meSats: Int!
 | 
				
			||||||
 | 
					    meCredits: Int!
 | 
				
			||||||
    meDontLikeSats: Int!
 | 
					    meDontLikeSats: Int!
 | 
				
			||||||
    meBookmark: Boolean!
 | 
					    meBookmark: Boolean!
 | 
				
			||||||
    meSubscription: Boolean!
 | 
					    meSubscription: Boolean!
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ extend type Mutation {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum PaymentMethod {
 | 
					enum PaymentMethod {
 | 
				
			||||||
 | 
					  REWARD_SATS
 | 
				
			||||||
  FEE_CREDIT
 | 
					  FEE_CREDIT
 | 
				
			||||||
  ZERO_COST
 | 
					  ZERO_COST
 | 
				
			||||||
  OPTIMISTIC
 | 
					  OPTIMISTIC
 | 
				
			||||||
@ -52,4 +53,9 @@ type DonatePaidAction implements PaidAction {
 | 
				
			|||||||
  paymentMethod: PaymentMethod!
 | 
					  paymentMethod: PaymentMethod!
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BuyCreditsPaidAction implements PaidAction {
 | 
				
			||||||
 | 
					  result: BuyCreditsResult
 | 
				
			||||||
 | 
					  invoice: Invoice
 | 
				
			||||||
 | 
					  paymentMethod: PaymentMethod!
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
`
 | 
					`
 | 
				
			||||||
 | 
				
			|||||||
@ -114,6 +114,8 @@ export default gql`
 | 
				
			|||||||
    withdrawMaxFeeDefault: Int!
 | 
					    withdrawMaxFeeDefault: Int!
 | 
				
			||||||
    proxyReceive: Boolean
 | 
					    proxyReceive: Boolean
 | 
				
			||||||
    directReceive: Boolean
 | 
					    directReceive: Boolean
 | 
				
			||||||
 | 
					    receiveCreditsBelowSats: Int!
 | 
				
			||||||
 | 
					    sendCreditsBelowSats: Int!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  type AuthMethods {
 | 
					  type AuthMethods {
 | 
				
			||||||
@ -130,6 +132,7 @@ export default gql`
 | 
				
			|||||||
    extremely sensitive
 | 
					    extremely sensitive
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    sats: Int!
 | 
					    sats: Int!
 | 
				
			||||||
 | 
					    credits: Int!
 | 
				
			||||||
    authMethods: AuthMethods!
 | 
					    authMethods: AuthMethods!
 | 
				
			||||||
    lnAddr: String
 | 
					    lnAddr: String
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -194,6 +197,8 @@ export default gql`
 | 
				
			|||||||
    walletsUpdatedAt: Date
 | 
					    walletsUpdatedAt: Date
 | 
				
			||||||
    proxyReceive: Boolean
 | 
					    proxyReceive: Boolean
 | 
				
			||||||
    directReceive: Boolean
 | 
					    directReceive: Boolean
 | 
				
			||||||
 | 
					    receiveCreditsBelowSats: Int!
 | 
				
			||||||
 | 
					    sendCreditsBelowSats: Int!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  type UserOptional {
 | 
					  type UserOptional {
 | 
				
			||||||
 | 
				
			|||||||
@ -83,6 +83,11 @@ const typeDefs = `
 | 
				
			|||||||
    removeWallet(id: ID!): Boolean
 | 
					    removeWallet(id: ID!): Boolean
 | 
				
			||||||
    deleteWalletLogs(wallet: String): Boolean
 | 
					    deleteWalletLogs(wallet: String): Boolean
 | 
				
			||||||
    setWalletPriority(id: ID!, priority: Int!): Boolean
 | 
					    setWalletPriority(id: ID!, priority: Int!): Boolean
 | 
				
			||||||
 | 
					    buyCredits(credits: Int!): BuyCreditsPaidAction!
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  type BuyCreditsResult {
 | 
				
			||||||
 | 
					    credits: Int!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface InvoiceOrDirect {
 | 
					  interface InvoiceOrDirect {
 | 
				
			||||||
@ -126,6 +131,7 @@ const typeDefs = `
 | 
				
			|||||||
    actionState: String
 | 
					    actionState: String
 | 
				
			||||||
    actionType: String
 | 
					    actionType: String
 | 
				
			||||||
    actionError: String
 | 
					    actionError: String
 | 
				
			||||||
 | 
					    invoiceForward: Boolean
 | 
				
			||||||
    item: Item
 | 
					    item: Item
 | 
				
			||||||
    itemAct: ItemAct
 | 
					    itemAct: ItemAct
 | 
				
			||||||
    forwardedSats: Int
 | 
					    forwardedSats: Int
 | 
				
			||||||
 | 
				
			|||||||
@ -5,8 +5,6 @@ import { useMe } from '@/components/me'
 | 
				
			|||||||
import { useMutation } from '@apollo/client'
 | 
					import { useMutation } from '@apollo/client'
 | 
				
			||||||
import { WELCOME_BANNER_MUTATION } from '@/fragments/users'
 | 
					import { WELCOME_BANNER_MUTATION } from '@/fragments/users'
 | 
				
			||||||
import { useToast } from '@/components/toast'
 | 
					import { useToast } from '@/components/toast'
 | 
				
			||||||
import { BALANCE_LIMIT_MSATS } from '@/lib/constants'
 | 
					 | 
				
			||||||
import { msatsToSats, numWithUnits } from '@/lib/format'
 | 
					 | 
				
			||||||
import Link from 'next/link'
 | 
					import Link from 'next/link'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function WelcomeBanner ({ Banner }) {
 | 
					export function WelcomeBanner ({ Banner }) {
 | 
				
			||||||
@ -102,27 +100,6 @@ export function MadnessBanner ({ handleClose }) {
 | 
				
			|||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function WalletLimitBanner () {
 | 
					 | 
				
			||||||
  const { me } = useMe()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
 | 
					 | 
				
			||||||
  if (!me || !limitReached) return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <Alert className={styles.banner} key='info' variant='warning'>
 | 
					 | 
				
			||||||
      <Alert.Heading>
 | 
					 | 
				
			||||||
        Your wallet is over the current limit ({numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))})
 | 
					 | 
				
			||||||
      </Alert.Heading>
 | 
					 | 
				
			||||||
      <p className='mb-1'>
 | 
					 | 
				
			||||||
        Deposits to your wallet from <strong>outside</strong> of SN are blocked.
 | 
					 | 
				
			||||||
      </p>
 | 
					 | 
				
			||||||
      <p>
 | 
					 | 
				
			||||||
        Please spend or withdraw sats to restore full wallet functionality.
 | 
					 | 
				
			||||||
      </p>
 | 
					 | 
				
			||||||
    </Alert>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function WalletSecurityBanner ({ isActive }) {
 | 
					export function WalletSecurityBanner ({ isActive }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Alert className={styles.banner} key='info' variant='warning'>
 | 
					    <Alert className={styles.banner} key='info' variant='warning'>
 | 
				
			||||||
 | 
				
			|||||||
@ -114,7 +114,7 @@ export function FeeButtonProvider ({ baseLineItems = DEFAULT_BASE_LINE_ITEMS, us
 | 
				
			|||||||
    const lines = { ...baseLineItems, ...lineItems, ...remoteLineItems }
 | 
					    const lines = { ...baseLineItems, ...lineItems, ...remoteLineItems }
 | 
				
			||||||
    const total = Object.values(lines).sort(sortHelper).reduce((acc, { modifier }) => modifier(acc), 0)
 | 
					    const total = Object.values(lines).sort(sortHelper).reduce((acc, { modifier }) => modifier(acc), 0)
 | 
				
			||||||
    // freebies: there's only a base cost and we don't have enough sats
 | 
					    // freebies: there's only a base cost and we don't have enough sats
 | 
				
			||||||
    const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total && !me?.privates?.disableFreebies
 | 
					    const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total && me?.privates?.credits < total && !me?.privates?.disableFreebies
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      lines,
 | 
					      lines,
 | 
				
			||||||
      merge: mergeLineItems,
 | 
					      merge: mergeLineItems,
 | 
				
			||||||
 | 
				
			|||||||
@ -179,9 +179,11 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
 | 
				
			|||||||
      </Form>)
 | 
					      </Form>)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function modifyActCache (cache, { result, invoice }) {
 | 
					function modifyActCache (cache, { result, invoice }, me) {
 | 
				
			||||||
  if (!result) return
 | 
					  if (!result) return
 | 
				
			||||||
  const { id, sats, act } = result
 | 
					  const { id, sats, act } = result
 | 
				
			||||||
 | 
					  const p2p = invoice?.invoiceForward
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  cache.modify({
 | 
					  cache.modify({
 | 
				
			||||||
    id: `Item:${id}`,
 | 
					    id: `Item:${id}`,
 | 
				
			||||||
    fields: {
 | 
					    fields: {
 | 
				
			||||||
@ -191,12 +193,24 @@ function modifyActCache (cache, { result, invoice }) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return existingSats
 | 
					        return existingSats
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      credits (existingCredits = 0) {
 | 
				
			||||||
 | 
					        if (act === 'TIP' && !p2p) {
 | 
				
			||||||
 | 
					          return existingCredits + sats
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return existingCredits
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      meSats: (existingSats = 0) => {
 | 
					      meSats: (existingSats = 0) => {
 | 
				
			||||||
        if (act === 'TIP') {
 | 
					        if (act === 'TIP' && me) {
 | 
				
			||||||
          return existingSats + sats
 | 
					          return existingSats + sats
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return existingSats
 | 
					        return existingSats
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      meCredits: (existingCredits = 0) => {
 | 
				
			||||||
 | 
					        if (act === 'TIP' && !p2p && me) {
 | 
				
			||||||
 | 
					          return existingCredits + sats
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return existingCredits
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      meDontLikeSats: (existingSats = 0) => {
 | 
					      meDontLikeSats: (existingSats = 0) => {
 | 
				
			||||||
        if (act === 'DONT_LIKE_THIS') {
 | 
					        if (act === 'DONT_LIKE_THIS') {
 | 
				
			||||||
          return existingSats + sats
 | 
					          return existingSats + sats
 | 
				
			||||||
@ -219,6 +233,8 @@ function modifyActCache (cache, { result, invoice }) {
 | 
				
			|||||||
function updateAncestors (cache, { result, invoice }) {
 | 
					function updateAncestors (cache, { result, invoice }) {
 | 
				
			||||||
  if (!result) return
 | 
					  if (!result) return
 | 
				
			||||||
  const { id, sats, act, path } = result
 | 
					  const { id, sats, act, path } = result
 | 
				
			||||||
 | 
					  const p2p = invoice?.invoiceForward
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (act === 'TIP') {
 | 
					  if (act === 'TIP') {
 | 
				
			||||||
    // update all ancestors
 | 
					    // update all ancestors
 | 
				
			||||||
    path.split('.').forEach(aId => {
 | 
					    path.split('.').forEach(aId => {
 | 
				
			||||||
@ -226,6 +242,12 @@ function updateAncestors (cache, { result, invoice }) {
 | 
				
			|||||||
      cache.modify({
 | 
					      cache.modify({
 | 
				
			||||||
        id: `Item:${aId}`,
 | 
					        id: `Item:${aId}`,
 | 
				
			||||||
        fields: {
 | 
					        fields: {
 | 
				
			||||||
 | 
					          commentCredits (existingCommentCredits = 0) {
 | 
				
			||||||
 | 
					            if (p2p) {
 | 
				
			||||||
 | 
					              return existingCommentCredits
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return existingCommentCredits + sats
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          commentSats (existingCommentSats = 0) {
 | 
					          commentSats (existingCommentSats = 0) {
 | 
				
			||||||
            return existingCommentSats + sats
 | 
					            return existingCommentSats + sats
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@ -237,6 +259,7 @@ function updateAncestors (cache, { result, invoice }) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
 | 
					export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
 | 
				
			||||||
 | 
					  const { me } = useMe()
 | 
				
			||||||
  // because the mutation name we use varies,
 | 
					  // because the mutation name we use varies,
 | 
				
			||||||
  // we need to extract the result/invoice from the response
 | 
					  // we need to extract the result/invoice from the response
 | 
				
			||||||
  const getPaidActionResult = data => Object.values(data)[0]
 | 
					  const getPaidActionResult = data => Object.values(data)[0]
 | 
				
			||||||
@ -253,7 +276,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
 | 
				
			|||||||
    update: (cache, { data }) => {
 | 
					    update: (cache, { data }) => {
 | 
				
			||||||
      const response = getPaidActionResult(data)
 | 
					      const response = getPaidActionResult(data)
 | 
				
			||||||
      if (!response) return
 | 
					      if (!response) return
 | 
				
			||||||
      modifyActCache(cache, response)
 | 
					      modifyActCache(cache, response, me)
 | 
				
			||||||
      options?.update?.(cache, { data })
 | 
					      options?.update?.(cache, { data })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    onPayError: (e, cache, { data }) => {
 | 
					    onPayError: (e, cache, { data }) => {
 | 
				
			||||||
@ -261,7 +284,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
 | 
				
			|||||||
      if (!response || !response.result) return
 | 
					      if (!response || !response.result) return
 | 
				
			||||||
      const { result: { sats } } = response
 | 
					      const { result: { sats } } = response
 | 
				
			||||||
      const negate = { ...response, result: { ...response.result, sats: -1 * sats } }
 | 
					      const negate = { ...response, result: { ...response.result, sats: -1 * sats } }
 | 
				
			||||||
      modifyActCache(cache, negate)
 | 
					      modifyActCache(cache, negate, me)
 | 
				
			||||||
      options?.onPayError?.(e, cache, { data })
 | 
					      options?.onPayError?.(e, cache, { data })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    onPaid: (cache, { data }) => {
 | 
					    onPaid: (cache, { data }) => {
 | 
				
			||||||
@ -286,7 +309,7 @@ export function useZap () {
 | 
				
			|||||||
    // add current sats to next tip since idempotent zaps use desired total zap not difference
 | 
					    // add current sats to next tip since idempotent zaps use desired total zap not difference
 | 
				
			||||||
    const sats = nextTip(meSats, { ...me?.privates })
 | 
					    const sats = nextTip(meSats, { ...me?.privates })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const variables = { id: item.id, sats, act: 'TIP' }
 | 
					    const variables = { id: item.id, sats, act: 'TIP', hasSendWallet: wallets.length > 0 }
 | 
				
			||||||
    const optimisticResponse = { act: { __typename: 'ItemActPaidAction', result: { path: item.path, ...variables } } }
 | 
					    const optimisticResponse = { act: { __typename: 'ItemActPaidAction', result: { path: item.path, ...variables } } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,39 @@ import classNames from 'classnames'
 | 
				
			|||||||
import SubPopover from './sub-popover'
 | 
					import SubPopover from './sub-popover'
 | 
				
			||||||
import useCanEdit from './use-can-edit'
 | 
					import useCanEdit from './use-can-edit'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function itemTitle (item) {
 | 
				
			||||||
 | 
					  let title = ''
 | 
				
			||||||
 | 
					  title += numWithUnits(item.upvotes, {
 | 
				
			||||||
 | 
					    abbreviate: false,
 | 
				
			||||||
 | 
					    unitSingular: 'zapper',
 | 
				
			||||||
 | 
					    unitPlural: 'zappers'
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  if (item.sats) {
 | 
				
			||||||
 | 
					    title += ` \\ ${numWithUnits(item.sats - item.credits, { abbreviate: false })}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (item.credits) {
 | 
				
			||||||
 | 
					    title += ` \\ ${numWithUnits(item.credits, { abbreviate: false, unitSingular: 'CC', unitPlural: 'CCs' })}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (item.mine) {
 | 
				
			||||||
 | 
					    title += ` (${numWithUnits(item.meSats, { abbreviate: false })} to post)`
 | 
				
			||||||
 | 
					  } else if (item.meSats || item.meDontLikeSats || item.meAnonSats) {
 | 
				
			||||||
 | 
					    const satSources = []
 | 
				
			||||||
 | 
					    if (item.meAnonSats || (item.meSats || 0) - (item.meCredits || 0) > 0) {
 | 
				
			||||||
 | 
					      satSources.push(`${numWithUnits((item.meSats || 0) + (item.meAnonSats || 0) - (item.meCredits || 0), { abbreviate: false })}`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (item.meCredits) {
 | 
				
			||||||
 | 
					      satSources.push(`${numWithUnits(item.meCredits, { abbreviate: false, unitSingular: 'CC', unitPlural: 'CCs' })}`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (item.meDontLikeSats) {
 | 
				
			||||||
 | 
					      satSources.push(`${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (satSources.length) {
 | 
				
			||||||
 | 
					      title += ` (${satSources.join(' & ')} from me)`
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return title
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function ItemInfo ({
 | 
					export default function ItemInfo ({
 | 
				
			||||||
  item, full, commentsText = 'comments',
 | 
					  item, full, commentsText = 'comments',
 | 
				
			||||||
  commentTextSingular = 'comment', className, embellishUser, extraInfo, edit, toggleEdit, editText,
 | 
					  commentTextSingular = 'comment', className, embellishUser, extraInfo, edit, toggleEdit, editText,
 | 
				
			||||||
@ -62,16 +95,7 @@ export default function ItemInfo ({
 | 
				
			|||||||
    <div className={className || `${styles.other}`}>
 | 
					    <div className={className || `${styles.other}`}>
 | 
				
			||||||
      {!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) &&
 | 
					      {!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) &&
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
          <span title={`from ${numWithUnits(item.upvotes, {
 | 
					          <span title={itemTitle(item)}>
 | 
				
			||||||
            abbreviate: false,
 | 
					 | 
				
			||||||
            unitSingular: 'stacker',
 | 
					 | 
				
			||||||
            unitPlural: 'stackers'
 | 
					 | 
				
			||||||
          })} ${item.mine
 | 
					 | 
				
			||||||
            ? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
 | 
					 | 
				
			||||||
            : `(${numWithUnits(meSats, { abbreviate: false })}${item.meDontLikeSats
 | 
					 | 
				
			||||||
              ? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
 | 
					 | 
				
			||||||
              : ''} from me)`} `}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            {numWithUnits(item.sats)}
 | 
					            {numWithUnits(item.sats)}
 | 
				
			||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
          <span> \ </span>
 | 
					          <span> \ </span>
 | 
				
			||||||
@ -229,11 +253,21 @@ function InfoDropdownItem ({ item }) {
 | 
				
			|||||||
          <div>cost</div>
 | 
					          <div>cost</div>
 | 
				
			||||||
          <div>{item.cost}</div>
 | 
					          <div>{item.cost}</div>
 | 
				
			||||||
          <div>sats</div>
 | 
					          <div>sats</div>
 | 
				
			||||||
          <div>{item.sats}</div>
 | 
					          <div>{item.sats - item.credits}</div>
 | 
				
			||||||
 | 
					          <div>CCs</div>
 | 
				
			||||||
 | 
					          <div>{item.credits}</div>
 | 
				
			||||||
 | 
					          <div>comment sats</div>
 | 
				
			||||||
 | 
					          <div>{item.commentSats - item.commentCredits}</div>
 | 
				
			||||||
 | 
					          <div>comment CCs</div>
 | 
				
			||||||
 | 
					          <div>{item.commentCredits}</div>
 | 
				
			||||||
          {me && (
 | 
					          {me && (
 | 
				
			||||||
            <>
 | 
					            <>
 | 
				
			||||||
              <div>sats from me</div>
 | 
					              <div>sats from me</div>
 | 
				
			||||||
              <div>{item.meSats}</div>
 | 
					              <div>{item.meSats - item.meCredits}</div>
 | 
				
			||||||
 | 
					              <div>CCs from me</div>
 | 
				
			||||||
 | 
					              <div>{item.meCredits}</div>
 | 
				
			||||||
 | 
					              <div>downsats from me</div>
 | 
				
			||||||
 | 
					              <div>{item.meDontLikeSats}</div>
 | 
				
			||||||
            </>
 | 
					            </>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
          <div>zappers</div>
 | 
					          <div>zappers</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -6,12 +6,11 @@ import BackArrow from '../../svgs/arrow-left-line.svg'
 | 
				
			|||||||
import { useCallback, useEffect, useState } from 'react'
 | 
					import { useCallback, useEffect, useState } from 'react'
 | 
				
			||||||
import Price from '../price'
 | 
					import Price from '../price'
 | 
				
			||||||
import SubSelect from '../sub-select'
 | 
					import SubSelect from '../sub-select'
 | 
				
			||||||
import { USER_ID, BALANCE_LIMIT_MSATS } from '../../lib/constants'
 | 
					import { USER_ID } from '../../lib/constants'
 | 
				
			||||||
import Head from 'next/head'
 | 
					import Head from 'next/head'
 | 
				
			||||||
import NoteIcon from '../../svgs/notification-4-fill.svg'
 | 
					import NoteIcon from '../../svgs/notification-4-fill.svg'
 | 
				
			||||||
import { useMe } from '../me'
 | 
					import { useMe } from '../me'
 | 
				
			||||||
import HiddenWalletSummary from '../hidden-wallet-summary'
 | 
					import { abbrNum } from '../../lib/format'
 | 
				
			||||||
import { abbrNum, msatsToSats } from '../../lib/format'
 | 
					 | 
				
			||||||
import { useServiceWorker } from '../serviceworker'
 | 
					import { useServiceWorker } from '../serviceworker'
 | 
				
			||||||
import { signOut } from 'next-auth/react'
 | 
					import { signOut } from 'next-auth/react'
 | 
				
			||||||
import Badges from '../badge'
 | 
					import Badges from '../badge'
 | 
				
			||||||
@ -25,7 +24,7 @@ import { useHasNewNotes } from '../use-has-new-notes'
 | 
				
			|||||||
import { useWallets } from '@/wallets/index'
 | 
					import { useWallets } from '@/wallets/index'
 | 
				
			||||||
import SwitchAccountList, { useAccounts } from '@/components/account'
 | 
					import SwitchAccountList, { useAccounts } from '@/components/account'
 | 
				
			||||||
import { useShowModal } from '@/components/modal'
 | 
					import { useShowModal } from '@/components/modal'
 | 
				
			||||||
 | 
					import { numWithUnits } from '@/lib/format'
 | 
				
			||||||
export function Brand ({ className }) {
 | 
					export function Brand ({ className }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Link href='/' passHref legacyBehavior>
 | 
					    <Link href='/' passHref legacyBehavior>
 | 
				
			||||||
@ -140,21 +139,24 @@ export function NavNotifications ({ className }) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function WalletSummary () {
 | 
					export function WalletSummary () {
 | 
				
			||||||
  const { me } = useMe()
 | 
					  const { me } = useMe()
 | 
				
			||||||
  if (!me) return null
 | 
					  if (!me || me.privates?.sats === 0) return null
 | 
				
			||||||
  if (me.privates?.hideWalletBalance) {
 | 
					  return (
 | 
				
			||||||
    return <HiddenWalletSummary abbreviate fixedWidth />
 | 
					    <span
 | 
				
			||||||
  }
 | 
					      className='text-monospace'
 | 
				
			||||||
  return `${abbrNum(me.privates?.sats)}`
 | 
					      title={`${numWithUnits(me.privates?.credits, { abbreviate: false, unitSingular: 'CC', unitPlural: 'CCs' })}`}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {`${abbrNum(me.privates?.sats)}`}
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function NavWalletSummary ({ className }) {
 | 
					export function NavWalletSummary ({ className }) {
 | 
				
			||||||
  const { me } = useMe()
 | 
					  const { me } = useMe()
 | 
				
			||||||
  const walletLimitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Nav.Item className={className}>
 | 
					    <Nav.Item className={className}>
 | 
				
			||||||
      <Link href='/wallet' passHref legacyBehavior>
 | 
					      <Link href='/credits' passHref legacyBehavior>
 | 
				
			||||||
        <Nav.Link eventKey='wallet' className={`${walletLimitReached ? 'text-warning' : 'text-success'} text-monospace px-0 text-nowrap`}>
 | 
					        <Nav.Link eventKey='credits' className='text-success text-monospace px-0 text-nowrap'>
 | 
				
			||||||
          <WalletSummary me={me} />
 | 
					          <WalletSummary me={me} />
 | 
				
			||||||
        </Nav.Link>
 | 
					        </Nav.Link>
 | 
				
			||||||
      </Link>
 | 
					      </Link>
 | 
				
			||||||
@ -194,8 +196,11 @@ export function MeDropdown ({ me, dropNavKey }) {
 | 
				
			|||||||
          <Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
 | 
					          <Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
 | 
				
			||||||
            <Dropdown.Item active={me.name + '/bookmarks' === dropNavKey}>bookmarks</Dropdown.Item>
 | 
					            <Dropdown.Item active={me.name + '/bookmarks' === dropNavKey}>bookmarks</Dropdown.Item>
 | 
				
			||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
          <Link href='/wallet' passHref legacyBehavior>
 | 
					          <Link href='/wallets' passHref legacyBehavior>
 | 
				
			||||||
            <Dropdown.Item eventKey='wallet'>wallet</Dropdown.Item>
 | 
					            <Dropdown.Item eventKey='wallets'>wallets</Dropdown.Item>
 | 
				
			||||||
 | 
					          </Link>
 | 
				
			||||||
 | 
					          <Link href='/credits' passHref legacyBehavior>
 | 
				
			||||||
 | 
					            <Dropdown.Item eventKey='credits'>credits</Dropdown.Item>
 | 
				
			||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
          <Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref legacyBehavior>
 | 
					          <Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref legacyBehavior>
 | 
				
			||||||
            <Dropdown.Item eventKey='satistics'>satistics</Dropdown.Item>
 | 
					            <Dropdown.Item eventKey='satistics'>satistics</Dropdown.Item>
 | 
				
			||||||
 | 
				
			|||||||
@ -59,8 +59,11 @@ export default function OffCanvas ({ me, dropNavKey }) {
 | 
				
			|||||||
                  <Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
 | 
					                  <Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
 | 
				
			||||||
                    <Dropdown.Item active={me.name + '/bookmarks' === dropNavKey}>bookmarks</Dropdown.Item>
 | 
					                    <Dropdown.Item active={me.name + '/bookmarks' === dropNavKey}>bookmarks</Dropdown.Item>
 | 
				
			||||||
                  </Link>
 | 
					                  </Link>
 | 
				
			||||||
                  <Link href='/wallet' passHref legacyBehavior>
 | 
					                  <Link href='/wallets' passHref legacyBehavior>
 | 
				
			||||||
                    <Dropdown.Item eventKey='wallet'>wallet</Dropdown.Item>
 | 
					                    <Dropdown.Item eventKey='wallets'>wallets</Dropdown.Item>
 | 
				
			||||||
 | 
					                  </Link>
 | 
				
			||||||
 | 
					                  <Link href='/credits' passHref legacyBehavior>
 | 
				
			||||||
 | 
					                    <Dropdown.Item eventKey='credits'>credits</Dropdown.Item>
 | 
				
			||||||
                  </Link>
 | 
					                  </Link>
 | 
				
			||||||
                  <Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref legacyBehavior>
 | 
					                  <Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref legacyBehavior>
 | 
				
			||||||
                    <Dropdown.Item eventKey='satistics'>satistics</Dropdown.Item>
 | 
					                    <Dropdown.Item eventKey='satistics'>satistics</Dropdown.Item>
 | 
				
			||||||
 | 
				
			|||||||
@ -535,9 +535,27 @@ function Referral ({ n }) {
 | 
				
			|||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function stackedText (item) {
 | 
				
			||||||
 | 
					  let text = ''
 | 
				
			||||||
 | 
					  if (item.sats - item.credits > 0) {
 | 
				
			||||||
 | 
					    text += `${numWithUnits(item.sats - item.credits, { abbreviate: false })}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (item.credits > 0) {
 | 
				
			||||||
 | 
					      text += ' and '
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (item.credits > 0) {
 | 
				
			||||||
 | 
					    text += `${numWithUnits(item.credits, { abbreviate: false, unitSingular: 'CC', unitPlural: 'CCs' })}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return text
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function Votification ({ n }) {
 | 
					function Votification ({ n }) {
 | 
				
			||||||
  let forwardedSats = 0
 | 
					  let forwardedSats = 0
 | 
				
			||||||
  let ForwardedUsers = null
 | 
					  let ForwardedUsers = null
 | 
				
			||||||
 | 
					  let stackedTextString
 | 
				
			||||||
 | 
					  let forwardedTextString
 | 
				
			||||||
  if (n.item.forwards?.length) {
 | 
					  if (n.item.forwards?.length) {
 | 
				
			||||||
    forwardedSats = Math.floor(n.earnedSats * n.item.forwards.map(fwd => fwd.pct).reduce((sum, cur) => sum + cur) / 100)
 | 
					    forwardedSats = Math.floor(n.earnedSats * n.item.forwards.map(fwd => fwd.pct).reduce((sum, cur) => sum + cur) / 100)
 | 
				
			||||||
    ForwardedUsers = () => n.item.forwards.map((fwd, i) =>
 | 
					    ForwardedUsers = () => n.item.forwards.map((fwd, i) =>
 | 
				
			||||||
@ -547,14 +565,18 @@ function Votification ({ n }) {
 | 
				
			|||||||
        </Link>
 | 
					        </Link>
 | 
				
			||||||
        {i !== n.item.forwards.length - 1 && ' '}
 | 
					        {i !== n.item.forwards.length - 1 && ' '}
 | 
				
			||||||
      </span>)
 | 
					      </span>)
 | 
				
			||||||
 | 
					    stackedTextString = numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'CC', unitPlural: 'CCs' })
 | 
				
			||||||
 | 
					    forwardedTextString = numWithUnits(forwardedSats, { abbreviate: false, unitSingular: 'CC', unitPlural: 'CCs' })
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    stackedTextString = stackedText(n.item)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <NoteHeader color='success'>
 | 
					      <NoteHeader color='success'>
 | 
				
			||||||
        your {n.item.title ? 'post' : 'reply'} stacked {numWithUnits(n.earnedSats, { abbreviate: false })}
 | 
					        your {n.item.title ? 'post' : 'reply'} stacked {stackedTextString}
 | 
				
			||||||
        {n.item.forwards?.length > 0 &&
 | 
					        {n.item.forwards?.length > 0 &&
 | 
				
			||||||
          <>
 | 
					          <>
 | 
				
			||||||
            {' '}and forwarded {numWithUnits(forwardedSats, { abbreviate: false })} to{' '}
 | 
					            {' '}and forwarded {forwardedTextString} to{' '}
 | 
				
			||||||
            <ForwardedUsers />
 | 
					            <ForwardedUsers />
 | 
				
			||||||
          </>}
 | 
					          </>}
 | 
				
			||||||
      </NoteHeader>
 | 
					      </NoteHeader>
 | 
				
			||||||
@ -567,7 +589,7 @@ function ForwardedVotification ({ n }) {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <NoteHeader color='success'>
 | 
					      <NoteHeader color='success'>
 | 
				
			||||||
        you were forwarded {numWithUnits(n.earnedSats, { abbreviate: false })} from
 | 
					        you were forwarded {numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'CC', unitPlural: 'CCs' })} from
 | 
				
			||||||
      </NoteHeader>
 | 
					      </NoteHeader>
 | 
				
			||||||
      <NoteItem item={n.item} />
 | 
					      <NoteItem item={n.item} />
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
 | 
				
			|||||||
@ -44,7 +44,7 @@ export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnte
 | 
				
			|||||||
            : <Card.Title className={styles.walletLogo}>{wallet.def.card.title}</Card.Title>}
 | 
					            : <Card.Title className={styles.walletLogo}>{wallet.def.card.title}</Card.Title>}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </Card.Body>
 | 
					      </Card.Body>
 | 
				
			||||||
      <Link href={`/settings/wallets/${wallet.def.name}`}>
 | 
					      <Link href={`/wallets/${wallet.def.name}`}>
 | 
				
			||||||
        <Card.Footer className={styles.attach}>
 | 
					        <Card.Footer className={styles.attach}>
 | 
				
			||||||
          {isConfigured(wallet)
 | 
					          {isConfigured(wallet)
 | 
				
			||||||
            ? <>configure<Gear width={14} height={14} /></>
 | 
					            ? <>configure<Gear width={14} height={14} /></>
 | 
				
			||||||
 | 
				
			|||||||
@ -281,7 +281,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    // only fetch new logs if we are on a page that uses logs
 | 
					    // only fetch new logs if we are on a page that uses logs
 | 
				
			||||||
    const needLogs = router.asPath.startsWith('/settings/wallets') || router.asPath.startsWith('/wallet/logs')
 | 
					    const needLogs = router.asPath.startsWith('/wallets')
 | 
				
			||||||
    if (!me || !needLogs) return
 | 
					    if (!me || !needLogs) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let timeout
 | 
					    let timeout
 | 
				
			||||||
 | 
				
			|||||||
@ -27,11 +27,13 @@ export const COMMENT_FIELDS = gql`
 | 
				
			|||||||
      ...StreakFields
 | 
					      ...StreakFields
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    sats
 | 
					    sats
 | 
				
			||||||
 | 
					    credits
 | 
				
			||||||
    meAnonSats @client
 | 
					    meAnonSats @client
 | 
				
			||||||
    upvotes
 | 
					    upvotes
 | 
				
			||||||
    freedFreebie
 | 
					    freedFreebie
 | 
				
			||||||
    boost
 | 
					    boost
 | 
				
			||||||
    meSats
 | 
					    meSats
 | 
				
			||||||
 | 
					    meCredits
 | 
				
			||||||
    meDontLikeSats
 | 
					    meDontLikeSats
 | 
				
			||||||
    meBookmark
 | 
					    meBookmark
 | 
				
			||||||
    meSubscription
 | 
					    meSubscription
 | 
				
			||||||
@ -39,6 +41,7 @@ export const COMMENT_FIELDS = gql`
 | 
				
			|||||||
    freebie
 | 
					    freebie
 | 
				
			||||||
    path
 | 
					    path
 | 
				
			||||||
    commentSats
 | 
					    commentSats
 | 
				
			||||||
 | 
					    commentCredits
 | 
				
			||||||
    mine
 | 
					    mine
 | 
				
			||||||
    otsHash
 | 
					    otsHash
 | 
				
			||||||
    ncomments
 | 
					    ncomments
 | 
				
			||||||
 | 
				
			|||||||
@ -38,6 +38,7 @@ export const ITEM_FIELDS = gql`
 | 
				
			|||||||
    otsHash
 | 
					    otsHash
 | 
				
			||||||
    position
 | 
					    position
 | 
				
			||||||
    sats
 | 
					    sats
 | 
				
			||||||
 | 
					    credits
 | 
				
			||||||
    meAnonSats @client
 | 
					    meAnonSats @client
 | 
				
			||||||
    boost
 | 
					    boost
 | 
				
			||||||
    bounty
 | 
					    bounty
 | 
				
			||||||
@ -46,6 +47,7 @@ export const ITEM_FIELDS = gql`
 | 
				
			|||||||
    path
 | 
					    path
 | 
				
			||||||
    upvotes
 | 
					    upvotes
 | 
				
			||||||
    meSats
 | 
					    meSats
 | 
				
			||||||
 | 
					    meCredits
 | 
				
			||||||
    meDontLikeSats
 | 
					    meDontLikeSats
 | 
				
			||||||
    meBookmark
 | 
					    meBookmark
 | 
				
			||||||
    meSubscription
 | 
					    meSubscription
 | 
				
			||||||
@ -55,6 +57,7 @@ export const ITEM_FIELDS = gql`
 | 
				
			|||||||
    bio
 | 
					    bio
 | 
				
			||||||
    ncomments
 | 
					    ncomments
 | 
				
			||||||
    commentSats
 | 
					    commentSats
 | 
				
			||||||
 | 
					    commentCredits
 | 
				
			||||||
    lastCommentAt
 | 
					    lastCommentAt
 | 
				
			||||||
    isJob
 | 
					    isJob
 | 
				
			||||||
    status
 | 
					    status
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ export const PAID_ACTION = gql`
 | 
				
			|||||||
  fragment PaidActionFields on PaidAction {
 | 
					  fragment PaidActionFields on PaidAction {
 | 
				
			||||||
    invoice {
 | 
					    invoice {
 | 
				
			||||||
      ...InvoiceFields
 | 
					      ...InvoiceFields
 | 
				
			||||||
 | 
					      invoiceForward
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    paymentMethod
 | 
					    paymentMethod
 | 
				
			||||||
  }`
 | 
					  }`
 | 
				
			||||||
@ -117,6 +118,17 @@ export const DONATE = gql`
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }`
 | 
					  }`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const BUY_CREDITS = gql`
 | 
				
			||||||
 | 
					  ${PAID_ACTION}
 | 
				
			||||||
 | 
					  mutation buyCredits($credits: Int!) {
 | 
				
			||||||
 | 
					    buyCredits(credits: $credits) {
 | 
				
			||||||
 | 
					      result {
 | 
				
			||||||
 | 
					        credits
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      ...PaidActionFields
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACT_MUTATION = gql`
 | 
					export const ACT_MUTATION = gql`
 | 
				
			||||||
  ${PAID_ACTION}
 | 
					  ${PAID_ACTION}
 | 
				
			||||||
  ${ITEM_ACT_PAID_ACTION_FIELDS}
 | 
					  ${ITEM_ACT_PAID_ACTION_FIELDS}
 | 
				
			||||||
 | 
				
			|||||||
@ -39,6 +39,7 @@ ${STREAK_FIELDS}
 | 
				
			|||||||
      nostrCrossposting
 | 
					      nostrCrossposting
 | 
				
			||||||
      nsfwMode
 | 
					      nsfwMode
 | 
				
			||||||
      sats
 | 
					      sats
 | 
				
			||||||
 | 
					      credits
 | 
				
			||||||
      tipDefault
 | 
					      tipDefault
 | 
				
			||||||
      tipRandom
 | 
					      tipRandom
 | 
				
			||||||
      tipRandomMin
 | 
					      tipRandomMin
 | 
				
			||||||
@ -116,6 +117,8 @@ export const SETTINGS_FIELDS = gql`
 | 
				
			|||||||
      apiKeyEnabled
 | 
					      apiKeyEnabled
 | 
				
			||||||
      proxyReceive
 | 
					      proxyReceive
 | 
				
			||||||
      directReceive
 | 
					      directReceive
 | 
				
			||||||
 | 
					      receiveCreditsBelowSats
 | 
				
			||||||
 | 
					      sendCreditsBelowSats
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }`
 | 
					  }`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,8 @@ export const PAID_ACTION_PAYMENT_METHODS = {
 | 
				
			|||||||
  PESSIMISTIC: 'PESSIMISTIC',
 | 
					  PESSIMISTIC: 'PESSIMISTIC',
 | 
				
			||||||
  OPTIMISTIC: 'OPTIMISTIC',
 | 
					  OPTIMISTIC: 'OPTIMISTIC',
 | 
				
			||||||
  DIRECT: 'DIRECT',
 | 
					  DIRECT: 'DIRECT',
 | 
				
			||||||
  P2P: 'P2P'
 | 
					  P2P: 'P2P',
 | 
				
			||||||
 | 
					  REWARD_SATS: 'REWARD_SATS'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
 | 
					export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
 | 
				
			||||||
export const NOFOLLOW_LIMIT = 1000
 | 
					export const NOFOLLOW_LIMIT = 1000
 | 
				
			||||||
@ -50,7 +51,6 @@ export const ITEM_EDIT_SECONDS = 600
 | 
				
			|||||||
export const ITEM_SPAM_INTERVAL = '10m'
 | 
					export const ITEM_SPAM_INTERVAL = '10m'
 | 
				
			||||||
export const ANON_ITEM_SPAM_INTERVAL = '0'
 | 
					export const ANON_ITEM_SPAM_INTERVAL = '0'
 | 
				
			||||||
export const INV_PENDING_LIMIT = 100
 | 
					export const INV_PENDING_LIMIT = 100
 | 
				
			||||||
export const BALANCE_LIMIT_MSATS = 100000000 // 100k sat
 | 
					 | 
				
			||||||
export const USER_ID = {
 | 
					export const USER_ID = {
 | 
				
			||||||
  k00b: 616,
 | 
					  k00b: 616,
 | 
				
			||||||
  ek: 6030,
 | 
					  ek: 6030,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,11 @@ import { string, ValidationError, number, object, array, boolean, date } from '.
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES,
 | 
					  BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES,
 | 
				
			||||||
  MIN_POLL_NUM_CHOICES, MAX_FORWARDS, BOOST_MULT, MAX_TERRITORY_DESC_LENGTH, POST_TYPES,
 | 
					  MIN_POLL_NUM_CHOICES, MAX_FORWARDS, BOOST_MULT, MAX_TERRITORY_DESC_LENGTH, POST_TYPES,
 | 
				
			||||||
  TERRITORY_BILLING_TYPES, MAX_COMMENT_TEXT_LENGTH, MAX_POST_TEXT_LENGTH, MIN_TITLE_LENGTH, BOUNTY_MIN, BOUNTY_MAX, BALANCE_LIMIT_MSATS
 | 
					  TERRITORY_BILLING_TYPES, MAX_COMMENT_TEXT_LENGTH, MAX_POST_TEXT_LENGTH, MIN_TITLE_LENGTH, BOUNTY_MIN, BOUNTY_MAX
 | 
				
			||||||
} from './constants'
 | 
					} from './constants'
 | 
				
			||||||
import { SUPPORTED_CURRENCIES } from './currency'
 | 
					import { SUPPORTED_CURRENCIES } from './currency'
 | 
				
			||||||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
 | 
					import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
 | 
				
			||||||
import { msatsToSats, numWithUnits, abbrNum } from './format'
 | 
					import { numWithUnits } from './format'
 | 
				
			||||||
import { SUB } from '@/fragments/subs'
 | 
					import { SUB } from '@/fragments/subs'
 | 
				
			||||||
import { NAME_QUERY } from '@/fragments/users'
 | 
					import { NAME_QUERY } from '@/fragments/users'
 | 
				
			||||||
import { datePivot } from './time'
 | 
					import { datePivot } from './time'
 | 
				
			||||||
@ -185,7 +185,7 @@ export function advSchema (args) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const autowithdrawSchemaMembers = object({
 | 
					export const autowithdrawSchemaMembers = object({
 | 
				
			||||||
  autoWithdrawThreshold: intValidator.required('required').min(0, 'must be at least 0').max(msatsToSats(BALANCE_LIMIT_MSATS), `must be at most ${abbrNum(msatsToSats(BALANCE_LIMIT_MSATS))}`).transform(Number),
 | 
					  autoWithdrawThreshold: intValidator.required('required').min(0, 'must be at least 0').max(10000, 'must be at most 10000').transform(Number),
 | 
				
			||||||
  autoWithdrawMaxFeePercent: floatValidator.required('required').min(0, 'must be at least 0').max(50, 'must not exceed 50').transform(Number),
 | 
					  autoWithdrawMaxFeePercent: floatValidator.required('required').min(0, 'must be at least 0').max(50, 'must not exceed 50').transform(Number),
 | 
				
			||||||
  autoWithdrawMaxFeeTotal: intValidator.required('required').min(0, 'must be at least 0').max(1_000, 'must not exceed 1000').transform(Number)
 | 
					  autoWithdrawMaxFeeTotal: intValidator.required('required').min(0, 'must be at least 0').max(1_000, 'must not exceed 1000').transform(Number)
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										108
									
								
								pages/credits.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								pages/credits.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
				
			||||||
 | 
					import { Form, Input, SubmitButton } from '@/components/form'
 | 
				
			||||||
 | 
					import { CenterLayout } from '@/components/layout'
 | 
				
			||||||
 | 
					import { useLightning } from '@/components/lightning'
 | 
				
			||||||
 | 
					import { useMe } from '@/components/me'
 | 
				
			||||||
 | 
					import { useShowModal } from '@/components/modal'
 | 
				
			||||||
 | 
					import { usePaidMutation } from '@/components/use-paid-mutation'
 | 
				
			||||||
 | 
					import { BUY_CREDITS } from '@/fragments/paidAction'
 | 
				
			||||||
 | 
					import { amountSchema } from '@/lib/validate'
 | 
				
			||||||
 | 
					import { Button, Col, InputGroup, Row } from 'react-bootstrap'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Credits () {
 | 
				
			||||||
 | 
					  const { me } = useMe()
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <CenterLayout footer footerLinks>
 | 
				
			||||||
 | 
					      <Row className='w-100 d-none d-sm-flex justify-content-center'>
 | 
				
			||||||
 | 
					        <Col>
 | 
				
			||||||
 | 
					          <h2 className='text-end me-1 me-md-3'>
 | 
				
			||||||
 | 
					            <div className='text-monospace'>
 | 
				
			||||||
 | 
					              {me?.privates?.credits}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className='text-muted'>cowboy credits</div>
 | 
				
			||||||
 | 
					            <BuyCreditsButton />
 | 
				
			||||||
 | 
					          </h2>
 | 
				
			||||||
 | 
					        </Col>
 | 
				
			||||||
 | 
					        <Col>
 | 
				
			||||||
 | 
					          <h2 className='text-start ms-1 ms-md-3'>
 | 
				
			||||||
 | 
					            <div className='text-monospace'>
 | 
				
			||||||
 | 
					              {me?.privates?.sats - me?.privates?.credits}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className='text-muted'>sats</div>
 | 
				
			||||||
 | 
					            <Button variant='success mt-3' href='/withdraw'>withdraw sats</Button>
 | 
				
			||||||
 | 
					          </h2>
 | 
				
			||||||
 | 
					        </Col>
 | 
				
			||||||
 | 
					      </Row>
 | 
				
			||||||
 | 
					      <Row className='w-100 d-flex d-sm-none justify-content-center my-5'>
 | 
				
			||||||
 | 
					        <Row className='mb-5'>
 | 
				
			||||||
 | 
					          <h2 className='text-start'>
 | 
				
			||||||
 | 
					            <div className='text-monospace'>
 | 
				
			||||||
 | 
					              {me?.privates?.credits}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className='text-muted'>cowboy credits</div>
 | 
				
			||||||
 | 
					            <BuyCreditsButton />
 | 
				
			||||||
 | 
					          </h2>
 | 
				
			||||||
 | 
					        </Row>
 | 
				
			||||||
 | 
					        <Row>
 | 
				
			||||||
 | 
					          <h2 className='text-end'>
 | 
				
			||||||
 | 
					            <div className='text-monospace'>
 | 
				
			||||||
 | 
					              {me?.privates?.sats - me?.privates?.credits}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className='text-muted'>sats</div>
 | 
				
			||||||
 | 
					            <Button variant='success mt-3' href='/withdraw'>withdraw sats</Button>
 | 
				
			||||||
 | 
					          </h2>
 | 
				
			||||||
 | 
					        </Row>
 | 
				
			||||||
 | 
					      </Row>
 | 
				
			||||||
 | 
					    </CenterLayout>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function BuyCreditsButton () {
 | 
				
			||||||
 | 
					  const showModal = useShowModal()
 | 
				
			||||||
 | 
					  const strike = useLightning()
 | 
				
			||||||
 | 
					  const [buyCredits] = usePaidMutation(BUY_CREDITS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <Button
 | 
				
			||||||
 | 
					        onClick={() => showModal(onClose => (
 | 
				
			||||||
 | 
					          <Form
 | 
				
			||||||
 | 
					            initial={{
 | 
				
			||||||
 | 
					              amount: 10000
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            schema={amountSchema}
 | 
				
			||||||
 | 
					            onSubmit={async ({ amount }) => {
 | 
				
			||||||
 | 
					              const { error } = await buyCredits({
 | 
				
			||||||
 | 
					                variables: {
 | 
				
			||||||
 | 
					                  credits: Number(amount)
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                onCompleted: () => {
 | 
				
			||||||
 | 
					                  strike()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					              onClose()
 | 
				
			||||||
 | 
					              if (error) throw error
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Input
 | 
				
			||||||
 | 
					              label='amount'
 | 
				
			||||||
 | 
					              name='amount'
 | 
				
			||||||
 | 
					              type='number'
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					              autoFocus
 | 
				
			||||||
 | 
					              append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <div className='d-flex'>
 | 
				
			||||||
 | 
					              <SubmitButton variant='secondary' className='ms-auto mt-1 px-4'>buy</SubmitButton>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </Form>
 | 
				
			||||||
 | 
					        ))}
 | 
				
			||||||
 | 
					        className='mt-3'
 | 
				
			||||||
 | 
					        variant='secondary'
 | 
				
			||||||
 | 
					      >buy credits
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -64,13 +64,13 @@ function InviteHeader ({ invite }) {
 | 
				
			|||||||
  if (invite.revoked) {
 | 
					  if (invite.revoked) {
 | 
				
			||||||
    Inner = () => <div className='text-danger'>this invite link expired</div>
 | 
					    Inner = () => <div className='text-danger'>this invite link expired</div>
 | 
				
			||||||
  } else if ((invite.limit && invite.limit <= invite.invitees.length) || invite.poor) {
 | 
					  } else if ((invite.limit && invite.limit <= invite.invitees.length) || invite.poor) {
 | 
				
			||||||
    Inner = () => <div className='text-danger'>this invite link has no more sats</div>
 | 
					    Inner = () => <div className='text-danger'>this invite link has no more cowboy credits</div>
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    Inner = () => (
 | 
					    Inner = () => (
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        Get <span className='text-success'>{invite.gift} free sats</span> from{' '}
 | 
					        Get <span className='text-success'>{invite.gift} cowboy credits</span> from{' '}
 | 
				
			||||||
        <Link href={`/${invite.user.name}`}>@{invite.user.name}</Link>{' '}
 | 
					        <Link href={`/${invite.user.name}`}>@{invite.user.name}</Link>{' '}
 | 
				
			||||||
        when you sign up today
 | 
					        when you sign up
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -264,7 +264,7 @@ export default function Satistics ({ ssrData }) {
 | 
				
			|||||||
          <div className={styles.rows}>
 | 
					          <div className={styles.rows}>
 | 
				
			||||||
            <div className={[styles.type, styles.head].join(' ')}>type</div>
 | 
					            <div className={[styles.type, styles.head].join(' ')}>type</div>
 | 
				
			||||||
            <div className={[styles.detail, styles.head].join(' ')}>detail</div>
 | 
					            <div className={[styles.detail, styles.head].join(' ')}>detail</div>
 | 
				
			||||||
            <div className={[styles.sats, styles.head].join(' ')}>sats</div>
 | 
					            <div className={[styles.sats, styles.head].join(' ')}>sats/credits</div>
 | 
				
			||||||
            {facts.map(f => <Fact key={f.type + f.id} fact={f} />)}
 | 
					            {facts.map(f => <Fact key={f.type + f.id} fact={f} />)}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -160,12 +160,15 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
            hideIsContributor: settings?.hideIsContributor,
 | 
					            hideIsContributor: settings?.hideIsContributor,
 | 
				
			||||||
            noReferralLinks: settings?.noReferralLinks,
 | 
					            noReferralLinks: settings?.noReferralLinks,
 | 
				
			||||||
            proxyReceive: settings?.proxyReceive,
 | 
					            proxyReceive: settings?.proxyReceive,
 | 
				
			||||||
            directReceive: settings?.directReceive
 | 
					            directReceive: settings?.directReceive,
 | 
				
			||||||
 | 
					            receiveCreditsBelowSats: settings?.receiveCreditsBelowSats,
 | 
				
			||||||
 | 
					            sendCreditsBelowSats: settings?.sendCreditsBelowSats
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          schema={settingsSchema}
 | 
					          schema={settingsSchema}
 | 
				
			||||||
          onSubmit={async ({
 | 
					          onSubmit={async ({
 | 
				
			||||||
            tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault,
 | 
					            tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault,
 | 
				
			||||||
            zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, satsFilter,
 | 
					            zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, satsFilter,
 | 
				
			||||||
 | 
					            receiveCreditsBelowSats, sendCreditsBelowSats,
 | 
				
			||||||
            ...values
 | 
					            ...values
 | 
				
			||||||
          }) => {
 | 
					          }) => {
 | 
				
			||||||
            if (nostrPubkey.length === 0) {
 | 
					            if (nostrPubkey.length === 0) {
 | 
				
			||||||
@ -191,6 +194,8 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
                    withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
 | 
					                    withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
 | 
				
			||||||
                    satsFilter: Number(satsFilter),
 | 
					                    satsFilter: Number(satsFilter),
 | 
				
			||||||
                    zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
 | 
					                    zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
 | 
				
			||||||
 | 
					                    receiveCreditsBelowSats: Number(receiveCreditsBelowSats),
 | 
				
			||||||
 | 
					                    sendCreditsBelowSats: Number(sendCreditsBelowSats),
 | 
				
			||||||
                    nostrPubkey,
 | 
					                    nostrPubkey,
 | 
				
			||||||
                    nostrRelays: nostrRelaysFiltered,
 | 
					                    nostrRelays: nostrRelaysFiltered,
 | 
				
			||||||
                    ...values
 | 
					                    ...values
 | 
				
			||||||
@ -335,6 +340,18 @@ export default function Settings ({ ssrData }) {
 | 
				
			|||||||
            name='noteCowboyHat'
 | 
					            name='noteCowboyHat'
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <div className='form-label'>wallet</div>
 | 
					          <div className='form-label'>wallet</div>
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            label='receive credits for zaps and deposits below'
 | 
				
			||||||
 | 
					            name='receiveCreditsBelowSats'
 | 
				
			||||||
 | 
					            required
 | 
				
			||||||
 | 
					            append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            label='send credits for zaps below'
 | 
				
			||||||
 | 
					            name='sendCreditsBelowSats'
 | 
				
			||||||
 | 
					            required
 | 
				
			||||||
 | 
					            append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
          <Checkbox
 | 
					          <Checkbox
 | 
				
			||||||
            label={
 | 
					            label={
 | 
				
			||||||
              <div className='d-flex align-items-center'>proxy deposits to attached wallets
 | 
					              <div className='d-flex align-items-center'>proxy deposits to attached wallets
 | 
				
			||||||
 | 
				
			|||||||
@ -92,7 +92,7 @@ export default function WalletSettings () {
 | 
				
			|||||||
            await save(values, values.enabled)
 | 
					            await save(values, values.enabled)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            toaster.success('saved settings')
 | 
					            toaster.success('saved settings')
 | 
				
			||||||
            router.push('/settings/wallets')
 | 
					            router.push('/wallets')
 | 
				
			||||||
          } catch (err) {
 | 
					          } catch (err) {
 | 
				
			||||||
            console.error(err)
 | 
					            console.error(err)
 | 
				
			||||||
            toaster.danger(err.message || err.toString?.())
 | 
					            toaster.danger(err.message || err.toString?.())
 | 
				
			||||||
@ -115,7 +115,7 @@ export default function WalletSettings () {
 | 
				
			|||||||
            try {
 | 
					            try {
 | 
				
			||||||
              await detach()
 | 
					              await detach()
 | 
				
			||||||
              toaster.success('saved settings')
 | 
					              toaster.success('saved settings')
 | 
				
			||||||
              router.push('/settings/wallets')
 | 
					              router.push('/wallets')
 | 
				
			||||||
            } catch (err) {
 | 
					            } catch (err) {
 | 
				
			||||||
              console.error(err)
 | 
					              console.error(err)
 | 
				
			||||||
              const message = 'failed to detach: ' + err.message || err.toString?.()
 | 
					              const message = 'failed to detach: ' + err.message || err.toString?.()
 | 
				
			||||||
@ -86,10 +86,10 @@ export default function Wallet ({ ssrData }) {
 | 
				
			|||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Layout>
 | 
					    <Layout>
 | 
				
			||||||
      <div className='py-5 w-100'>
 | 
					      <div className='py-5 w-100'>
 | 
				
			||||||
        <h2 className='mb-2 text-center'>attach wallets</h2>
 | 
					        <h2 className='mb-2 text-center'>wallets</h2>
 | 
				
			||||||
        <h6 className='text-muted text-center'>attach wallets to supplement your SN wallet</h6>
 | 
					        <h6 className='text-muted text-center'>use real bitcoin</h6>
 | 
				
			||||||
        <div className='text-center'>
 | 
					        <div className='text-center'>
 | 
				
			||||||
          <Link href='/wallet/logs' className='text-muted fw-bold text-underline'>
 | 
					          <Link href='/wallets/logs' className='text-muted fw-bold text-underline'>
 | 
				
			||||||
            wallet logs
 | 
					            wallet logs
 | 
				
			||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
@ -1,198 +1,68 @@
 | 
				
			|||||||
import { useRouter } from 'next/router'
 | 
					 | 
				
			||||||
import { Checkbox, Form, Input, InputUserSuggest, SubmitButton } from '@/components/form'
 | 
					 | 
				
			||||||
import Link from 'next/link'
 | 
					 | 
				
			||||||
import Button from 'react-bootstrap/Button'
 | 
					 | 
				
			||||||
import { gql, useMutation, useQuery } from '@apollo/client'
 | 
					 | 
				
			||||||
import Qr, { QrSkeleton } from '@/components/qr'
 | 
					 | 
				
			||||||
import { CenterLayout } from '@/components/layout'
 | 
					 | 
				
			||||||
import InputGroup from 'react-bootstrap/InputGroup'
 | 
					 | 
				
			||||||
import { WithdrawlSkeleton } from '@/pages/withdrawals/[id]'
 | 
					 | 
				
			||||||
import { useMe } from '@/components/me'
 | 
					 | 
				
			||||||
import { useEffect, useState } from 'react'
 | 
					 | 
				
			||||||
import { requestProvider } from 'webln'
 | 
					 | 
				
			||||||
import Alert from 'react-bootstrap/Alert'
 | 
					 | 
				
			||||||
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '@/fragments/wallet'
 | 
					 | 
				
			||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
					import { getGetServerSideProps } from '@/api/ssrApollo'
 | 
				
			||||||
import { amountSchema, lnAddrSchema, withdrawlSchema } from '@/lib/validate'
 | 
					import { CenterLayout } from '@/components/layout'
 | 
				
			||||||
import Nav from 'react-bootstrap/Nav'
 | 
					import Link from 'next/link'
 | 
				
			||||||
import { BALANCE_LIMIT_MSATS, FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
 | 
					import { useRouter } from 'next/router'
 | 
				
			||||||
import { msatsToSats, numWithUnits } from '@/lib/format'
 | 
					import { InputGroup, Nav } from 'react-bootstrap'
 | 
				
			||||||
import styles from '@/components/user-header.module.css'
 | 
					import styles from '@/components/user-header.module.css'
 | 
				
			||||||
import HiddenWalletSummary from '@/components/hidden-wallet-summary'
 | 
					import { gql, useMutation, useQuery } from '@apollo/client'
 | 
				
			||||||
import AccordianItem from '@/components/accordian-item'
 | 
					import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '@/fragments/wallet'
 | 
				
			||||||
import { lnAddrOptions } from '@/lib/lnurl'
 | 
					import { requestProvider } from 'webln'
 | 
				
			||||||
import useDebounceCallback from '@/components/use-debounce-callback'
 | 
					import { useEffect, useState } from 'react'
 | 
				
			||||||
import { Scanner } from '@yudiel/react-qr-scanner'
 | 
					import { useMe } from '@/components/me'
 | 
				
			||||||
import CameraIcon from '@/svgs/camera-line.svg'
 | 
					import { WithdrawlSkeleton } from './withdrawals/[id]'
 | 
				
			||||||
 | 
					import { Checkbox, Form, Input, InputUserSuggest, SubmitButton } from '@/components/form'
 | 
				
			||||||
 | 
					import { lnAddrSchema, withdrawlSchema } from '@/lib/validate'
 | 
				
			||||||
import { useShowModal } from '@/components/modal'
 | 
					import { useShowModal } from '@/components/modal'
 | 
				
			||||||
import { useField } from 'formik'
 | 
					import { useField } from 'formik'
 | 
				
			||||||
import { useToast } from '@/components/toast'
 | 
					import { useToast } from '@/components/toast'
 | 
				
			||||||
import { WalletLimitBanner } from '@/components/banners'
 | 
					import { Scanner } from '@yudiel/react-qr-scanner'
 | 
				
			||||||
import Plug from '@/svgs/plug.svg'
 | 
					 | 
				
			||||||
import { decode } from 'bolt11'
 | 
					import { decode } from 'bolt11'
 | 
				
			||||||
 | 
					import CameraIcon from '@/svgs/camera-line.svg'
 | 
				
			||||||
 | 
					import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
 | 
				
			||||||
 | 
					import Qr, { QrSkeleton } from '@/components/qr'
 | 
				
			||||||
 | 
					import useDebounceCallback from '@/components/use-debounce-callback'
 | 
				
			||||||
 | 
					import { lnAddrOptions } from '@/lib/lnurl'
 | 
				
			||||||
 | 
					import AccordianItem from '@/components/accordian-item'
 | 
				
			||||||
 | 
					import { numWithUnits } from '@/lib/format'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
					export const getServerSideProps = getGetServerSideProps({ authRequired: true })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Wallet () {
 | 
					export default function Withdraw () {
 | 
				
			||||||
  const router = useRouter()
 | 
					  return (
 | 
				
			||||||
 | 
					    <CenterLayout>
 | 
				
			||||||
  if (router.query.type === 'fund') {
 | 
					      <WithdrawForm />
 | 
				
			||||||
    return (
 | 
					    </CenterLayout>
 | 
				
			||||||
      <CenterLayout>
 | 
					  )
 | 
				
			||||||
        <FundForm />
 | 
					 | 
				
			||||||
      </CenterLayout>
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  } else if (router.query.type?.includes('withdraw')) {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <CenterLayout>
 | 
					 | 
				
			||||||
        <WithdrawalForm />
 | 
					 | 
				
			||||||
      </CenterLayout>
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <CenterLayout>
 | 
					 | 
				
			||||||
        <YouHaveSats />
 | 
					 | 
				
			||||||
        <WalletLimitBanner />
 | 
					 | 
				
			||||||
        <WalletForm />
 | 
					 | 
				
			||||||
        <WalletHistory />
 | 
					 | 
				
			||||||
      </CenterLayout>
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function YouHaveSats () {
 | 
					function WithdrawForm () {
 | 
				
			||||||
 | 
					  const router = useRouter()
 | 
				
			||||||
  const { me } = useMe()
 | 
					  const { me } = useMe()
 | 
				
			||||||
  const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <h2 className={`${me ? 'visible' : 'invisible'} ${limitReached ? 'text-warning' : 'text-success'}`}>
 | 
					 | 
				
			||||||
      you have{' '}
 | 
					 | 
				
			||||||
      <span className='text-monospace'>{me && (
 | 
					 | 
				
			||||||
        me.privates?.hideWalletBalance
 | 
					 | 
				
			||||||
          ? <HiddenWalletSummary />
 | 
					 | 
				
			||||||
          : numWithUnits(me.privates?.sats, { abbreviate: false, format: true })
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
      </span>
 | 
					 | 
				
			||||||
    </h2>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function WalletHistory () {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className='d-flex flex-column text-center'>
 | 
					 | 
				
			||||||
      <div>
 | 
					 | 
				
			||||||
        <Link href='/satistics?inc=invoice,withdrawal' className='text-muted fw-bold text-underline'>
 | 
					 | 
				
			||||||
          wallet history
 | 
					 | 
				
			||||||
        </Link>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function WalletForm () {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className='align-items-center text-center pt-5 pb-4'>
 | 
					 | 
				
			||||||
      <Link href='/wallet?type=fund'>
 | 
					 | 
				
			||||||
        <Button variant='success'>fund</Button>
 | 
					 | 
				
			||||||
      </Link>
 | 
					 | 
				
			||||||
      <span className='mx-3 fw-bold text-muted'>or</span>
 | 
					 | 
				
			||||||
      <Link href='/wallet?type=withdraw'>
 | 
					 | 
				
			||||||
        <Button variant='success'>withdraw</Button>
 | 
					 | 
				
			||||||
      </Link>
 | 
					 | 
				
			||||||
      <div className='mt-5'>
 | 
					 | 
				
			||||||
        <Link href='/settings/wallets'>
 | 
					 | 
				
			||||||
          <Button variant='info'>attach wallets <Plug className='fill-white ms-1' width={16} height={16} /></Button>
 | 
					 | 
				
			||||||
        </Link>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function FundForm () {
 | 
					 | 
				
			||||||
  const { me } = useMe()
 | 
					 | 
				
			||||||
  const [showAlert, setShowAlert] = useState(true)
 | 
					 | 
				
			||||||
  const router = useRouter()
 | 
					 | 
				
			||||||
  const [createInvoice, { called, error }] = useMutation(gql`
 | 
					 | 
				
			||||||
    mutation createInvoice($amount: Int!) {
 | 
					 | 
				
			||||||
      createInvoice(amount: $amount) {
 | 
					 | 
				
			||||||
        __typename
 | 
					 | 
				
			||||||
        id
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    setShowAlert(!window.localStorage.getItem('hideLnAddrAlert'))
 | 
					 | 
				
			||||||
  }, [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (called && !error) {
 | 
					 | 
				
			||||||
    return <QrSkeleton description status='generating' bolt11Info />
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <>
 | 
					 | 
				
			||||||
      <YouHaveSats />
 | 
					 | 
				
			||||||
      <WalletLimitBanner />
 | 
					 | 
				
			||||||
      <div className='w-100 py-5'>
 | 
					 | 
				
			||||||
        {me && showAlert &&
 | 
					 | 
				
			||||||
          <Alert
 | 
					 | 
				
			||||||
            variant='success' dismissible onClose={() => {
 | 
					 | 
				
			||||||
              window.localStorage.setItem('hideLnAddrAlert', 'yep')
 | 
					 | 
				
			||||||
              setShowAlert(false)
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            You can also fund your account via lightning address with <strong>{`${me.name}@stacker.news`}</strong>
 | 
					 | 
				
			||||||
          </Alert>}
 | 
					 | 
				
			||||||
        <Form
 | 
					 | 
				
			||||||
          initial={{
 | 
					 | 
				
			||||||
            amount: 1000
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
          schema={amountSchema}
 | 
					 | 
				
			||||||
          onSubmit={async ({ amount }) => {
 | 
					 | 
				
			||||||
            const { data } = await createInvoice({ variables: { amount: Number(amount) } })
 | 
					 | 
				
			||||||
            if (data.createInvoice.__typename === 'Direct') {
 | 
					 | 
				
			||||||
              router.push(`/directs/${data.createInvoice.id}`)
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              router.push(`/invoices/${data.createInvoice.id}`)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <Input
 | 
					 | 
				
			||||||
            label='amount'
 | 
					 | 
				
			||||||
            name='amount'
 | 
					 | 
				
			||||||
            required
 | 
					 | 
				
			||||||
            autoFocus
 | 
					 | 
				
			||||||
            append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
          <SubmitButton variant='success' className='mt-2'>generate invoice</SubmitButton>
 | 
					 | 
				
			||||||
        </Form>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <WalletHistory />
 | 
					 | 
				
			||||||
    </>
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function WithdrawalForm () {
 | 
					 | 
				
			||||||
  const router = useRouter()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className='w-100 d-flex flex-column align-items-center py-5'>
 | 
					    <div className='w-100 d-flex flex-column align-items-center py-5'>
 | 
				
			||||||
      <YouHaveSats />
 | 
					      <h2 className='text-start ms-1 ms-md-3'>
 | 
				
			||||||
 | 
					        <div className='text-monospace'>
 | 
				
			||||||
 | 
					          {numWithUnits(me?.privates?.sats - me?.privates?.credits, { abbreviate: false, format: true, unitSingular: 'sats', unitPlural: 'sats' })}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </h2>
 | 
				
			||||||
      <Nav
 | 
					      <Nav
 | 
				
			||||||
        className={styles.nav}
 | 
					        className={styles.nav}
 | 
				
			||||||
        activeKey={router.query.type}
 | 
					        activeKey={router.query.type ?? 'invoice'}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <Nav.Item>
 | 
					        <Nav.Item>
 | 
				
			||||||
          <Link href='/wallet?type=withdraw' passHref legacyBehavior>
 | 
					          <Link href='/withdraw' passHref legacyBehavior>
 | 
				
			||||||
            <Nav.Link eventKey='withdraw'>invoice</Nav.Link>
 | 
					            <Nav.Link eventKey='invoice'>invoice</Nav.Link>
 | 
				
			||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
        </Nav.Item>
 | 
					        </Nav.Item>
 | 
				
			||||||
        <Nav.Item>
 | 
					        <Nav.Item>
 | 
				
			||||||
          <Link href='/wallet?type=lnurl-withdraw' passHref legacyBehavior>
 | 
					          <Link href='/withdraw?type=lnurl' passHref legacyBehavior>
 | 
				
			||||||
            <Nav.Link eventKey='lnurl-withdraw'>QR code</Nav.Link>
 | 
					            <Nav.Link eventKey='lnurl'>QR code</Nav.Link>
 | 
				
			||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
        </Nav.Item>
 | 
					        </Nav.Item>
 | 
				
			||||||
        <Nav.Item>
 | 
					        <Nav.Item>
 | 
				
			||||||
          <Link href='/wallet?type=lnaddr-withdraw' passHref legacyBehavior>
 | 
					          <Link href='/withdraw?type=lnaddr' passHref legacyBehavior>
 | 
				
			||||||
            <Nav.Link eventKey='lnaddr-withdraw'>lightning address</Nav.Link>
 | 
					            <Nav.Link eventKey='lnaddr'>lightning address</Nav.Link>
 | 
				
			||||||
          </Link>
 | 
					          </Link>
 | 
				
			||||||
        </Nav.Item>
 | 
					        </Nav.Item>
 | 
				
			||||||
      </Nav>
 | 
					      </Nav>
 | 
				
			||||||
@ -205,12 +75,12 @@ export function SelectedWithdrawalForm () {
 | 
				
			|||||||
  const router = useRouter()
 | 
					  const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  switch (router.query.type) {
 | 
					  switch (router.query.type) {
 | 
				
			||||||
    case 'withdraw':
 | 
					    case 'lnurl':
 | 
				
			||||||
      return <InvWithdrawal />
 | 
					      return <LnurlWithdrawal />
 | 
				
			||||||
    case 'lnurl-withdraw':
 | 
					    case 'lnaddr':
 | 
				
			||||||
      return <LnWithdrawal />
 | 
					 | 
				
			||||||
    case 'lnaddr-withdraw':
 | 
					 | 
				
			||||||
      return <LnAddrWithdrawal />
 | 
					      return <LnAddrWithdrawal />
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return <InvWithdrawal />
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -271,7 +141,9 @@ export function InvWithdrawal () {
 | 
				
			|||||||
          required
 | 
					          required
 | 
				
			||||||
          append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
					          append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <SubmitButton variant='success' className='mt-2'>withdraw</SubmitButton>
 | 
					        <div className='d-flex justify-content-end mt-4'>
 | 
				
			||||||
 | 
					          <SubmitButton variant='success'>withdraw</SubmitButton>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </Form>
 | 
					      </Form>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
@ -343,7 +215,7 @@ function LnQRWith ({ k1, encodedUrl }) {
 | 
				
			|||||||
  return <Qr value={encodedUrl} status='waiting for you' />
 | 
					  return <Qr value={encodedUrl} status='waiting for you' />
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function LnWithdrawal () {
 | 
					export function LnurlWithdrawal () {
 | 
				
			||||||
  // query for challenge
 | 
					  // query for challenge
 | 
				
			||||||
  const [createWith, { data, error }] = useMutation(gql`
 | 
					  const [createWith, { data, error }] = useMutation(gql`
 | 
				
			||||||
    mutation createWith {
 | 
					    mutation createWith {
 | 
				
			||||||
@ -507,7 +379,9 @@ export function LnAddrWithdrawal () {
 | 
				
			|||||||
              />
 | 
					              />
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>}
 | 
					          </div>}
 | 
				
			||||||
        <SubmitButton variant='success' className='mt-2'>send</SubmitButton>
 | 
					        <div className='d-flex justify-content-end mt-4'>
 | 
				
			||||||
 | 
					          <SubmitButton variant='success'>send</SubmitButton>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </Form>
 | 
					      </Form>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
							
								
								
									
										78
									
								
								prisma/migrations/20241203195142_fee_credits/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								prisma/migrations/20241203195142_fee_credits/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "Item" ADD COLUMN     "mcredits" BIGINT NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					ADD COLUMN     "commentMcredits" BIGINT NOT NULL DEFAULT 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "users" ADD COLUMN     "mcredits" BIGINT NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					ADD COLUMN     "stackedMcredits" BIGINT NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					ADD COLUMN     "receiveCreditsBelowSats" INTEGER NOT NULL DEFAULT 10,
 | 
				
			||||||
 | 
					ADD COLUMN     "sendCreditsBelowSats" INTEGER NOT NULL DEFAULT 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- default to true now
 | 
				
			||||||
 | 
					ALTER TABLE "users" ALTER COLUMN "proxyReceive" SET DEFAULT true,
 | 
				
			||||||
 | 
					ALTER COLUMN "directReceive" SET DEFAULT true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- if they don't have either set, set to true
 | 
				
			||||||
 | 
					UPDATE "users" SET "proxyReceive" = true, "directReceive" = true
 | 
				
			||||||
 | 
					WHERE NOT "proxyReceive" AND NOT "directReceive";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- add mcredits check
 | 
				
			||||||
 | 
					ALTER TABLE users ADD CONSTRAINT "mcredits_positive" CHECK ("mcredits" >= 0) NOT VALID;
 | 
				
			||||||
 | 
					ALTER TABLE users ADD CONSTRAINT "stackedMcredits_positive" CHECK ("stackedMcredits" >= 0) NOT VALID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- add cowboy credits
 | 
				
			||||||
 | 
					CREATE OR REPLACE FUNCTION item_comments_zaprank_with_me(_item_id int, _global_seed int, _me_id int, _level int, _where text, _order_by text)
 | 
				
			||||||
 | 
					  RETURNS jsonb
 | 
				
			||||||
 | 
					  LANGUAGE plpgsql VOLATILE PARALLEL SAFE AS
 | 
				
			||||||
 | 
					$$
 | 
				
			||||||
 | 
					DECLARE
 | 
				
			||||||
 | 
					    result  jsonb;
 | 
				
			||||||
 | 
					BEGIN
 | 
				
			||||||
 | 
					    IF _level < 1 THEN
 | 
				
			||||||
 | 
					        RETURN '[]'::jsonb;
 | 
				
			||||||
 | 
					    END IF;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    EXECUTE 'CREATE TEMP TABLE IF NOT EXISTS t_item ON COMMIT DROP AS'
 | 
				
			||||||
 | 
					        || '    SELECT "Item".*, "Item".created_at at time zone ''UTC'' AS "createdAt", "Item".updated_at at time zone ''UTC'' AS "updatedAt", '
 | 
				
			||||||
 | 
					        || '    "Item"."invoicePaidAt" at time zone ''UTC'' AS "invoicePaidAtUTC", to_jsonb(users.*) || jsonb_build_object(''meMute'', "Mute"."mutedId" IS NOT NULL) AS user, '
 | 
				
			||||||
 | 
					        || '    COALESCE("ItemAct"."meMsats", 0) AS "meMsats", COALESCE("ItemAct"."mePendingMsats", 0) as "mePendingMsats", COALESCE("ItemAct"."meDontLikeMsats", 0) AS "meDontLikeMsats", '
 | 
				
			||||||
 | 
					        || '    COALESCE("ItemAct"."meMcredits", 0) AS "meMcredits", COALESCE("ItemAct"."mePendingMcredits", 0) as "mePendingMcredits", '
 | 
				
			||||||
 | 
					        || '    "Bookmark"."itemId" IS NOT NULL AS "meBookmark", "ThreadSubscription"."itemId" IS NOT NULL AS "meSubscription", '
 | 
				
			||||||
 | 
					        || '    GREATEST(g.tf_hot_score, l.tf_hot_score) AS personal_hot_score, GREATEST(g.tf_top_score, l.tf_top_score) AS personal_top_score '
 | 
				
			||||||
 | 
					        || '    FROM "Item" '
 | 
				
			||||||
 | 
					        || '    JOIN users ON users.id = "Item"."userId" '
 | 
				
			||||||
 | 
					        || '    LEFT JOIN "Mute" ON "Mute"."muterId" = $5 AND "Mute"."mutedId" = "Item"."userId"'
 | 
				
			||||||
 | 
					        || '    LEFT JOIN "Bookmark" ON "Bookmark"."userId" = $5 AND "Bookmark"."itemId" = "Item".id '
 | 
				
			||||||
 | 
					        || '    LEFT JOIN "ThreadSubscription" ON "ThreadSubscription"."userId" = $5 AND "ThreadSubscription"."itemId" = "Item".id '
 | 
				
			||||||
 | 
					        || '    LEFT JOIN LATERAL ( '
 | 
				
			||||||
 | 
					        || '        SELECT "itemId", '
 | 
				
			||||||
 | 
					        || '            sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM ''FAILED'' AND "InvoiceForward".id IS NOT NULL AND (act = ''FEE'' OR act = ''TIP'')) AS "meMsats", '
 | 
				
			||||||
 | 
					        || '            sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM ''FAILED'' AND "InvoiceForward".id IS NULL AND (act = ''FEE'' OR act = ''TIP'')) AS "meMcredits", '
 | 
				
			||||||
 | 
					        || '            sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS NOT DISTINCT FROM ''PENDING'' AND "InvoiceForward".id IS NOT NULL AND (act = ''FEE'' OR act = ''TIP'')) AS "mePendingMsats", '
 | 
				
			||||||
 | 
					        || '            sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS NOT DISTINCT FROM ''PENDING'' AND "InvoiceForward".id IS NULL AND (act = ''FEE'' OR act = ''TIP'')) AS "mePendingMcredits", '
 | 
				
			||||||
 | 
					        || '            sum("ItemAct".msats) FILTER (WHERE "invoiceActionState" IS DISTINCT FROM ''FAILED'' AND act = ''DONT_LIKE_THIS'') AS "meDontLikeMsats" '
 | 
				
			||||||
 | 
					        || '        FROM "ItemAct" '
 | 
				
			||||||
 | 
					        || '        LEFT JOIN "Invoice" ON "Invoice".id = "ItemAct"."invoiceId" '
 | 
				
			||||||
 | 
					        || '        LEFT JOIN "InvoiceForward" ON "InvoiceForward"."invoiceId" = "Invoice"."id" '
 | 
				
			||||||
 | 
					        || '        WHERE "ItemAct"."userId" = $5 '
 | 
				
			||||||
 | 
					        || '        AND "ItemAct"."itemId" = "Item".id '
 | 
				
			||||||
 | 
					        || '        GROUP BY "ItemAct"."itemId" '
 | 
				
			||||||
 | 
					        || '    ) "ItemAct" ON true '
 | 
				
			||||||
 | 
					        || '    LEFT JOIN zap_rank_personal_view g ON g."viewerId" = $6 AND g.id = "Item".id '
 | 
				
			||||||
 | 
					        || '    LEFT JOIN zap_rank_personal_view l ON l."viewerId" = $5 AND l.id = g.id '
 | 
				
			||||||
 | 
					        || '    WHERE  "Item".path <@ (SELECT path FROM "Item" WHERE id = $1) ' || _where || ' '
 | 
				
			||||||
 | 
					    USING _item_id, _level, _where, _order_by, _me_id, _global_seed;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    EXECUTE ''
 | 
				
			||||||
 | 
					        || 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
 | 
				
			||||||
 | 
					        || 'FROM  ( '
 | 
				
			||||||
 | 
					        || '    SELECT "Item".*, item_comments_zaprank_with_me("Item".id, $6, $5, $2 - 1, $3, $4) AS comments '
 | 
				
			||||||
 | 
					        || '    FROM t_item "Item" '
 | 
				
			||||||
 | 
					        || '    WHERE  "Item"."parentId" = $1 '
 | 
				
			||||||
 | 
					        ||      _order_by
 | 
				
			||||||
 | 
					        || ' ) sub'
 | 
				
			||||||
 | 
					    INTO result USING _item_id, _level, _where, _order_by, _me_id, _global_seed;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    RETURN result;
 | 
				
			||||||
 | 
					END
 | 
				
			||||||
 | 
					$$;
 | 
				
			||||||
@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					DROP TRIGGER IF EXISTS user_streak ON users;
 | 
				
			||||||
 | 
					CREATE TRIGGER user_streak
 | 
				
			||||||
 | 
					    AFTER UPDATE ON users
 | 
				
			||||||
 | 
					    FOR EACH ROW
 | 
				
			||||||
 | 
					    WHEN (NEW.msats < OLD.msats OR NEW.mcredits < OLD.mcredits)
 | 
				
			||||||
 | 
					    EXECUTE PROCEDURE user_streak_check();
 | 
				
			||||||
@ -1,4 +0,0 @@
 | 
				
			|||||||
-- fix existing boost jobs
 | 
					 | 
				
			||||||
UPDATE pgboss.job
 | 
					 | 
				
			||||||
SET keepuntil = startafter + interval '10 days'
 | 
					 | 
				
			||||||
WHERE name = 'expireBoost' AND state = 'created';
 | 
					 | 
				
			||||||
@ -39,6 +39,7 @@ model User {
 | 
				
			|||||||
  trust                     Float                @default(0)
 | 
					  trust                     Float                @default(0)
 | 
				
			||||||
  lastSeenAt                DateTime?
 | 
					  lastSeenAt                DateTime?
 | 
				
			||||||
  stackedMsats              BigInt               @default(0)
 | 
					  stackedMsats              BigInt               @default(0)
 | 
				
			||||||
 | 
					  stackedMcredits           BigInt               @default(0)
 | 
				
			||||||
  noteAllDescendants        Boolean              @default(true)
 | 
					  noteAllDescendants        Boolean              @default(true)
 | 
				
			||||||
  noteDeposits              Boolean              @default(true)
 | 
					  noteDeposits              Boolean              @default(true)
 | 
				
			||||||
  noteWithdrawals           Boolean              @default(true)
 | 
					  noteWithdrawals           Boolean              @default(true)
 | 
				
			||||||
@ -119,6 +120,9 @@ model User {
 | 
				
			|||||||
  autoWithdrawMaxFeePercent Float?
 | 
					  autoWithdrawMaxFeePercent Float?
 | 
				
			||||||
  autoWithdrawThreshold     Int?
 | 
					  autoWithdrawThreshold     Int?
 | 
				
			||||||
  autoWithdrawMaxFeeTotal   Int?
 | 
					  autoWithdrawMaxFeeTotal   Int?
 | 
				
			||||||
 | 
					  mcredits                  BigInt               @default(0)
 | 
				
			||||||
 | 
					  receiveCreditsBelowSats   Int                  @default(10)
 | 
				
			||||||
 | 
					  sendCreditsBelowSats      Int                  @default(10)
 | 
				
			||||||
  muters                    Mute[]               @relation("muter")
 | 
					  muters                    Mute[]               @relation("muter")
 | 
				
			||||||
  muteds                    Mute[]               @relation("muted")
 | 
					  muteds                    Mute[]               @relation("muted")
 | 
				
			||||||
  ArcOut                    Arc[]                @relation("fromUser")
 | 
					  ArcOut                    Arc[]                @relation("fromUser")
 | 
				
			||||||
@ -140,8 +144,8 @@ model User {
 | 
				
			|||||||
  vaultKeyHash              String               @default("")
 | 
					  vaultKeyHash              String               @default("")
 | 
				
			||||||
  walletsUpdatedAt          DateTime?
 | 
					  walletsUpdatedAt          DateTime?
 | 
				
			||||||
  vaultEntries              VaultEntry[]         @relation("VaultEntries")
 | 
					  vaultEntries              VaultEntry[]         @relation("VaultEntries")
 | 
				
			||||||
  proxyReceive              Boolean              @default(false)
 | 
					  proxyReceive              Boolean              @default(true)
 | 
				
			||||||
  directReceive             Boolean              @default(false)
 | 
					  directReceive             Boolean              @default(true)
 | 
				
			||||||
  DirectPaymentReceived     DirectPayment[]      @relation("DirectPaymentReceived")
 | 
					  DirectPaymentReceived     DirectPayment[]      @relation("DirectPaymentReceived")
 | 
				
			||||||
  DirectPaymentSent         DirectPayment[]      @relation("DirectPaymentSent")
 | 
					  DirectPaymentSent         DirectPayment[]      @relation("DirectPaymentSent")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -520,10 +524,12 @@ model Item {
 | 
				
			|||||||
  pollCost            Int?
 | 
					  pollCost            Int?
 | 
				
			||||||
  paidImgLink         Boolean               @default(false)
 | 
					  paidImgLink         Boolean               @default(false)
 | 
				
			||||||
  commentMsats        BigInt                @default(0)
 | 
					  commentMsats        BigInt                @default(0)
 | 
				
			||||||
 | 
					  commentMcredits     BigInt                @default(0)
 | 
				
			||||||
  lastCommentAt       DateTime?
 | 
					  lastCommentAt       DateTime?
 | 
				
			||||||
  lastZapAt           DateTime?
 | 
					  lastZapAt           DateTime?
 | 
				
			||||||
  ncomments           Int                   @default(0)
 | 
					  ncomments           Int                   @default(0)
 | 
				
			||||||
  msats               BigInt                @default(0)
 | 
					  msats               BigInt                @default(0)
 | 
				
			||||||
 | 
					  mcredits            BigInt                @default(0)
 | 
				
			||||||
  cost                Int                   @default(0)
 | 
					  cost                Int                   @default(0)
 | 
				
			||||||
  weightedDownVotes   Float                 @default(0)
 | 
					  weightedDownVotes   Float                 @default(0)
 | 
				
			||||||
  bio                 Boolean               @default(false)
 | 
					  bio                 Boolean               @default(false)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
# Wallets
 | 
					# Wallets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Every wallet that you can see at [/settings/wallets](https://stacker.news/settings/wallets) is implemented as a plugin in this directory.
 | 
					Every wallet that you can see at [/wallets](https://stacker.news/wallets) is implemented as a plugin in this directory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This README explains how you can add another wallet for use with Stacker News.
 | 
					This README explains how you can add another wallet for use with Stacker News.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -59,11 +59,11 @@ This is an optional value. Set this to true if your wallet needs to be configure
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- `fields: WalletField[]`
 | 
					- `fields: WalletField[]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Wallet fields define what this wallet requires for configuration and thus are used to construct the forms like the one you can see at [/settings/wallets/lnbits](https://stacker.news/settings/walletslnbits).
 | 
					Wallet fields define what this wallet requires for configuration and thus are used to construct the forms like the one you can see at [/wallets/lnbits](https://stacker.news/walletslnbits).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- `card: WalletCard`
 | 
					- `card: WalletCard`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Wallet cards are the components you can see at [/settings/wallets](https://stacker.news/settings/wallets). This property customizes this card for this wallet.
 | 
					Wallet cards are the components you can see at [/wallets](https://stacker.news/wallets). This property customizes this card for this wallet.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- `validate: (config) => void`
 | 
					- `validate: (config) => void`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { useMe } from '@/components/me'
 | 
				
			|||||||
import useVault from '@/components/vault/use-vault'
 | 
					import useVault from '@/components/vault/use-vault'
 | 
				
			||||||
import { useCallback } from 'react'
 | 
					import { useCallback } from 'react'
 | 
				
			||||||
import { canReceive, canSend, getStorageKey, saveWalletLocally, siftConfig, upsertWalletVariables } from './common'
 | 
					import { canReceive, canSend, getStorageKey, saveWalletLocally, siftConfig, upsertWalletVariables } from './common'
 | 
				
			||||||
import { useMutation } from '@apollo/client'
 | 
					import { gql, useMutation } from '@apollo/client'
 | 
				
			||||||
import { generateMutation } from './graphql'
 | 
					import { generateMutation } from './graphql'
 | 
				
			||||||
import { REMOVE_WALLET } from '@/fragments/wallet'
 | 
					import { REMOVE_WALLET } from '@/fragments/wallet'
 | 
				
			||||||
import { useWalletLogger } from '@/wallets/logger'
 | 
					import { useWalletLogger } from '@/wallets/logger'
 | 
				
			||||||
@ -18,6 +18,7 @@ export function useWalletConfigurator (wallet) {
 | 
				
			|||||||
  const logger = useWalletLogger(wallet)
 | 
					  const logger = useWalletLogger(wallet)
 | 
				
			||||||
  const [upsertWallet] = useMutation(generateMutation(wallet?.def))
 | 
					  const [upsertWallet] = useMutation(generateMutation(wallet?.def))
 | 
				
			||||||
  const [removeWallet] = useMutation(REMOVE_WALLET)
 | 
					  const [removeWallet] = useMutation(REMOVE_WALLET)
 | 
				
			||||||
 | 
					  const [disableFreebies] = useMutation(gql`mutation { disableFreebies }`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _saveToServer = useCallback(async (serverConfig, clientConfig, validateLightning) => {
 | 
					  const _saveToServer = useCallback(async (serverConfig, clientConfig, validateLightning) => {
 | 
				
			||||||
    const variables = await upsertWalletVariables(
 | 
					    const variables = await upsertWalletVariables(
 | 
				
			||||||
@ -116,6 +117,7 @@ export function useWalletConfigurator (wallet) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newCanSend) {
 | 
					    if (newCanSend) {
 | 
				
			||||||
 | 
					      disableFreebies().catch(console.error)
 | 
				
			||||||
      if (oldCanSend) {
 | 
					      if (oldCanSend) {
 | 
				
			||||||
        logger.ok('details for sending updated')
 | 
					        logger.ok('details for sending updated')
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
@ -130,7 +132,7 @@ export function useWalletConfigurator (wallet) {
 | 
				
			|||||||
      logger.info('details for sending deleted')
 | 
					      logger.info('details for sending deleted')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [isActive, wallet.def, wallet.config, _saveToServer, _saveToLocal, _validate,
 | 
					  }, [isActive, wallet.def, wallet.config, _saveToServer, _saveToLocal, _validate,
 | 
				
			||||||
    _detachFromLocal, _detachFromServer])
 | 
					    _detachFromLocal, _detachFromServer, disableFreebies])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const detach = useCallback(async () => {
 | 
					  const detach = useCallback(async () => {
 | 
				
			||||||
    if (isActive) {
 | 
					    if (isActive) {
 | 
				
			||||||
 | 
				
			|||||||
@ -27,9 +27,11 @@ const ITEM_SEARCH_FIELDS = gql`
 | 
				
			|||||||
    remote
 | 
					    remote
 | 
				
			||||||
    upvotes
 | 
					    upvotes
 | 
				
			||||||
    sats
 | 
					    sats
 | 
				
			||||||
 | 
					    credits
 | 
				
			||||||
    boost
 | 
					    boost
 | 
				
			||||||
    lastCommentAt
 | 
					    lastCommentAt
 | 
				
			||||||
    commentSats
 | 
					    commentSats
 | 
				
			||||||
 | 
					    commentCredits
 | 
				
			||||||
    path
 | 
					    path
 | 
				
			||||||
    ncomments
 | 
					    ncomments
 | 
				
			||||||
  }`
 | 
					  }`
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user