Store transactions and manipulate client-side

This commit is contained in:
Austin Chen 2022-03-31 21:52:06 -07:00
parent 5311718619
commit 709ddfa7a9
5 changed files with 193 additions and 9 deletions

View File

@ -52,5 +52,10 @@ service cloud.firestore {
allow read;
allow write: if request.auth.uid == userId;
}
match /transactions/{transactionId} {
allow read;
allow write;
}
}
}

View File

@ -5,6 +5,7 @@ import { User } from '../../common/user'
import { formatMoney } from '../../common/util/format'
import { useUser } from '../hooks/use-user'
import { buyLeaderboardSlot } from '../lib/firebase/api-call'
import { Transaction, writeTransaction } from '../lib/firebase/transactions'
import { AmountInput } from './amount-input'
import { Avatar } from './avatar'
import { Col } from './layout/col'
@ -104,10 +105,7 @@ export function BuySlotModal(props: {
<>
<Modal open={open} setOpen={setOpen}>
<Col className="gap-5 rounded-md bg-white p-6 text-gray-500">
<Title
text={`Buy #${slot} on ${title}`}
className="!mt-0 !text-2xl"
/>
<Title text={`Buy slot #${slot}`} className="!mt-0" />
<Label>Current value: {formatMoney(value)}</Label>
{user && (
@ -136,7 +134,15 @@ export function BuySlotModal(props: {
label={ENV_CONFIG.moneyMoniker}
/>
<button className="btn btn-primary">
<button
className="btn btn-primary"
onClick={() => {
if (user) {
buySlot({ holder, buyer: user, amount: value, slot, message })
setOpen(false)
}
}}
>
Buy Slot ({formatMoney(value)})
</button>
<div className="-mt-2 text-sm">
@ -153,3 +159,65 @@ export function BuySlotModal(props: {
</>
)
}
async function buySlot(options: {
holder: User
buyer: User
amount: number
slot: number
message: string
}) {
const { holder, buyer, amount, slot, message } = options
const createdTime = Date.now()
const buyTransaction: Transaction = {
id: '',
createdTime,
fromId: buyer.id,
fromName: buyer.name,
fromUsername: buyer.username,
fromAvatarUrl: buyer.avatarUrl,
toId: holder.id,
toName: holder.name,
toUsername: holder.username,
toAvatarUrl: holder.avatarUrl,
amount: amount,
category: 'BUY_LEADERBOARD_SLOT',
description: `${buyer.name} bought a slot from ${holder.name}`,
data: {
slot,
message,
},
}
const feeTransaction: Transaction = {
id: '',
createdTime,
fromId: holder.id,
fromName: holder.name,
fromUsername: holder.username,
fromAvatarUrl: holder.avatarUrl,
// Send fee to Manifold Markets official account
toId: 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2',
toName: 'Manifold Markets',
toUsername: 'ManifoldMarkets',
toAvatarUrl: 'https://manifold.markets/logo-bg-white.png',
amount: 10, // TODO: Calculate fee
category: 'LEADERBOARD_TAX',
description: `${holder.name} paid M$ 10 in fees`,
data: {
slot,
},
}
await Promise.all([
writeTransaction(buyTransaction),
writeTransaction(feeTransaction),
])
}

View File

@ -0,0 +1,11 @@
import { useState, useEffect } from 'react'
import {
listenForTransactions,
Transaction,
} from '../lib/firebase/transactions'
export const useTransactions = () => {
const [transactions, setTransactions] = useState<Transaction[] | undefined>()
useEffect(() => listenForTransactions(setTransactions), [])
return transactions
}

View File

@ -0,0 +1,56 @@
import { collection, doc, query, setDoc } from 'firebase/firestore'
import { db } from './init'
import { getValues, listenForValues } from './utils'
export type Transaction = {
id: string
createdTime: number
fromId: string
fromName: string
fromUsername: string
fromAvatarUrl?: string
toId: string
toName: string
toUsername: string
toAvatarUrl?: string
amount: number
category: 'BUY_LEADERBOARD_SLOT' | 'LEADERBOARD_TAX'
// Human-readable description
description?: string
// Structured metadata for different kinds of transactions
data?: SlotData | TaxData
}
type SlotData = {
slot: number
message: string
}
type TaxData = {
slot: number
}
export async function listAllTransactions() {
const col = collection(db, 'transactions')
const transactions = await getValues<Transaction>(col)
transactions.sort((t1, t2) => t1.createdTime - t2.createdTime)
return transactions
}
export function listenForTransactions(setTxns: (txns: Transaction[]) => void) {
const col = collection(db, 'transactions')
const queryAll = query(col)
return listenForValues<Transaction>(queryAll, setTxns)
}
export async function writeTransaction(transaction: Transaction) {
const col = collection(db, 'transactions')
const newRef = doc(col)
transaction.id = newRef.id
await setDoc(newRef, transaction)
}

View File

@ -8,6 +8,8 @@ import { formatMoney } from '../../common/util/format'
import { fromPropz, usePropz } from '../hooks/use-propz'
import { Manaboard } from '../components/manaboard'
import { Title } from '../components/title'
import { useTransactions } from '../hooks/use-transactions'
import { Transaction } from '../lib/firebase/transactions'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz() {
@ -92,7 +94,12 @@ function Explanation() {
</div>
)
}
// TODOs
// [ ] Correctly calculate tax
// [ ] List history of purchases at the bottom
// [ ] Restrict to at most buying one slot per user?
// [ ] Set to 50 top traders
// [ ] Deduct amount from user's balance, either in UX or for real
export default function Manaboards(props: {
topTraders: User[]
topCreators: User[]
@ -103,9 +110,46 @@ export default function Manaboards(props: {
}
const { topTraders, topCreators } = props
// Find the most recent purchases of each slot, and replace the entries in topTraders
const transactions = useTransactions() ?? []
// Iterate from oldest to newest transactions, so recent purchases overwrite older ones
const sortedTxns = _.sortBy(transactions, 'createdTime')
for (const txn of sortedTxns) {
if (txn.category === 'BUY_LEADERBOARD_SLOT') {
const buyer = userFromBuy(txn)
const slot = txn.data?.slot ?? 0
topTraders[slot - 1] = buyer
}
}
console.log('sorted txn', sortedTxns)
function userFromBuy(txn: Transaction): User {
return {
id: txn.fromId,
// @ts-ignore
name: txn.data?.message ?? txn.fromName,
username: txn.fromUsername,
avatarUrl: txn.fromAvatarUrl,
// Dummy data which shouldn't be relied on
createdTime: 0,
creatorVolumeCached: 0,
totalPnLCached: 0,
balance: 0,
totalDeposits: 0,
}
}
return (
<Page margin rightSidebar={<Explanation />}>
<Title text={'Leaderboards (FOR SALE!)'} />
<Title text={'🏅 Leaderboards'} />
{/* <div className="absolute right-[700px] top-8">
<img
className="h-18 mx-auto w-24 object-cover transition hover:rotate-12"
src="https://i.etsystatic.com/8800089/r/il/b79fe6/1591362635/il_fullxfull.1591362635_4523.jpg"
/>
</div> */}
<div className="prose mb-8 text-gray-600">
<p>
Manafold Markets is running low on mana, so we&#39;re selling our
@ -115,8 +159,8 @@ export default function Manaboards(props: {
</div>
<Col className="mt-6 items-center gap-10">
<Manaboard title="🏅 Top traders" users={topTraders} />
<Manaboard title="🏅 Top creators" users={topCreators} />
<Manaboard title="" users={topTraders} />
{/* <Manaboard title="🏅 Top creators" users={topCreators} /> */}
</Col>
</Page>
)