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