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 read; | ||||||
|       allow write: if request.auth.uid == userId; |       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 { formatMoney } from '../../common/util/format' | ||||||
| import { useUser } from '../hooks/use-user' | import { useUser } from '../hooks/use-user' | ||||||
| import { buyLeaderboardSlot } from '../lib/firebase/api-call' | import { buyLeaderboardSlot } from '../lib/firebase/api-call' | ||||||
|  | import { Transaction, writeTransaction } from '../lib/firebase/transactions' | ||||||
| import { AmountInput } from './amount-input' | import { AmountInput } from './amount-input' | ||||||
| import { Avatar } from './avatar' | import { Avatar } from './avatar' | ||||||
| import { Col } from './layout/col' | import { Col } from './layout/col' | ||||||
|  | @ -104,10 +105,7 @@ export function BuySlotModal(props: { | ||||||
|     <> |     <> | ||||||
|       <Modal open={open} setOpen={setOpen}> |       <Modal open={open} setOpen={setOpen}> | ||||||
|         <Col className="gap-5 rounded-md bg-white p-6 text-gray-500"> |         <Col className="gap-5 rounded-md bg-white p-6 text-gray-500"> | ||||||
|           <Title |           <Title text={`Buy slot #${slot}`} className="!mt-0" /> | ||||||
|             text={`Buy #${slot} on ${title}`} |  | ||||||
|             className="!mt-0 !text-2xl" |  | ||||||
|           /> |  | ||||||
| 
 | 
 | ||||||
|           <Label>Current value: {formatMoney(value)}</Label> |           <Label>Current value: {formatMoney(value)}</Label> | ||||||
|           {user && ( |           {user && ( | ||||||
|  | @ -136,7 +134,15 @@ export function BuySlotModal(props: { | ||||||
|             label={ENV_CONFIG.moneyMoniker} |             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)}) |             Buy Slot ({formatMoney(value)}) | ||||||
|           </button> |           </button> | ||||||
|           <div className="-mt-2 text-sm"> |           <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 { fromPropz, usePropz } from '../hooks/use-propz' | ||||||
| import { Manaboard } from '../components/manaboard' | import { Manaboard } from '../components/manaboard' | ||||||
| import { Title } from '../components/title' | import { Title } from '../components/title' | ||||||
|  | import { useTransactions } from '../hooks/use-transactions' | ||||||
|  | import { Transaction } from '../lib/firebase/transactions' | ||||||
| 
 | 
 | ||||||
| export const getStaticProps = fromPropz(getStaticPropz) | export const getStaticProps = fromPropz(getStaticPropz) | ||||||
| export async function getStaticPropz() { | export async function getStaticPropz() { | ||||||
|  | @ -92,7 +94,12 @@ function Explanation() { | ||||||
|     </div> |     </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: { | export default function Manaboards(props: { | ||||||
|   topTraders: User[] |   topTraders: User[] | ||||||
|   topCreators: User[] |   topCreators: User[] | ||||||
|  | @ -103,9 +110,46 @@ export default function Manaboards(props: { | ||||||
|   } |   } | ||||||
|   const { topTraders, topCreators } = 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 ( |   return ( | ||||||
|     <Page margin rightSidebar={<Explanation />}> |     <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"> |       <div className="prose mb-8 text-gray-600"> | ||||||
|         <p> |         <p> | ||||||
|           Manafold Markets is running low on mana, so we're selling our |           Manafold Markets is running low on mana, so we're selling our | ||||||
|  | @ -115,8 +159,8 @@ export default function Manaboards(props: { | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <Col className="mt-6 items-center gap-10"> |       <Col className="mt-6 items-center gap-10"> | ||||||
|         <Manaboard title="🏅 Top traders" users={topTraders} /> |         <Manaboard title="" users={topTraders} /> | ||||||
|         <Manaboard title="🏅 Top creators" users={topCreators} /> |         {/* <Manaboard title="🏅 Top creators" users={topCreators} /> */} | ||||||
|       </Col> |       </Col> | ||||||
|     </Page> |     </Page> | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user