diff --git a/functions/src/claim-manalink.ts b/functions/src/claim-manalink.ts index 4bcd8b16..3822bbf7 100644 --- a/functions/src/claim-manalink.ts +++ b/functions/src/claim-manalink.ts @@ -1,102 +1,104 @@ -import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' +import { z } from 'zod' import { User } from 'common/user' import { Manalink } from 'common/manalink' import { runTxn, TxnData } from './transact' +import { APIError, newEndpoint, validate } from './api' -export const claimManalink = functions - .runWith({ minInstances: 1 }) - .https.onCall(async (slug: string, context) => { - const userId = context?.auth?.uid - if (!userId) return { status: 'error', message: 'Not authorized' } +const bodySchema = z.object({ + slug: z.string(), +}) - // Run as transaction to prevent race conditions. - return await firestore.runTransaction(async (transaction) => { - // Look up the manalink - const manalinkDoc = firestore.doc(`manalinks/${slug}`) - const manalinkSnap = await transaction.get(manalinkDoc) - if (!manalinkSnap.exists) { - return { status: 'error', message: 'Manalink not found' } - } - const manalink = manalinkSnap.data() as Manalink +export const claimmanalink = newEndpoint({}, async (req, auth) => { + const { slug } = validate(bodySchema, req.body) - const { amount, fromId, claimedUserIds } = manalink + // Run as transaction to prevent race conditions. + return await firestore.runTransaction(async (transaction) => { + // Look up the manalink + const manalinkDoc = firestore.doc(`manalinks/${slug}`) + const manalinkSnap = await transaction.get(manalinkDoc) + if (!manalinkSnap.exists) { + throw new APIError(400, 'Manalink not found') + } + const manalink = manalinkSnap.data() as Manalink - if (amount <= 0 || isNaN(amount) || !isFinite(amount)) - return { status: 'error', message: 'Invalid amount' } + const { amount, fromId, claimedUserIds } = manalink - const fromDoc = firestore.doc(`users/${fromId}`) - const fromSnap = await transaction.get(fromDoc) - if (!fromSnap.exists) { - return { status: 'error', message: `User ${fromId} not found` } - } - const fromUser = fromSnap.data() as User + if (amount <= 0 || isNaN(amount) || !isFinite(amount)) + throw new APIError(500, 'Invalid amount') - // Only permit one redemption per user per link - if (claimedUserIds.includes(userId)) { - return { - status: 'error', - message: `${fromUser.name} already redeemed manalink ${slug}`, - } - } + const fromDoc = firestore.doc(`users/${fromId}`) + const fromSnap = await transaction.get(fromDoc) + if (!fromSnap.exists) { + throw new APIError(500, `User ${fromId} not found`) + } + const fromUser = fromSnap.data() as User - // Disallow expired or maxed out links - if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) { - return { - status: 'error', - message: `Manalink ${slug} expired on ${new Date( - manalink.expiresTime - ).toLocaleString()}`, - } - } - if ( - manalink.maxUses != null && - manalink.maxUses <= manalink.claims.length - ) { - return { - status: 'error', - message: `Manalink ${slug} has reached its max uses of ${manalink.maxUses}`, - } - } + // Only permit one redemption per user per link + if (claimedUserIds.includes(auth.uid)) { + throw new APIError(400, `You already redeemed manalink ${slug}`) + } - if (fromUser.balance < amount) { - return { - status: 'error', - message: `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `, - } - } + // Disallow expired or maxed out links + if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) { + throw new APIError( + 400, + `Manalink ${slug} expired on ${new Date( + manalink.expiresTime + ).toLocaleString()}` + ) + } + if ( + manalink.maxUses != null && + manalink.maxUses <= manalink.claims.length + ) { + throw new APIError( + 400, + `Manalink ${slug} has reached its max uses of ${manalink.maxUses}` + ) + } - // Actually execute the txn - const data: TxnData = { - fromId, - fromType: 'USER', - toId: userId, - toType: 'USER', - amount, - token: 'M$', - category: 'MANALINK', - description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${userId}`, - } - const result = await runTxn(transaction, data) - const txnId = result.txn?.id - if (!txnId) { - return { status: 'error', message: result.message } - } + if (fromUser.balance < amount) { + throw new APIError( + 400, + `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} ` + ) + } - // Update the manalink object with this info - const claim = { - toId: userId, - txnId, - claimedTime: Date.now(), - } - transaction.update(manalinkDoc, { - claimedUserIds: [...claimedUserIds, userId], - claims: [...manalink.claims, claim], - }) + // Actually execute the txn + const data: TxnData = { + fromId, + fromType: 'USER', + toId: auth.uid, + toType: 'USER', + amount, + token: 'M$', + category: 'MANALINK', + description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${auth.uid}`, + } + const result = await runTxn(transaction, data) + const txnId = result.txn?.id + if (!txnId) { + throw new APIError( + 500, + result.message ?? 'An error occurred posting the transaction.' + ) + } - return { status: 'success', message: 'Manalink claimed' } + // Update the manalink object with this info + const claim = { + toId: auth.uid, + txnId, + claimedTime: Date.now(), + } + transaction.update(manalinkDoc, { + claimedUserIds: [...claimedUserIds, auth.uid], + claims: [...manalink.claims, claim], }) + + return { message: 'Manalink claimed' } }) +}) const firestore = admin.firestore() diff --git a/functions/src/index.ts b/functions/src/index.ts index 2800bb7d..7c839396 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -3,7 +3,6 @@ import * as admin from 'firebase-admin' admin.initializeApp() // v1 -export * from './claim-manalink' export * from './transact' export * from './stripe' export * from './create-user' @@ -34,6 +33,7 @@ export * from './change-user-info' export * from './place-bet' export * from './sell-bet' export * from './sell-shares' +export * from './claim-manalink' export * from './create-contract' export * from './add-liquidity' export * from './withdraw-liquidity' diff --git a/web/lib/firebase/api-call.ts b/web/lib/firebase/api-call.ts index d169ea72..04c6b7ce 100644 --- a/web/lib/firebase/api-call.ts +++ b/web/lib/firebase/api-call.ts @@ -10,6 +10,9 @@ export class APIError extends Error { this.name = 'APIError' this.details = details } + toString() { + return this.name + } } export async function call(url: string, method: string, params: any) { @@ -82,6 +85,10 @@ export function sellBet(params: any) { return call(getFunctionUrl('sellbet'), 'POST', params) } +export function claimManalink(params: any) { + return call(getFunctionUrl('claimmanalink'), 'POST', params) +} + export function createGroup(params: any) { return call(getFunctionUrl('creategroup'), 'POST', params) } diff --git a/web/lib/firebase/fn-call.ts b/web/lib/firebase/fn-call.ts index 3b16af70..6867b5bb 100644 --- a/web/lib/firebase/fn-call.ts +++ b/web/lib/firebase/fn-call.ts @@ -36,8 +36,3 @@ export const createUser: () => Promise = () => { .then((r) => (r.data as any)?.user || null) .catch(() => null) } - -export const claimManalink = cloudFunction< - string, - { status: 'error' | 'success'; message?: string } ->('claimManalink') diff --git a/web/pages/link/[slug].tsx b/web/pages/link/[slug].tsx index eed68e1a..b36a9057 100644 --- a/web/pages/link/[slug].tsx +++ b/web/pages/link/[slug].tsx @@ -2,7 +2,7 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { SEO } from 'web/components/SEO' import { Title } from 'web/components/title' -import { claimManalink } from 'web/lib/firebase/fn-call' +import { claimManalink } from 'web/lib/firebase/api-call' import { useManalink } from 'web/lib/firebase/manalinks' import { ManalinkCard } from 'web/components/manalink-card' import { useUser } from 'web/hooks/use-user' @@ -42,10 +42,7 @@ export default function ClaimPage() { if (user == null) { await firebaseLogin() } - const result = await claimManalink(manalink.slug) - if (result.data.status == 'error') { - throw new Error(result.data.message) - } + await claimManalink({ slug: manalink.slug }) user && router.push(`/${user.username}?claimed-mana=yes`) } catch (e) { console.log(e)