Set up cloud function for writing txns
This commit is contained in:
parent
7c50c98bc5
commit
ec45dfa311
31
common/txn.ts
Normal file
31
common/txn.ts
Normal 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
|
||||||
|
}
|
|
@ -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
82
functions/src/transact.ts
Normal 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()
|
Loading…
Reference in New Issue
Block a user