a90b765670
* Adding, awarding, and sorting by bounties
* Add notification for bounty award as tip
* Fix merge
* Wording
* Allow adding in batches of m250
* import
* imports
* Style tabs
* Refund unused bounties
* Show curreantly available, reset open to 0
* Refactor
* Rerun check prs
* reset yarn.lock
* Revert "reset yarn.lock"
This reverts commit 4606984276
.
* undo yarn.lock changes
* Track comment bounties
163 lines
4.9 KiB
TypeScript
163 lines
4.9 KiB
TypeScript
import * as admin from 'firebase-admin'
|
|
import { z } from 'zod'
|
|
|
|
import { Contract } from '../../common/contract'
|
|
import { User } from '../../common/user'
|
|
import { removeUndefinedProps } from '../../common/util/object'
|
|
import { APIError, newEndpoint, validate } from './api'
|
|
import {
|
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
|
} from '../../common/antes'
|
|
import { isProd } from './utils'
|
|
import {
|
|
CommentBountyDepositTxn,
|
|
CommentBountyWithdrawalTxn,
|
|
} from '../../common/txn'
|
|
import { runTxn } from './transact'
|
|
import { Comment } from '../../common/comment'
|
|
import { createBountyNotification } from './create-notification'
|
|
|
|
const bodySchema = z.object({
|
|
contractId: z.string(),
|
|
amount: z.number().gt(0),
|
|
})
|
|
const awardBodySchema = z.object({
|
|
contractId: z.string(),
|
|
commentId: z.string(),
|
|
amount: z.number().gt(0),
|
|
})
|
|
|
|
export const addcommentbounty = newEndpoint({}, async (req, auth) => {
|
|
const { amount, contractId } = validate(bodySchema, req.body)
|
|
|
|
if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
|
|
|
|
// run as transaction to prevent race conditions
|
|
return await firestore.runTransaction(async (transaction) => {
|
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
|
const userSnap = await transaction.get(userDoc)
|
|
if (!userSnap.exists) throw new APIError(400, 'User not found')
|
|
const user = userSnap.data() as User
|
|
|
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
|
const contractSnap = await transaction.get(contractDoc)
|
|
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
|
const contract = contractSnap.data() as Contract
|
|
|
|
if (user.balance < amount)
|
|
throw new APIError(400, 'Insufficient user balance')
|
|
|
|
const newCommentBountyTxn = {
|
|
fromId: user.id,
|
|
fromType: 'USER',
|
|
toId: isProd()
|
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
|
toType: 'BANK',
|
|
amount,
|
|
token: 'M$',
|
|
category: 'COMMENT_BOUNTY',
|
|
data: {
|
|
contractId,
|
|
},
|
|
description: `Deposit M$${amount} from ${user.id} for comment bounty for contract ${contractId}`,
|
|
} as CommentBountyDepositTxn
|
|
|
|
const result = await runTxn(transaction, newCommentBountyTxn)
|
|
|
|
transaction.update(
|
|
contractDoc,
|
|
removeUndefinedProps({
|
|
openCommentBounties: (contract.openCommentBounties ?? 0) + amount,
|
|
})
|
|
)
|
|
|
|
return result
|
|
})
|
|
})
|
|
export const awardcommentbounty = newEndpoint({}, async (req, auth) => {
|
|
const { amount, commentId, contractId } = validate(awardBodySchema, req.body)
|
|
|
|
if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
|
|
|
|
// run as transaction to prevent race conditions
|
|
const res = await firestore.runTransaction(async (transaction) => {
|
|
const userDoc = firestore.doc(`users/${auth.uid}`)
|
|
const userSnap = await transaction.get(userDoc)
|
|
if (!userSnap.exists) throw new APIError(400, 'User not found')
|
|
const user = userSnap.data() as User
|
|
|
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
|
const contractSnap = await transaction.get(contractDoc)
|
|
if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
|
|
const contract = contractSnap.data() as Contract
|
|
|
|
if (user.id !== contract.creatorId)
|
|
throw new APIError(
|
|
400,
|
|
'Only contract creator can award comment bounties'
|
|
)
|
|
|
|
const commentDoc = firestore.doc(
|
|
`contracts/${contractId}/comments/${commentId}`
|
|
)
|
|
const commentSnap = await transaction.get(commentDoc)
|
|
if (!commentSnap.exists) throw new APIError(400, 'Invalid comment')
|
|
|
|
const comment = commentSnap.data() as Comment
|
|
const amountAvailable = contract.openCommentBounties ?? 0
|
|
if (amountAvailable < amount)
|
|
throw new APIError(400, 'Insufficient open bounty balance')
|
|
|
|
const newCommentBountyTxn = {
|
|
fromId: isProd()
|
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
|
fromType: 'BANK',
|
|
toId: comment.userId,
|
|
toType: 'USER',
|
|
amount,
|
|
token: 'M$',
|
|
category: 'COMMENT_BOUNTY',
|
|
data: {
|
|
contractId,
|
|
commentId,
|
|
},
|
|
description: `Withdrawal M$${amount} from BANK for comment ${comment.id} bounty for contract ${contractId}`,
|
|
} as CommentBountyWithdrawalTxn
|
|
|
|
const result = await runTxn(transaction, newCommentBountyTxn)
|
|
|
|
await transaction.update(
|
|
contractDoc,
|
|
removeUndefinedProps({
|
|
openCommentBounties: amountAvailable - amount,
|
|
})
|
|
)
|
|
await transaction.update(
|
|
commentDoc,
|
|
removeUndefinedProps({
|
|
bountiesAwarded: (comment.bountiesAwarded ?? 0) + amount,
|
|
})
|
|
)
|
|
|
|
return { ...result, comment, contract, user }
|
|
})
|
|
if (res.txn?.id) {
|
|
const { comment, contract, user } = res
|
|
await createBountyNotification(
|
|
user,
|
|
comment.userId,
|
|
amount,
|
|
res.txn.id,
|
|
contract,
|
|
comment.id
|
|
)
|
|
}
|
|
|
|
return res
|
|
})
|
|
|
|
const firestore = admin.firestore()
|