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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
|
@ -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…
Reference in New Issue