manifold/functions/src/transact.ts
Austin Chen 6cc2d8af58
Manalink: Send mana to anyone via link (#114)
* Set up Firestore structure for mana bounty links

* Split up manalinks into successes and failures

* Allow clients to create manalinks

* Track txnId and successful users

* Store custom amounts in the link

* List all manalinks you've created

* Support backend for claiming manalinks

* Add some more error handling

* Tweak readme

* Fix typescript breakage

* Revert "Convert common imports in functions to be absolute"

This reverts commit c03518e906.

* Scaffolding so `claimManalink` works

* Clean up imports

* Barebones endpoint to claim mana

* Fix rules to only allow link creators to query

* Design out claim giftcard

* List all claimed transactions

* Style in a more awesome card

* Fix import

* Padding tweak

* Fix useManalinkTxns hook

* /send -> /link

* Tidy up some details

* Do a bunch of random manalinks work

* Fix up LinksTable to build

* Clean up LinksTable an absurd amount

* Basic details functionality on manalinks table

* Work on manalink claim stuff

* Fix up some merge mess

* Not-signed-in flow implemented

* Better manalinks table

* Only show outstanding links in table

* Use new `ManalinkTxn` type

* /link -> /links

* Change manalinks page UI to use nice looking tabs

* Many fixes to manalinks UI

* Default to 1 use

* Tidying up

* Some copy changes based on feedback

* Add required index

Co-authored-by: Marshall Polaris <marshall@pol.rs>
2022-06-23 01:07:52 -07:00

85 lines
2.5 KiB
TypeScript

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { User } from '../../common/user'
import { Txn } from '../../common/txn'
import { removeUndefinedProps } from '../../common/util/object'
export type TxnData = Omit<Txn, 'id' | 'createdTime'>
export const transact = functions
.runWith({ minInstances: 1 })
.https.onCall(async (data: TxnData, context) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
const { amount, fromType, fromId } = data
if (fromType !== 'USER')
return {
status: 'error',
message: "From type is only implemented for type 'user'.",
}
if (fromId !== userId)
return {
status: 'error',
message: 'Must be authenticated with userId equal to specified fromId.',
}
if (isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' }
// Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => {
await runTxn(transaction, data)
})
})
export async function runTxn(
fbTransaction: admin.firestore.Transaction,
data: TxnData
) {
const { amount, fromId, toId, toType } = data
const fromDoc = firestore.doc(`users/${fromId}`)
const fromSnap = await fbTransaction.get(fromDoc)
if (!fromSnap.exists) {
return { status: 'error', message: 'User not found' }
}
const fromUser = fromSnap.data() as User
if (fromUser.balance < amount) {
return {
status: 'error',
message: `Insufficient balance: ${fromUser.username} needed ${amount} but only had ${fromUser.balance} `,
}
}
// TODO: Track payments received by charities, bank, contracts too.
if (toType === 'USER') {
const toDoc = firestore.doc(`users/${toId}`)
const toSnap = await fbTransaction.get(toDoc)
if (!toSnap.exists) {
return { status: 'error', message: 'User not found' }
}
const toUser = toSnap.data() as User
fbTransaction.update(toDoc, {
balance: toUser.balance + amount,
totalDeposits: toUser.totalDeposits + amount,
})
}
const newTxnDoc = firestore.collection(`txns/`).doc()
const txn = { id: newTxnDoc.id, createdTime: Date.now(), ...data }
fbTransaction.create(newTxnDoc, removeUndefinedProps(txn))
fbTransaction.update(fromDoc, {
balance: fromUser.balance - amount,
totalDeposits: fromUser.totalDeposits - amount,
})
return { status: 'success', txn }
}
const firestore = admin.firestore()