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