Store transactions and manipulate client-side
This commit is contained in:
parent
5311718619
commit
709ddfa7a9
|
@ -52,5 +52,10 @@ service cloud.firestore {
|
|||
allow read;
|
||||
allow write: if request.auth.uid == userId;
|
||||
}
|
||||
|
||||
match /transactions/{transactionId} {
|
||||
allow read;
|
||||
allow write;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
])
|
||||
}
|
||||
|
|
11
web/hooks/use-transactions.ts
Normal file
11
web/hooks/use-transactions.ts
Normal 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
|
||||
}
|
56
web/lib/firebase/transactions.ts
Normal file
56
web/lib/firebase/transactions.ts
Normal 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)
|
||||
}
|
|
@ -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'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>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue
Block a user