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