Set up cloud function for writing txns

This commit is contained in:
Austin Chen 2022-04-21 04:09:31 -07:00
parent 7c50c98bc5
commit ec45dfa311
3 changed files with 117 additions and 0 deletions

31
common/txn.ts Normal file
View File

@ -0,0 +1,31 @@
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
export type Txn = {
id: string
createdTime: number
fromId: string
fromName: string
fromUsername: string
fromAvatarUrl?: string
toId: string
toName: string
toUsername: string
toAvatarUrl?: string
amount: number
category: TxnCategory
// Human-readable description
description?: string
// Structured metadata for different kinds of txns
data?: TxnData
}
export type TxnCategory = 'TO_CHARITY' // | 'TIP' | 'BET' | ...
export type TxnData = CharityData // | TipData | BetData | ...
export type CharityData = {
// TODO: Could fill this in
}

View File

@ -64,5 +64,9 @@ service cloud.firestore {
allow read; allow read;
allow write: if request.auth.uid == userId; allow write: if request.auth.uid == userId;
} }
match /txns/{txnId} {
allow read;
}
} }
} }

82
functions/src/transact.ts Normal file
View File

@ -0,0 +1,82 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { User } from '../../common/user'
import { Txn, TxnCategory, TxnData } from '../../common/txn'
export const transact = functions.runWith({ minInstances: 1 }).https.onCall(
async (
data: {
amount: number
toId: string
category: TxnCategory
description?: string
txnData?: TxnData
},
context
) => {
const fromId = context?.auth?.uid
if (!fromId) return { status: 'error', message: 'Not authorized' }
const { amount, toId, category, description, txnData } = data
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' }
// run as transaction to prevent race conditions
return await firestore.runTransaction(async (transaction) => {
const fromDoc = firestore.doc(`users/${fromId}`)
const fromSnap = await transaction.get(fromDoc)
if (!fromSnap.exists) {
return { status: 'error', message: 'User not found' }
}
const fromUser = fromSnap.data() as User
const toDoc = firestore.doc(`users/${toId}`)
const toSnap = await transaction.get(toDoc)
if (!toSnap.exists) {
return { status: 'error', message: 'User not found' }
}
const toUser = toSnap.data() as User
if (fromUser.balance < amount) {
return {
status: 'error',
message: `Insufficient balance: ${fromUser.username} needed ${amount} but only had ${fromUser.balance} `,
}
}
const newTxnDoc = firestore.collection(`txns/`).doc()
const txn: Txn = {
id: newTxnDoc.id,
// @ts-ignore - this is a firestore doc
createdTime: admin.firestore.FieldValue.serverTimestamp(),
fromId,
fromName: fromUser.name,
fromUsername: fromUser.username,
fromAvatarUrl: fromUser.avatarUrl,
toId,
toName: toUser.name,
toUsername: toUser.username,
toAvatarUrl: toUser.avatarUrl,
amount,
category,
description,
data: txnData,
}
transaction.create(newTxnDoc, txn)
transaction.update(fromDoc, { balance: fromUser.balance - amount })
transaction.update(toDoc, { balance: toUser.balance + amount })
return { status: 'success', txnId: newTxnDoc.id }
})
}
)
const firestore = admin.firestore()