diff --git a/functions/src/close-market.ts b/functions/src/close-market.ts new file mode 100644 index 00000000..86d73107 --- /dev/null +++ b/functions/src/close-market.ts @@ -0,0 +1,50 @@ +import * as admin from 'firebase-admin' +import { z } from 'zod' + +import { Contract } from '../../common/contract' +import { getUser } from './utils' + +import { isAdmin, isManifoldId } from '../../common/envs/constants' +import { APIError, newEndpoint, validate } from './api' + +const bodySchema = z.object({ + contractId: z.string(), +}) + +export const closemarket = newEndpoint({}, async (req, auth) => { + const { contractId } = validate(bodySchema, req.body) + const contractDoc = firestore.doc(`contracts/${contractId}`) + const contractSnap = await contractDoc.get() + if (!contractSnap.exists) + throw new APIError(404, 'No contract exists with the provided ID') + const contract = contractSnap.data() as Contract + const { creatorId, closeTime } = contract + const firebaseUser = await admin.auth().getUser(auth.uid) + + if ( + creatorId !== auth.uid && + !isManifoldId(auth.uid) && + !isAdmin(firebaseUser.email) + ) + throw new APIError(403, 'User is not creator of contract') + + const now = Date.now() + if (closeTime && closeTime < now) + throw new APIError(400, 'Contract already closed') + + const creator = await getUser(creatorId) + if (!creator) throw new APIError(500, 'Creator not found') + + const updatedContract = { + ...contract, + closeTime: now, + } + + await contractDoc.update(updatedContract) + + console.log('contract ', contractId, 'closed') + + return updatedContract +}) + +const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index adfee75e..2cb6f515 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -50,6 +50,7 @@ export * from './resolve-market' export * from './unsubscribe' export * from './stripe' export * from './mana-bonus-email' +export * from './close-market' import { health } from './health' import { transact } from './transact' @@ -66,6 +67,7 @@ import { addliquidity } from './add-liquidity' import { withdrawliquidity } from './withdraw-liquidity' import { creategroup } from './create-group' import { resolvemarket } from './resolve-market' +import { closemarket } from './close-market' import { unsubscribe } from './unsubscribe' import { stripewebhook, createcheckoutsession } from './stripe' import { getcurrentuser } from './get-current-user' @@ -91,6 +93,7 @@ const addLiquidityFunction = toCloudFunction(addliquidity) const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity) const createGroupFunction = toCloudFunction(creategroup) const resolveMarketFunction = toCloudFunction(resolvemarket) +const closeMarketFunction = toCloudFunction(closemarket) const unsubscribeFunction = toCloudFunction(unsubscribe) const stripeWebhookFunction = toCloudFunction(stripewebhook) const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession) @@ -115,11 +118,12 @@ export { withdrawLiquidityFunction as withdrawliquidity, createGroupFunction as creategroup, resolveMarketFunction as resolvemarket, + closeMarketFunction as closemarket, unsubscribeFunction as unsubscribe, stripeWebhookFunction as stripewebhook, createCheckoutSessionFunction as createcheckoutsession, getCurrentUserFunction as getcurrentuser, acceptChallenge as acceptchallenge, createPostFunction as createpost, - saveTwitchCredentials as savetwitchcredentials + saveTwitchCredentials as savetwitchcredentials, } diff --git a/web/next.config.js b/web/next.config.js index cf727fd4..21b375ba 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -9,6 +9,9 @@ module.exports = { reactStrictMode: true, optimizeFonts: false, experimental: { + images: { + allowFutureImage: true, + }, scrollRestoration: true, externalDir: true, modularizeImports: { diff --git a/web/pages/api/v0/market/[id]/close.ts b/web/pages/api/v0/market/[id]/close.ts new file mode 100644 index 00000000..d1c9ac5e --- /dev/null +++ b/web/pages/api/v0/market/[id]/close.ts @@ -0,0 +1,28 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' + +export const config = { api: { bodyParser: true } } + +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + + const { id } = req.query + const contractId = id as string + + if (req.body) req.body.contractId = contractId + try { + const backendRes = await fetchBackend(req, 'closemarket') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +}