6cc2d8af58
* 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>
85 lines
2.5 KiB
TypeScript
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()
|