Migrate claimManalink function to v2 (#628)

* Implement helpful `toString` on client `APIError`

* Migrate claimManalink function to v2
This commit is contained in:
Marshall Polaris 2022-07-08 15:28:04 -07:00 committed by GitHub
parent d9f42caa6a
commit fdde73710e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 94 additions and 93 deletions

View File

@ -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' }
}) })
}) })

View File

@ -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'

View File

@ -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)
} }

View File

@ -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')

View File

@ -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)