Migrate claimManalink function to v2 (#628)
* Implement helpful `toString` on client `APIError` * Migrate claimManalink function to v2
This commit is contained in:
		
							parent
							
								
									d9f42caa6a
								
							
						
					
					
						commit
						fdde73710e
					
				|  | @ -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() | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
|  | @ -36,8 +36,3 @@ export const createUser: () => Promise<User | null> = () => { | |||
|     .then((r) => (r.data as any)?.user || null) | ||||
|     .catch(() => null) | ||||
| } | ||||
| 
 | ||||
| export const claimManalink = cloudFunction< | ||||
|   string, | ||||
|   { status: 'error' | 'success'; message?: string } | ||||
| >('claimManalink') | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user