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)
|
||||
- `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`
|
||||
|
||||
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 = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
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 = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
|
||||
]
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ export const anonable = false
|
|||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
|
||||
]
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
|
|||
import * as DONATE from './donate'
|
||||
import * as BOOST from './boost'
|
||||
import * as RECEIVE from './receive'
|
||||
import * as BUY_CREDITS from './buyCredits'
|
||||
import * as INVITE_GIFT from './inviteGift'
|
||||
|
||||
export const paidActions = {
|
||||
|
@ -33,6 +34,7 @@ export const paidActions = {
|
|||
TERRITORY_UNARCHIVE,
|
||||
DONATE,
|
||||
RECEIVE,
|
||||
BUY_CREDITS,
|
||||
INVITE_GIFT
|
||||
}
|
||||
|
||||
|
@ -96,7 +98,8 @@ export default async function performPaidAction (actionType, args, incomingConte
|
|||
|
||||
// additional payment methods that logged in users can use
|
||||
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 {
|
||||
return await performNoInvoiceAction(actionType, args, contextWithPaymentMethod)
|
||||
} catch (e) {
|
||||
|
@ -141,6 +144,13 @@ async function performNoInvoiceAction (actionType, args, incomingContext) {
|
|||
const context = { ...incomingContext, tx }
|
||||
|
||||
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({
|
||||
where: {
|
||||
id: me?.id ?? USER_ID.anon
|
||||
|
|
|
@ -5,7 +5,8 @@ import { notifyInvite } from '@/lib/webPush'
|
|||
export const anonable = false
|
||||
|
||||
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 }) {
|
||||
|
@ -36,7 +37,7 @@ export async function perform ({ id, userId }, { me, cost, tx }) {
|
|||
}
|
||||
},
|
||||
data: {
|
||||
msats: {
|
||||
mcredits: {
|
||||
increment: cost
|
||||
},
|
||||
inviteId: id,
|
||||
|
|
|
@ -8,6 +8,7 @@ export const anonable = true
|
|||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
|
||||
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,
|
||||
// cost must be greater than user's balance, and user has not disabled freebies
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export const anonable = true
|
|||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
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 { msatsToSats, numWithUnits } from '@/lib/format'
|
||||
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
|
||||
import { datePivot } from '@/lib/time'
|
||||
|
||||
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
|
||||
const MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES = 10
|
||||
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) {
|
||||
const { models, me } = context
|
||||
|
@ -56,47 +54,3 @@ export async function assertBelowMaxPendingDirectPayments (userId, context) {
|
|||
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 = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
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 { notifyDeposit } from '@/lib/webPush'
|
||||
import { getInvoiceableWallets } from '@/wallets/server'
|
||||
import { assertBelowBalanceLimit } from './lib/assert'
|
||||
|
||||
export const anonable = false
|
||||
|
||||
|
@ -19,13 +18,16 @@ export async function getCost ({ msats }) {
|
|||
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.DIRECT && !me?.directReceive) return null
|
||||
if ((cost + me.msats) <= satsToMsats(me.autoWithdrawThreshold)) return null
|
||||
|
||||
const wallets = await getInvoiceableWallets(me.id, { models })
|
||||
if (wallets.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (cost < satsToMsats(me.receiveCreditsBelowSats)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return me.id
|
||||
}
|
||||
|
||||
|
@ -39,7 +41,7 @@ export async function perform ({
|
|||
lud18Data,
|
||||
noteStr
|
||||
}, { me, tx }) {
|
||||
const invoice = await tx.invoice.update({
|
||||
return await tx.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: {
|
||||
comment,
|
||||
|
@ -48,11 +50,6 @@ export async function perform ({
|
|||
},
|
||||
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 }) {
|
||||
|
@ -73,7 +70,7 @@ export async function onPaid ({ invoice }, { tx }) {
|
|||
await tx.user.update({
|
||||
where: { id: invoice.userId },
|
||||
data: {
|
||||
msats: {
|
||||
mcredits: {
|
||||
increment: invoice.msatsReceived
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ export const anonable = false
|
|||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
|
||||
]
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export const anonable = false
|
|||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
|
||||
]
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export const anonable = false
|
|||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
|
||||
]
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export const anonable = false
|
|||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
|
||||
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
|
||||
]
|
||||
|
||||
|
|
|
@ -6,8 +6,9 @@ import { getInvoiceableWallets } from '@/wallets/server'
|
|||
export const anonable = true
|
||||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
|
||||
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.PESSIMISTIC
|
||||
]
|
||||
|
@ -16,16 +17,38 @@ export async function getCost ({ 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({
|
||||
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 })
|
||||
|
||||
// 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 () {
|
||||
|
@ -90,32 +113,38 @@ export async function onPaid ({ invoice, actIds }, { tx }) {
|
|||
const sats = msatsToSats(msats)
|
||||
const itemAct = acts.find(act => act.act === 'TIP')
|
||||
|
||||
// give user and all forwards the sats
|
||||
await tx.$executeRaw`
|
||||
WITH forwardees AS (
|
||||
SELECT "userId", ((${itemAct.msats}::BIGINT * pct) / 100)::BIGINT AS msats
|
||||
FROM "ItemForward"
|
||||
WHERE "itemId" = ${itemAct.itemId}::INTEGER
|
||||
), total_forwarded AS (
|
||||
SELECT COALESCE(SUM(msats), 0) as msats
|
||||
FROM forwardees
|
||||
), recipients AS (
|
||||
SELECT "userId", msats, msats AS "stackedMsats" FROM forwardees
|
||||
UNION
|
||||
SELECT ${itemAct.item.userId}::INTEGER as "userId",
|
||||
CASE WHEN ${!!invoice?.invoiceForward}::BOOLEAN
|
||||
THEN 0::BIGINT
|
||||
ELSE ${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT
|
||||
END as msats,
|
||||
${itemAct.msats}::BIGINT - (SELECT msats FROM total_forwarded)::BIGINT as "stackedMsats"
|
||||
ORDER BY "userId" ASC -- order to prevent deadlocks
|
||||
)
|
||||
UPDATE users
|
||||
SET
|
||||
msats = users.msats + recipients.msats,
|
||||
"stackedMsats" = users."stackedMsats" + recipients."stackedMsats"
|
||||
FROM recipients
|
||||
WHERE users.id = recipients."userId"`
|
||||
if (invoice?.invoiceForward) {
|
||||
// only the op got sats and we need to add it to their stackedMsats
|
||||
// because the sats were p2p
|
||||
await tx.user.update({
|
||||
where: { id: itemAct.item.userId },
|
||||
data: { stackedMsats: { increment: itemAct.msats } }
|
||||
})
|
||||
} else {
|
||||
// splits only use mcredits
|
||||
await tx.$executeRaw`
|
||||
WITH forwardees AS (
|
||||
SELECT "userId", ((${itemAct.msats}::BIGINT * pct) / 100)::BIGINT AS mcredits
|
||||
FROM "ItemForward"
|
||||
WHERE "itemId" = ${itemAct.itemId}::INTEGER
|
||||
), total_forwarded AS (
|
||||
SELECT COALESCE(SUM(mcredits), 0) as mcredits
|
||||
FROM forwardees
|
||||
), recipients AS (
|
||||
SELECT "userId", mcredits FROM forwardees
|
||||
UNION
|
||||
SELECT ${itemAct.item.userId}::INTEGER as "userId",
|
||||
${itemAct.msats}::BIGINT - (SELECT mcredits FROM total_forwarded)::BIGINT as mcredits
|
||||
ORDER BY "userId" ASC -- order to prevent deadlocks
|
||||
)
|
||||
UPDATE users
|
||||
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
|
||||
// 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),
|
||||
upvotes = upvotes + zap.first_vote,
|
||||
msats = "Item".msats + ${msats}::BIGINT,
|
||||
mcredits = "Item".mcredits + ${invoice?.invoiceForward ? 0n : msats}::BIGINT,
|
||||
"lastZapAt" = now()
|
||||
FROM zap, zapper
|
||||
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
|
||||
)
|
||||
UPDATE "Item"
|
||||
SET "commentMsats" = "Item"."commentMsats" + ${msats}::BIGINT
|
||||
SET "commentMsats" = "Item"."commentMsats" + ${msats}::BIGINT,
|
||||
"commentMcredits" = "Item"."commentMcredits" + ${invoice?.invoiceForward ? 0n : msats}::BIGINT
|
||||
FROM zapped
|
||||
WHERE "Item".path @> zapped.path AND "Item".id <> zapped.id`
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ export default {
|
|||
},
|
||||
poor: async (invite, args, { me, models }) => {
|
||||
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 }) => {
|
||||
return invite.userId === me?.id ? invite.description : undefined
|
||||
|
|
|
@ -150,6 +150,7 @@ export async function itemQueryWithMeta ({ me, models, query, orderBy = '' }, ..
|
|||
return await models.$queryRawUnsafe(`
|
||||
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"."meMcredits", 0) as "meMcredits", COALESCE("ItemAct"."mePendingMcredits", 0) as "mePendingMcredits",
|
||||
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",
|
||||
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 LATERAL (
|
||||
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 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 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" = ${me.id}
|
||||
AND "ItemAct"."itemId" = "Item".id
|
||||
GROUP BY "ItemAct"."itemId"
|
||||
|
@ -940,7 +945,7 @@ export default {
|
|||
|
||||
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 })
|
||||
await validateSchema(actSchema, { sats, act })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
@ -974,7 +979,7 @@ export default {
|
|||
}
|
||||
|
||||
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') {
|
||||
return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd })
|
||||
} else if (act === 'BOOST') {
|
||||
|
@ -1049,11 +1054,17 @@ export default {
|
|||
},
|
||||
Item: {
|
||||
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 }) => {
|
||||
return msatsToSats(item.commentMsats)
|
||||
},
|
||||
commentCredits: async (item, args, { models }) => {
|
||||
return msatsToSats(item.commentMcredits)
|
||||
},
|
||||
isJob: async (item, args, { models }) => {
|
||||
return item.subName === 'jobs'
|
||||
},
|
||||
|
@ -1170,8 +1181,8 @@ export default {
|
|||
},
|
||||
meSats: async (item, args, { me, models }) => {
|
||||
if (!me) return 0
|
||||
if (typeof item.meMsats !== 'undefined') {
|
||||
return msatsToSats(item.meMsats)
|
||||
if (typeof item.meMsats !== 'undefined' && typeof item.meMcredits !== 'undefined') {
|
||||
return msatsToSats(BigInt(item.meMsats) + BigInt(item.meMcredits))
|
||||
}
|
||||
|
||||
const { _sum: { msats } } = await models.itemAct.aggregate({
|
||||
|
@ -1197,6 +1208,38 @@ export default {
|
|||
|
||||
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 }) => {
|
||||
if (!me) return 0
|
||||
if (typeof item.meDontLikeMsats !== 'undefined') {
|
||||
|
|
|
@ -247,7 +247,7 @@ export default {
|
|||
WHERE "Withdrawl"."userId" = $1
|
||||
AND "Withdrawl".status = 'CONFIRMED'
|
||||
AND "Withdrawl".created_at < $2
|
||||
AND ("InvoiceForward"."id" IS NULL OR "Invoice"."actionType" = 'ZAP')
|
||||
AND "InvoiceForward"."id" IS NULL
|
||||
GROUP BY "Withdrawl".id
|
||||
ORDER BY "sortTime" DESC
|
||||
LIMIT ${LIMIT})`
|
||||
|
|
|
@ -21,6 +21,8 @@ function paidActionType (actionType) {
|
|||
return 'PollVotePaidAction'
|
||||
case 'RECEIVE':
|
||||
return 'ReceivePaidAction'
|
||||
case 'BUY_CREDITS':
|
||||
return 'BuyCreditsPaidAction'
|
||||
default:
|
||||
throw new Error('Unknown action type')
|
||||
}
|
||||
|
|
|
@ -1024,7 +1024,13 @@ export default {
|
|||
if (!me || me.id !== user.id) {
|
||||
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,
|
||||
hasInvites: async (user, args, { models }) => {
|
||||
|
@ -1106,7 +1112,7 @@ export default {
|
|||
|
||||
if (!when || when === 'forever') {
|
||||
// forever
|
||||
return (user.stackedMsats && msatsToSats(user.stackedMsats)) || 0
|
||||
return ((user.stackedMsats && msatsToSats(user.stackedMsats)) || 0)
|
||||
}
|
||||
|
||||
const range = whenRange(when, from, to)
|
||||
|
|
|
@ -583,6 +583,9 @@ const resolvers = {
|
|||
await models.walletLog.deleteMany({ where: { userId: me.id, wallet } })
|
||||
|
||||
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
|
||||
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 }) => {
|
||||
try {
|
||||
return JSON.parse(invoice.desc)
|
||||
|
|
|
@ -60,7 +60,7 @@ export default gql`
|
|||
hash: String, hmac: String): ItemPaidAction!
|
||||
updateNoteId(id: ID!, noteId: String!): Item!
|
||||
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!
|
||||
toggleOutlaw(id: ID!): Item!
|
||||
}
|
||||
|
@ -127,10 +127,13 @@ export default gql`
|
|||
bountyPaidTo: [Int]
|
||||
noteId: String
|
||||
sats: Int!
|
||||
credits: Int!
|
||||
commentSats: Int!
|
||||
commentCredits: Int!
|
||||
lastCommentAt: Date
|
||||
upvotes: Int!
|
||||
meSats: Int!
|
||||
meCredits: Int!
|
||||
meDontLikeSats: Int!
|
||||
meBookmark: Boolean!
|
||||
meSubscription: Boolean!
|
||||
|
|
|
@ -11,6 +11,7 @@ extend type Mutation {
|
|||
}
|
||||
|
||||
enum PaymentMethod {
|
||||
REWARD_SATS
|
||||
FEE_CREDIT
|
||||
ZERO_COST
|
||||
OPTIMISTIC
|
||||
|
@ -52,4 +53,9 @@ type DonatePaidAction implements PaidAction {
|
|||
paymentMethod: PaymentMethod!
|
||||
}
|
||||
|
||||
type BuyCreditsPaidAction implements PaidAction {
|
||||
result: BuyCreditsResult
|
||||
invoice: Invoice
|
||||
paymentMethod: PaymentMethod!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -114,6 +114,8 @@ export default gql`
|
|||
withdrawMaxFeeDefault: Int!
|
||||
proxyReceive: Boolean
|
||||
directReceive: Boolean
|
||||
receiveCreditsBelowSats: Int!
|
||||
sendCreditsBelowSats: Int!
|
||||
}
|
||||
|
||||
type AuthMethods {
|
||||
|
@ -130,6 +132,7 @@ export default gql`
|
|||
extremely sensitive
|
||||
"""
|
||||
sats: Int!
|
||||
credits: Int!
|
||||
authMethods: AuthMethods!
|
||||
lnAddr: String
|
||||
|
||||
|
@ -194,6 +197,8 @@ export default gql`
|
|||
walletsUpdatedAt: Date
|
||||
proxyReceive: Boolean
|
||||
directReceive: Boolean
|
||||
receiveCreditsBelowSats: Int!
|
||||
sendCreditsBelowSats: Int!
|
||||
}
|
||||
|
||||
type UserOptional {
|
||||
|
|
|
@ -83,6 +83,11 @@ const typeDefs = `
|
|||
removeWallet(id: ID!): Boolean
|
||||
deleteWalletLogs(wallet: String): Boolean
|
||||
setWalletPriority(id: ID!, priority: Int!): Boolean
|
||||
buyCredits(credits: Int!): BuyCreditsPaidAction!
|
||||
}
|
||||
|
||||
type BuyCreditsResult {
|
||||
credits: Int!
|
||||
}
|
||||
|
||||
interface InvoiceOrDirect {
|
||||
|
@ -126,6 +131,7 @@ const typeDefs = `
|
|||
actionState: String
|
||||
actionType: String
|
||||
actionError: String
|
||||
invoiceForward: Boolean
|
||||
item: Item
|
||||
itemAct: ItemAct
|
||||
forwardedSats: Int
|
||||
|
|
|
@ -5,8 +5,6 @@ import { useMe } from '@/components/me'
|
|||
import { useMutation } from '@apollo/client'
|
||||
import { WELCOME_BANNER_MUTATION } from '@/fragments/users'
|
||||
import { useToast } from '@/components/toast'
|
||||
import { BALANCE_LIMIT_MSATS } from '@/lib/constants'
|
||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||
import Link from 'next/link'
|
||||
|
||||
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 }) {
|
||||
return (
|
||||
<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 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
|
||||
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 {
|
||||
lines,
|
||||
merge: mergeLineItems,
|
||||
|
|
|
@ -179,9 +179,11 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
|
|||
</Form>)
|
||||
}
|
||||
|
||||
function modifyActCache (cache, { result, invoice }) {
|
||||
function modifyActCache (cache, { result, invoice }, me) {
|
||||
if (!result) return
|
||||
const { id, sats, act } = result
|
||||
const p2p = invoice?.invoiceForward
|
||||
|
||||
cache.modify({
|
||||
id: `Item:${id}`,
|
||||
fields: {
|
||||
|
@ -191,12 +193,24 @@ function modifyActCache (cache, { result, invoice }) {
|
|||
}
|
||||
return existingSats
|
||||
},
|
||||
credits (existingCredits = 0) {
|
||||
if (act === 'TIP' && !p2p) {
|
||||
return existingCredits + sats
|
||||
}
|
||||
return existingCredits
|
||||
},
|
||||
meSats: (existingSats = 0) => {
|
||||
if (act === 'TIP') {
|
||||
if (act === 'TIP' && me) {
|
||||
return existingSats + sats
|
||||
}
|
||||
return existingSats
|
||||
},
|
||||
meCredits: (existingCredits = 0) => {
|
||||
if (act === 'TIP' && !p2p && me) {
|
||||
return existingCredits + sats
|
||||
}
|
||||
return existingCredits
|
||||
},
|
||||
meDontLikeSats: (existingSats = 0) => {
|
||||
if (act === 'DONT_LIKE_THIS') {
|
||||
return existingSats + sats
|
||||
|
@ -219,6 +233,8 @@ function modifyActCache (cache, { result, invoice }) {
|
|||
function updateAncestors (cache, { result, invoice }) {
|
||||
if (!result) return
|
||||
const { id, sats, act, path } = result
|
||||
const p2p = invoice?.invoiceForward
|
||||
|
||||
if (act === 'TIP') {
|
||||
// update all ancestors
|
||||
path.split('.').forEach(aId => {
|
||||
|
@ -226,6 +242,12 @@ function updateAncestors (cache, { result, invoice }) {
|
|||
cache.modify({
|
||||
id: `Item:${aId}`,
|
||||
fields: {
|
||||
commentCredits (existingCommentCredits = 0) {
|
||||
if (p2p) {
|
||||
return existingCommentCredits
|
||||
}
|
||||
return existingCommentCredits + sats
|
||||
},
|
||||
commentSats (existingCommentSats = 0) {
|
||||
return existingCommentSats + sats
|
||||
}
|
||||
|
@ -237,6 +259,7 @@ function updateAncestors (cache, { result, invoice }) {
|
|||
}
|
||||
|
||||
export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
|
||||
const { me } = useMe()
|
||||
// because the mutation name we use varies,
|
||||
// we need to extract the result/invoice from the response
|
||||
const getPaidActionResult = data => Object.values(data)[0]
|
||||
|
@ -253,7 +276,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
|
|||
update: (cache, { data }) => {
|
||||
const response = getPaidActionResult(data)
|
||||
if (!response) return
|
||||
modifyActCache(cache, response)
|
||||
modifyActCache(cache, response, me)
|
||||
options?.update?.(cache, { data })
|
||||
},
|
||||
onPayError: (e, cache, { data }) => {
|
||||
|
@ -261,7 +284,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
|
|||
if (!response || !response.result) return
|
||||
const { result: { sats } } = response
|
||||
const negate = { ...response, result: { ...response.result, sats: -1 * sats } }
|
||||
modifyActCache(cache, negate)
|
||||
modifyActCache(cache, negate, me)
|
||||
options?.onPayError?.(e, 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
|
||||
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 } } }
|
||||
|
||||
try {
|
||||
|
|
|
@ -30,6 +30,39 @@ import classNames from 'classnames'
|
|||
import SubPopover from './sub-popover'
|
||||
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 ({
|
||||
item, full, commentsText = 'comments',
|
||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, edit, toggleEdit, editText,
|
||||
|
@ -62,16 +95,7 @@ export default function ItemInfo ({
|
|||
<div className={className || `${styles.other}`}>
|
||||
{!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) &&
|
||||
<>
|
||||
<span title={`from ${numWithUnits(item.upvotes, {
|
||||
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)`} `}
|
||||
>
|
||||
<span title={itemTitle(item)}>
|
||||
{numWithUnits(item.sats)}
|
||||
</span>
|
||||
<span> \ </span>
|
||||
|
@ -229,11 +253,21 @@ function InfoDropdownItem ({ item }) {
|
|||
<div>cost</div>
|
||||
<div>{item.cost}</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 && (
|
||||
<>
|
||||
<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>
|
||||
|
|
|
@ -6,12 +6,11 @@ import BackArrow from '../../svgs/arrow-left-line.svg'
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Price from '../price'
|
||||
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 NoteIcon from '../../svgs/notification-4-fill.svg'
|
||||
import { useMe } from '../me'
|
||||
import HiddenWalletSummary from '../hidden-wallet-summary'
|
||||
import { abbrNum, msatsToSats } from '../../lib/format'
|
||||
import { abbrNum } from '../../lib/format'
|
||||
import { useServiceWorker } from '../serviceworker'
|
||||
import { signOut } from 'next-auth/react'
|
||||
import Badges from '../badge'
|
||||
|
@ -25,7 +24,7 @@ import { useHasNewNotes } from '../use-has-new-notes'
|
|||
import { useWallets } from '@/wallets/index'
|
||||
import SwitchAccountList, { useAccounts } from '@/components/account'
|
||||
import { useShowModal } from '@/components/modal'
|
||||
|
||||
import { numWithUnits } from '@/lib/format'
|
||||
export function Brand ({ className }) {
|
||||
return (
|
||||
<Link href='/' passHref legacyBehavior>
|
||||
|
@ -140,21 +139,24 @@ export function NavNotifications ({ className }) {
|
|||
|
||||
export function WalletSummary () {
|
||||
const { me } = useMe()
|
||||
if (!me) return null
|
||||
if (me.privates?.hideWalletBalance) {
|
||||
return <HiddenWalletSummary abbreviate fixedWidth />
|
||||
}
|
||||
return `${abbrNum(me.privates?.sats)}`
|
||||
if (!me || me.privates?.sats === 0) return null
|
||||
return (
|
||||
<span
|
||||
className='text-monospace'
|
||||
title={`${numWithUnits(me.privates?.credits, { abbreviate: false, unitSingular: 'CC', unitPlural: 'CCs' })}`}
|
||||
>
|
||||
{`${abbrNum(me.privates?.sats)}`}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavWalletSummary ({ className }) {
|
||||
const { me } = useMe()
|
||||
const walletLimitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||
|
||||
return (
|
||||
<Nav.Item className={className}>
|
||||
<Link href='/wallet' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='wallet' className={`${walletLimitReached ? 'text-warning' : 'text-success'} text-monospace px-0 text-nowrap`}>
|
||||
<Link href='/credits' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='credits' className='text-success text-monospace px-0 text-nowrap'>
|
||||
<WalletSummary me={me} />
|
||||
</Nav.Link>
|
||||
</Link>
|
||||
|
@ -194,8 +196,11 @@ export function MeDropdown ({ me, dropNavKey }) {
|
|||
<Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
|
||||
<Dropdown.Item active={me.name + '/bookmarks' === dropNavKey}>bookmarks</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href='/wallet' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='wallet'>wallet</Dropdown.Item>
|
||||
<Link href='/wallets' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='wallets'>wallets</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href='/credits' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='credits'>credits</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='satistics'>satistics</Dropdown.Item>
|
||||
|
|
|
@ -59,8 +59,11 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||
<Link href={'/' + me.name + '/bookmarks'} passHref legacyBehavior>
|
||||
<Dropdown.Item active={me.name + '/bookmarks' === dropNavKey}>bookmarks</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href='/wallet' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='wallet'>wallet</Dropdown.Item>
|
||||
<Link href='/wallets' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='wallets'>wallets</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href='/credits' passHref legacyBehavior>
|
||||
<Dropdown.Item eventKey='credits'>credits</Dropdown.Item>
|
||||
</Link>
|
||||
<Link href='/satistics?inc=invoice,withdrawal,stacked,spent' passHref legacyBehavior>
|
||||
<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 }) {
|
||||
let forwardedSats = 0
|
||||
let ForwardedUsers = null
|
||||
let stackedTextString
|
||||
let forwardedTextString
|
||||
if (n.item.forwards?.length) {
|
||||
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) =>
|
||||
|
@ -547,14 +565,18 @@ function Votification ({ n }) {
|
|||
</Link>
|
||||
{i !== n.item.forwards.length - 1 && ' '}
|
||||
</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 (
|
||||
<>
|
||||
<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 &&
|
||||
<>
|
||||
{' '}and forwarded {numWithUnits(forwardedSats, { abbreviate: false })} to{' '}
|
||||
{' '}and forwarded {forwardedTextString} to{' '}
|
||||
<ForwardedUsers />
|
||||
</>}
|
||||
</NoteHeader>
|
||||
|
@ -567,7 +589,7 @@ function ForwardedVotification ({ n }) {
|
|||
return (
|
||||
<>
|
||||
<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>
|
||||
<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>}
|
||||
</div>
|
||||
</Card.Body>
|
||||
<Link href={`/settings/wallets/${wallet.def.name}`}>
|
||||
<Link href={`/wallets/${wallet.def.name}`}>
|
||||
<Card.Footer className={styles.attach}>
|
||||
{isConfigured(wallet)
|
||||
? <>configure<Gear width={14} height={14} /></>
|
||||
|
|
|
@ -281,7 +281,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||
|
||||
useEffect(() => {
|
||||
// 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
|
||||
|
||||
let timeout
|
||||
|
|
|
@ -27,11 +27,13 @@ export const COMMENT_FIELDS = gql`
|
|||
...StreakFields
|
||||
}
|
||||
sats
|
||||
credits
|
||||
meAnonSats @client
|
||||
upvotes
|
||||
freedFreebie
|
||||
boost
|
||||
meSats
|
||||
meCredits
|
||||
meDontLikeSats
|
||||
meBookmark
|
||||
meSubscription
|
||||
|
@ -39,6 +41,7 @@ export const COMMENT_FIELDS = gql`
|
|||
freebie
|
||||
path
|
||||
commentSats
|
||||
commentCredits
|
||||
mine
|
||||
otsHash
|
||||
ncomments
|
||||
|
|
|
@ -38,6 +38,7 @@ export const ITEM_FIELDS = gql`
|
|||
otsHash
|
||||
position
|
||||
sats
|
||||
credits
|
||||
meAnonSats @client
|
||||
boost
|
||||
bounty
|
||||
|
@ -46,6 +47,7 @@ export const ITEM_FIELDS = gql`
|
|||
path
|
||||
upvotes
|
||||
meSats
|
||||
meCredits
|
||||
meDontLikeSats
|
||||
meBookmark
|
||||
meSubscription
|
||||
|
@ -55,6 +57,7 @@ export const ITEM_FIELDS = gql`
|
|||
bio
|
||||
ncomments
|
||||
commentSats
|
||||
commentCredits
|
||||
lastCommentAt
|
||||
isJob
|
||||
status
|
||||
|
|
|
@ -11,6 +11,7 @@ export const PAID_ACTION = gql`
|
|||
fragment PaidActionFields on PaidAction {
|
||||
invoice {
|
||||
...InvoiceFields
|
||||
invoiceForward
|
||||
}
|
||||
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`
|
||||
${PAID_ACTION}
|
||||
${ITEM_ACT_PAID_ACTION_FIELDS}
|
||||
|
|
|
@ -39,6 +39,7 @@ ${STREAK_FIELDS}
|
|||
nostrCrossposting
|
||||
nsfwMode
|
||||
sats
|
||||
credits
|
||||
tipDefault
|
||||
tipRandom
|
||||
tipRandomMin
|
||||
|
@ -116,6 +117,8 @@ export const SETTINGS_FIELDS = gql`
|
|||
apiKeyEnabled
|
||||
proxyReceive
|
||||
directReceive
|
||||
receiveCreditsBelowSats
|
||||
sendCreditsBelowSats
|
||||
}
|
||||
}`
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ export const PAID_ACTION_PAYMENT_METHODS = {
|
|||
PESSIMISTIC: 'PESSIMISTIC',
|
||||
OPTIMISTIC: 'OPTIMISTIC',
|
||||
DIRECT: 'DIRECT',
|
||||
P2P: 'P2P'
|
||||
P2P: 'P2P',
|
||||
REWARD_SATS: 'REWARD_SATS'
|
||||
}
|
||||
export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
|
||||
export const NOFOLLOW_LIMIT = 1000
|
||||
|
@ -50,7 +51,6 @@ export const ITEM_EDIT_SECONDS = 600
|
|||
export const ITEM_SPAM_INTERVAL = '10m'
|
||||
export const ANON_ITEM_SPAM_INTERVAL = '0'
|
||||
export const INV_PENDING_LIMIT = 100
|
||||
export const BALANCE_LIMIT_MSATS = 100000000 // 100k sat
|
||||
export const USER_ID = {
|
||||
k00b: 616,
|
||||
ek: 6030,
|
||||
|
|
|
@ -2,11 +2,11 @@ import { string, ValidationError, number, object, array, boolean, date } from '.
|
|||
import {
|
||||
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,
|
||||
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'
|
||||
import { SUPPORTED_CURRENCIES } from './currency'
|
||||
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 { NAME_QUERY } from '@/fragments/users'
|
||||
import { datePivot } from './time'
|
||||
|
@ -185,7 +185,7 @@ export function advSchema (args) {
|
|||
}
|
||||
|
||||
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),
|
||||
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) {
|
||||
Inner = () => <div className='text-danger'>this invite link expired</div>
|
||||
} 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 {
|
||||
Inner = () => (
|
||||
<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>{' '}
|
||||
when you sign up today
|
||||
when you sign up
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -264,7 +264,7 @@ export default function Satistics ({ ssrData }) {
|
|||
<div className={styles.rows}>
|
||||
<div className={[styles.type, styles.head].join(' ')}>type</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} />)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -160,12 +160,15 @@ export default function Settings ({ ssrData }) {
|
|||
hideIsContributor: settings?.hideIsContributor,
|
||||
noReferralLinks: settings?.noReferralLinks,
|
||||
proxyReceive: settings?.proxyReceive,
|
||||
directReceive: settings?.directReceive
|
||||
directReceive: settings?.directReceive,
|
||||
receiveCreditsBelowSats: settings?.receiveCreditsBelowSats,
|
||||
sendCreditsBelowSats: settings?.sendCreditsBelowSats
|
||||
}}
|
||||
schema={settingsSchema}
|
||||
onSubmit={async ({
|
||||
tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault,
|
||||
zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, satsFilter,
|
||||
receiveCreditsBelowSats, sendCreditsBelowSats,
|
||||
...values
|
||||
}) => {
|
||||
if (nostrPubkey.length === 0) {
|
||||
|
@ -191,6 +194,8 @@ export default function Settings ({ ssrData }) {
|
|||
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
|
||||
satsFilter: Number(satsFilter),
|
||||
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
|
||||
receiveCreditsBelowSats: Number(receiveCreditsBelowSats),
|
||||
sendCreditsBelowSats: Number(sendCreditsBelowSats),
|
||||
nostrPubkey,
|
||||
nostrRelays: nostrRelaysFiltered,
|
||||
...values
|
||||
|
@ -335,6 +340,18 @@ export default function Settings ({ ssrData }) {
|
|||
name='noteCowboyHat'
|
||||
/>
|
||||
<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
|
||||
label={
|
||||
<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)
|
||||
|
||||
toaster.success('saved settings')
|
||||
router.push('/settings/wallets')
|
||||
router.push('/wallets')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger(err.message || err.toString?.())
|
||||
|
@ -115,7 +115,7 @@ export default function WalletSettings () {
|
|||
try {
|
||||
await detach()
|
||||
toaster.success('saved settings')
|
||||
router.push('/settings/wallets')
|
||||
router.push('/wallets')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const message = 'failed to detach: ' + err.message || err.toString?.()
|
|
@ -86,10 +86,10 @@ export default function Wallet ({ ssrData }) {
|
|||
return (
|
||||
<Layout>
|
||||
<div className='py-5 w-100'>
|
||||
<h2 className='mb-2 text-center'>attach wallets</h2>
|
||||
<h6 className='text-muted text-center'>attach wallets to supplement your SN wallet</h6>
|
||||
<h2 className='mb-2 text-center'>wallets</h2>
|
||||
<h6 className='text-muted text-center'>use real bitcoin</h6>
|
||||
<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
|
||||
</Link>
|
||||
</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 { amountSchema, lnAddrSchema, withdrawlSchema } from '@/lib/validate'
|
||||
import Nav from 'react-bootstrap/Nav'
|
||||
import { BALANCE_LIMIT_MSATS, FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||
import { CenterLayout } from '@/components/layout'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { InputGroup, Nav } from 'react-bootstrap'
|
||||
import styles from '@/components/user-header.module.css'
|
||||
import HiddenWalletSummary from '@/components/hidden-wallet-summary'
|
||||
import AccordianItem from '@/components/accordian-item'
|
||||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
import useDebounceCallback from '@/components/use-debounce-callback'
|
||||
import { Scanner } from '@yudiel/react-qr-scanner'
|
||||
import CameraIcon from '@/svgs/camera-line.svg'
|
||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
||||
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '@/fragments/wallet'
|
||||
import { requestProvider } from 'webln'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMe } from '@/components/me'
|
||||
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 { useField } from 'formik'
|
||||
import { useToast } from '@/components/toast'
|
||||
import { WalletLimitBanner } from '@/components/banners'
|
||||
import Plug from '@/svgs/plug.svg'
|
||||
import { Scanner } from '@yudiel/react-qr-scanner'
|
||||
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 default function Wallet () {
|
||||
const router = useRouter()
|
||||
|
||||
if (router.query.type === 'fund') {
|
||||
return (
|
||||
<CenterLayout>
|
||||
<FundForm />
|
||||
</CenterLayout>
|
||||
)
|
||||
} else if (router.query.type?.includes('withdraw')) {
|
||||
return (
|
||||
<CenterLayout>
|
||||
<WithdrawalForm />
|
||||
</CenterLayout>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<CenterLayout>
|
||||
<YouHaveSats />
|
||||
<WalletLimitBanner />
|
||||
<WalletForm />
|
||||
<WalletHistory />
|
||||
</CenterLayout>
|
||||
)
|
||||
}
|
||||
export default function Withdraw () {
|
||||
return (
|
||||
<CenterLayout>
|
||||
<WithdrawForm />
|
||||
</CenterLayout>
|
||||
)
|
||||
}
|
||||
|
||||
function YouHaveSats () {
|
||||
function WithdrawForm () {
|
||||
const router = useRouter()
|
||||
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 (
|
||||
<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
|
||||
className={styles.nav}
|
||||
activeKey={router.query.type}
|
||||
activeKey={router.query.type ?? 'invoice'}
|
||||
>
|
||||
<Nav.Item>
|
||||
<Link href='/wallet?type=withdraw' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='withdraw'>invoice</Nav.Link>
|
||||
<Link href='/withdraw' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='invoice'>invoice</Nav.Link>
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Link href='/wallet?type=lnurl-withdraw' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='lnurl-withdraw'>QR code</Nav.Link>
|
||||
<Link href='/withdraw?type=lnurl' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='lnurl'>QR code</Nav.Link>
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Link href='/wallet?type=lnaddr-withdraw' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='lnaddr-withdraw'>lightning address</Nav.Link>
|
||||
<Link href='/withdraw?type=lnaddr' passHref legacyBehavior>
|
||||
<Nav.Link eventKey='lnaddr'>lightning address</Nav.Link>
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
</Nav>
|
||||
|
@ -205,12 +75,12 @@ export function SelectedWithdrawalForm () {
|
|||
const router = useRouter()
|
||||
|
||||
switch (router.query.type) {
|
||||
case 'withdraw':
|
||||
return <InvWithdrawal />
|
||||
case 'lnurl-withdraw':
|
||||
return <LnWithdrawal />
|
||||
case 'lnaddr-withdraw':
|
||||
case 'lnurl':
|
||||
return <LnurlWithdrawal />
|
||||
case 'lnaddr':
|
||||
return <LnAddrWithdrawal />
|
||||
default:
|
||||
return <InvWithdrawal />
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,7 +141,9 @@ export function InvWithdrawal () {
|
|||
required
|
||||
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>
|
||||
</>
|
||||
)
|
||||
|
@ -343,7 +215,7 @@ function LnQRWith ({ k1, encodedUrl }) {
|
|||
return <Qr value={encodedUrl} status='waiting for you' />
|
||||
}
|
||||
|
||||
export function LnWithdrawal () {
|
||||
export function LnurlWithdrawal () {
|
||||
// query for challenge
|
||||
const [createWith, { data, error }] = useMutation(gql`
|
||||
mutation createWith {
|
||||
|
@ -507,7 +379,9 @@ export function LnAddrWithdrawal () {
|
|||
/>
|
||||
</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>
|
||||
</>
|
||||
)
|
|
@ -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)
|
||||
lastSeenAt DateTime?
|
||||
stackedMsats BigInt @default(0)
|
||||
stackedMcredits BigInt @default(0)
|
||||
noteAllDescendants Boolean @default(true)
|
||||
noteDeposits Boolean @default(true)
|
||||
noteWithdrawals Boolean @default(true)
|
||||
|
@ -119,6 +120,9 @@ model User {
|
|||
autoWithdrawMaxFeePercent Float?
|
||||
autoWithdrawThreshold Int?
|
||||
autoWithdrawMaxFeeTotal Int?
|
||||
mcredits BigInt @default(0)
|
||||
receiveCreditsBelowSats Int @default(10)
|
||||
sendCreditsBelowSats Int @default(10)
|
||||
muters Mute[] @relation("muter")
|
||||
muteds Mute[] @relation("muted")
|
||||
ArcOut Arc[] @relation("fromUser")
|
||||
|
@ -140,8 +144,8 @@ model User {
|
|||
vaultKeyHash String @default("")
|
||||
walletsUpdatedAt DateTime?
|
||||
vaultEntries VaultEntry[] @relation("VaultEntries")
|
||||
proxyReceive Boolean @default(false)
|
||||
directReceive Boolean @default(false)
|
||||
proxyReceive Boolean @default(true)
|
||||
directReceive Boolean @default(true)
|
||||
DirectPaymentReceived DirectPayment[] @relation("DirectPaymentReceived")
|
||||
DirectPaymentSent DirectPayment[] @relation("DirectPaymentSent")
|
||||
|
||||
|
@ -520,10 +524,12 @@ model Item {
|
|||
pollCost Int?
|
||||
paidImgLink Boolean @default(false)
|
||||
commentMsats BigInt @default(0)
|
||||
commentMcredits BigInt @default(0)
|
||||
lastCommentAt DateTime?
|
||||
lastZapAt DateTime?
|
||||
ncomments Int @default(0)
|
||||
msats BigInt @default(0)
|
||||
mcredits BigInt @default(0)
|
||||
cost Int @default(0)
|
||||
weightedDownVotes Float @default(0)
|
||||
bio Boolean @default(false)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
|
@ -59,11 +59,11 @@ This is an optional value. Set this to true if your wallet needs to be configure
|
|||
|
||||
- `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`
|
||||
|
||||
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`
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useMe } from '@/components/me'
|
|||
import useVault from '@/components/vault/use-vault'
|
||||
import { useCallback } from 'react'
|
||||
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 { REMOVE_WALLET } from '@/fragments/wallet'
|
||||
import { useWalletLogger } from '@/wallets/logger'
|
||||
|
@ -18,6 +18,7 @@ export function useWalletConfigurator (wallet) {
|
|||
const logger = useWalletLogger(wallet)
|
||||
const [upsertWallet] = useMutation(generateMutation(wallet?.def))
|
||||
const [removeWallet] = useMutation(REMOVE_WALLET)
|
||||
const [disableFreebies] = useMutation(gql`mutation { disableFreebies }`)
|
||||
|
||||
const _saveToServer = useCallback(async (serverConfig, clientConfig, validateLightning) => {
|
||||
const variables = await upsertWalletVariables(
|
||||
|
@ -116,6 +117,7 @@ export function useWalletConfigurator (wallet) {
|
|||
}
|
||||
|
||||
if (newCanSend) {
|
||||
disableFreebies().catch(console.error)
|
||||
if (oldCanSend) {
|
||||
logger.ok('details for sending updated')
|
||||
} else {
|
||||
|
@ -130,7 +132,7 @@ export function useWalletConfigurator (wallet) {
|
|||
logger.info('details for sending deleted')
|
||||
}
|
||||
}, [isActive, wallet.def, wallet.config, _saveToServer, _saveToLocal, _validate,
|
||||
_detachFromLocal, _detachFromServer])
|
||||
_detachFromLocal, _detachFromServer, disableFreebies])
|
||||
|
||||
const detach = useCallback(async () => {
|
||||
if (isActive) {
|
||||
|
|
|
@ -27,9 +27,11 @@ const ITEM_SEARCH_FIELDS = gql`
|
|||
remote
|
||||
upvotes
|
||||
sats
|
||||
credits
|
||||
boost
|
||||
lastCommentAt
|
||||
commentSats
|
||||
commentCredits
|
||||
path
|
||||
ncomments
|
||||
}`
|
||||
|
|
Loading…
Reference in New Issue