IZEVQF627FA7VV25KJAWYWGGC35LZUUBBQRPN5ZAINTQLPEDRTEAC
JKMHA2QGDSVHD4DKDYQUYNJJ3LUQCOPOWEC3543BDWDXLYIBBZXQC
NJZ3DKZYZTAEHPAEXS3XFWIPCJFR3D4642UQMGQABVFGNKQUEQVAC
MK7ODWHUEPOMAEVK5D3OWW25QRAXKDO4ZVJKNZGWKB5434MP2LNAC
75N3UJ4JK56KXF56GASGPAWLFYGJDETVJNYTF4KXFCQM767JUU5AC
WO2MINIF4TXOHWSE7JWXRZYN64XRVLYIRFMF4SMPSOXKA2V77KMQC
LAROLAYUGJ4Q5AEFV5EJMIA2ZKBNCBWHHHPCJ3CKCNIUIYUKRFVQC
TLQ72DSJD7GGPWN6HGBHAVPBRQFKEQ6KSK43U7JWWID4ZWAF47JAC
5W5M56VJFJEBXMGBVKGCKPHOEMVTKUOQMLPJP7VNDQLTYNJXXLHQC
EMVTF2IWNQGRL44FC4JNG5FYYQTZSFPNM6SOM7IAEH6T7PPK2NVAC
64C6AWH66FDKU6UE6Z6JPX2J2GBM2JOPTH2GL6LHKAIUBGNGDZ5AC
LUM4VQJIHJKQWWD5NVWTVSNPKQTMGQQICTFOTM6W4BMME2G3G5RQC
MWUPXTBF2LATVOJLJTXSDFB3OMFGMXDNETWJA3JHUOUBTUJ7WJAAC
TZQJVHBAMDNWDBYCDE3SDVGBG2T5FOE3J5JAD6NENRW36XBHUUFQC
WFZDMVUXZ2KPTMRAZGEYHKEJTKOKWVYCXKKAKQ7K6I5TMSLBUJ4QC
GKGVYBZGPJXO7N7GLHLRNYQPXFHBQSNQN53OKRFCXLQEYDTC5I4QC
NVOCQVASZWTKQJG7GPH7KHKZZR7NUG4WLV5YY4KAIRPCJRWCZPIAC
7DBNV3GV773FH5ZLQWFX4RBOS4Q3CIK2RYZNNABY3ZOETYZCXRNQC
OBFPJS2GHO2PEHBHGEHKIUOUAFIQHPIZXEVD2YIE3ZIE2PVMH5VAC
WZUHEZSBRKHQMNWDKVG4X6DDIQEAXTGI6IGAJ5ERPRQ3W2KUMX4QC
TCOAKCGGHOIRJCTZYEZQ3K6KCGL2LGAYGYFRGSPCHBTJJY2V6AXAC
ADMKQQGCGVSHHIMVQ4XFRDCG544SBJCYALSKZV45CQQBZ4ACUH2AC
7XN3I3QJHYMKU2DCUXX34WQMSJ4ZJOWW7FME34EANO3E5W4Q632AC
2Y2QZFVFSKXEFEGYJB5A7GI735ONWPCF7DVTIY5T73AUEVTZTBBQC
64VI73NPSFNWTL6UXM6YHRFLNJZ3NUJ2R3CL53MO2HSZWFGBIRTQC
W35DDBFYF6Z4ZPCFEO5RPAONZLYCSNTXUSTS6FIUUVZHCI6Q7GHAC
RSEB2NFGUBTFESE5BJKDUVQL5Y5ZVGY5O4CJX2LNP63MS3NRHHZQC
Z3M53KTLZMPOISMHE25SZJSWX5TA37IV33IRE7KNRAD3PKEAEJXQC
| total < auction ^. raiseAmount =
let remainder = (auction ^. raiseAmount) ++ invert total
winFraction :: Rational
winFraction = (toRational $ remainder ^. satoshis) / (toRational $ x ^. (bidAmount . satoshis))
| total < (auction ^. raiseAmount) =
let remainder = (auction ^. raiseAmount) - total
winFraction = toRational $ remainder / (x ^. bidAmount)
{-# LANGUAGE ScopedTypeVariables, OverloadedStrings, NoImplicitPrelude #-}
{-# LANGUAGE TemplateHaskell #-}
module Quixotic.Database.PostgreSQL (postgresQDB) where
import Blaze.ByteString.Builder (fromByteString)
import ClassyPrelude
import Control.Lens
import Data.Fixed
import Data.Hourglass
import qualified Data.List as DL
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.ToField
import Database.PostgreSQL.Simple.FromField
import Database.PostgreSQL.Simple.ToRow
import Database.PostgreSQL.Simple.FromRow
import Network.Bitcoin
import Quixotic
import Quixotic.Auction
import Quixotic.Database
import Quixotic.TimeLog
import Quixotic.Users
eventTypeParser :: FieldParser EventType
eventTypeParser f v = fromField f v >>= nameEvent
uidParser :: FieldParser UserId
uidParser f v = UserId <$> fromField f v
secondsParser :: FieldParser Seconds
secondsParser f v = Seconds <$> fromField f v
usernameParser :: FieldParser UserName
usernameParser f v = UserName <$> fromField f v
btcAddrParser :: FieldParser BtcAddr
btcAddrParser f v = BtcAddr <$> fromField f v
btcParser :: FieldParser BTC
btcParser f v = fromField f v
workEventParser :: RowParser WorkEvent
workEventParser = WorkEvent <$> fieldWith eventTypeParser <*> field
logEntryParser :: RowParser LogEntry
logEntryParser = LogEntry <$> fieldWith btcAddrParser <*> workEventParser
auctionRowParser :: RowParser Auction
auctionRowParser = Auction <$> fieldWith btcParser <*> field
bidRowParser :: RowParser Bid
bidRowParser = Bid <$> fieldWith uidParser
<*> fieldWith secondsParser
<*> fieldWith btcParser
<*> field
userRowParser :: RowParser User
userRowParser = User <$> fieldWith usernameParser
<*> fieldWith btcAddrParser
<*> field
-- Local newtypes to permit field serialization
newtype PBTC = PBTC { pBTC :: BTC }
instance ToField PBTC where
toField (PBTC btc) = Plain . fromByteString . fromString $ showFixed False btc
-- Local newtypes to permit row deserialization via
-- typeclass. Wish I could just pass the RowParser instances
newtype PLogEntry = PLogEntry { pLogEntry :: LogEntry }
instance FromRow PLogEntry where
fromRow = PLogEntry <$> logEntryParser
newtype PBid = PBid { pBid :: Bid }
instance FromRow PBid where
fromRow = PBid <$> bidRowParser
newtype PUser = PUser { pUser :: User }
instance FromRow PUser where
fromRow = PUser <$> userRowParser
newtype PAuction = PAuction { pAuction :: Auction }
instance FromRow PAuction where
fromRow = PAuction <$> auctionRowParser
recordEvent' :: UserId -> LogEntry -> ReaderT Connection IO ()
recordEvent' (UserId uid) (LogEntry a e) = do
conn <- ask
void . lift $ execute conn
"INSERT INTO work_events (user_id, btc_addr, event_type, event_time) VALUES (?, ?, ?, ?)"
( uid
, a ^. address
, e ^. (eventType . to eventName)
, e ^. eventTime
)
readWorkIndex' :: ReaderT Connection IO WorkIndex
readWorkIndex' = do
conn <- ask
rows <- lift $ query_ conn
"SELECT btc_addr, event_type, event_time from work_events"
pure . workIndex $ fmap pLogEntry rows
newAuction' :: Auction -> ReaderT Connection IO AuctionId
newAuction' auc = do
conn <- ask
aucIds <- lift $ query conn
"INSERT INTO auctions (raise_amount, end_time) VALUES (?, ?) RETURNING id"
(auc ^. (raiseAmount.to PBTC), auc ^. auctionEnd)
pure . AuctionId . fromOnly $ DL.head aucIds
readAuction' :: AuctionId -> ReaderT Connection IO (Maybe Auction)
readAuction' aucId = do
conn <- ask
rows <- lift $ query conn
"SELECT raise_amount, end_time FROM auctions WHERE ROWID = ?"
(Only (aucId ^. _AuctionId))
pure . fmap pAuction $ headMay rows
recordBid' :: AuctionId -> Bid -> ReaderT Connection IO ()
recordBid' (AuctionId aucId) bid = do
conn <- ask
void . lift $ execute conn
"INSERT INTO bids (auction_id, user_id, bid_seconds, bid_amount, bid_time) values (?, ?, ?, ?, ?)"
( aucId
, bid ^. (bidUser._UserId)
, case bid ^. bidSeconds of (Seconds i) -> i
, bid ^. (bidAmount.to PBTC)
, bid ^. bidTime
)
readBids' :: AuctionId -> ReaderT Connection IO [Bid]
readBids' aucId = do
conn <- ask
rows <- lift $ query conn
"SELECT user_id, bid_seconds, bid_amount, bid_time FROM bids WHERE auction_id = ?"
(Only $ (aucId ^. _AuctionId))
pure $ fmap pBid rows
createUser' :: User -> ReaderT Connection IO UserId
createUser' user = do
conn <- ask
uids <- lift $ query conn
"INSERT INTO users (handle, btc_addr, email) VALUES (?, ?) RETURNING id"
(user ^. (username._UserName), user ^. (userAddress.address), user ^. userEmail)
pure . UserId . fromOnly $ DL.head uids
findUser' :: UserId -> ReaderT Connection IO (Maybe User)
findUser' (UserId uid) = do
conn <- ask
users <- lift $ query conn
"SELECT handle, btc_addr, email FROM users WHERE id = ?"
(Only uid)
pure . fmap pUser $ headMay users
findUserByHandle' :: Handle -> ReaderT Connection IO (Maybe QDBUser)
findUserByHandle' = undefined
postgresQDB :: QDB (ReaderT Connection IO)
postgresQDB = QDB
{ recordEvent = recordEvent'
, readWorkIndex = readWorkIndex'
, newAuction = newAuction'
, readAuction = readAuction'
, recordBid = recordBid'
, readBids = readBids'
, createUser = createUser'
, findUser = findUser'
, findUserByHandle = findUserByHandle'
}
instance ToField PBTC where
toField (PBTC (BTC i)) = toField i
recordEvent' :: LogEntry -> ReaderT Connection IO ()
recordEvent' logEntry = do
-- TODO: Record the user id
recordEvent' :: UserId -> LogEntry -> ReaderT Connection IO ()
recordEvent' _ logEntry = do
data QDB m a = QDB
{ recordEvent :: LogEntry -> ReaderT a m ()
, readWorkIndex :: ReaderT a m WorkIndex
, newAuction :: Auction -> ReaderT a m AuctionId
, readAuction :: AuctionId -> ReaderT a m (Maybe Auction)
, recordBid :: AuctionId -> Bid -> ReaderT a m ()
, readBids :: AuctionId -> ReaderT a m [Bid]
, createUser :: User -> ReaderT a m UserId
data QDBUser = QDBUser
{ _userId :: UserId
, _user :: User
}
makeLenses ''QDBUser
data QDB m = QDB
{ recordEvent :: UserId -> LogEntry -> m ()
, readWorkIndex :: m WorkIndex
, newAuction :: Auction -> m AuctionId
, readAuction :: AuctionId -> m (Maybe Auction)
, recordBid :: AuctionId -> Bid -> m ()
, readBids :: AuctionId -> m [Bid]
, createUser :: User -> m UserId
, findUser :: UserId -> m (Maybe User)
, findUserByHandle :: Handle -> m (Maybe QDBUser)
newtype BTC = BTC { _satoshis :: Int64 } deriving (Show, Eq, Ord)
makeLenses ''BTC
instance Semigroup BTC where
(<>) (BTC b1) (BTC b2) = BTC $ b1 + b2
instance Monoid BTC where
mempty = BTC 0
mappend = (<>)
instance Group BTC where
invert (BTC b) = BTC . negate $ b
instance Abelian BTC where
main :: IO ()
main = do
cfg <- loadQConfig "quixotic.cfg"
db <- open $ dbName cfg
sconf <- snapConfig cfg
simpleHttpServe sconf $ runReaderT (site sqliteQDB) db
site :: QDB IO a -> ReaderT a Snap ()
site qdb = route
[ ("logStart/:btcAddr", logWorkHandler qdb StartWork)
, ("logEnd/:btcAddr", logWorkHandler qdb StopWork)
, ("loggedIntervals/:btcAddr", loggedIntervalsHandler qdb)
, ("payouts", payoutsHandler qdb)
]
main :: IO ()
main = do
cfg <- loadQConfig "quixotic.cfg"
sconf <- snapConfig cfg
--simpleHttpServe sconf $ runReaderT (site sqliteQDB) db
serveSnaplet sconf $ appInit cfg
appInit :: QConfig -> SnapletInit App App
appInit QConfig{..} = makeSnaplet "quixotic" "Quixotic Time Tracker" Nothing $ do
qdbs <- nestSnaplet "qdb" qdb qdbpgSnapletInit
sesss <- nestSnaplet "sessions" sess $
initCookieSessionManager (fpToString authSiteKey) "quookie" cookieTimeout
pgs <- nestSnaplet "db" db pgsInit
auths <- nestSnaplet "auth" auth $ initPostgresAuth sess pgs
addRoutes [ ("logStart/:btcAddr", logWorkHandler StartWork)
, ("logEnd/:btcAddr", logWorkHandler StopWork)
-- , ("loggedIntervals/:btcAddr", loggedIntervalsHandler qdb)
-- , ("payouts", payoutsHandler qdb)
]
return $ App qdbs sesss pgs auths
logWorkHandler :: QDB IO a -> EventType -> ReaderT a Snap ()
logWorkHandler qdb ev = do
let QDB{..} = qdb
addrBytes <- lift $ getParam "btcAddr"
timestamp <- lift $ liftIO getCurrentTime
maybe
(lift $ snapError 400 "")
(\a -> mapReaderT liftIO $ recordEvent (LogEntry a (WorkEvent ev timestamp)))
(fmap decodeUtf8 addrBytes >>= parseBtcAddr)
qdbpgSnapletInit :: SnapletInit a PQDB
qdbpgSnapletInit = makeSnaplet "qdbpg" "QDB on Postgresql" Nothing $ do
return postgresQDB
loggedIntervalsHandler :: QDB IO a -> ReaderT a Snap ()
loggedIntervalsHandler qdb = do
let QDB{..} = qdb
widx <- mapReaderT liftIO $ readWorkIndex
lift . modifyResponse $ addHeader "content-type" "application/json"
lift . writeLBS . A.encode $ mapKeys (^. address) widx
logWorkHandler :: EventType -> Handler App App ()
logWorkHandler evType = do
QDB{..} <- with qdb mempty
pg <- with db getPostgresState
authedUser <- with auth currentUser
qUid <-
addrBytes <- getParam "btcAddr"
timestamp <- liftIO getCurrentTime
let workEvent = WorkEvent evType timestamp
btcAddr = fmap decodeUtf8 addrBytes >>= parseBtcAddr
storeEv uid addr = runReaderT . recordEvent uid $ LogEntry addr workEvent
maybe (snapError 400 "") (liftPG . storeEv) btcAddr
payoutsHandler :: QDB IO a -> ReaderT a Snap ()
payoutsHandler qdb = do
let QDB{..} = qdb
dep = linearDepreciation (Months 6) (Months 60)
ptime <- lift . liftIO $ getCurrentTime
widx <- mapReaderT liftIO $ readWorkIndex
lift . modifyResponse $ addHeader "content-type" "application/json"
lift . writeLBS . A.encode . PayoutsResponse $ payouts dep ptime widx
--loggedIntervalsHandler :: QDB IO a -> ReaderT a Snap ()
--loggedIntervalsHandler qdb = do
-- let QDB{..} = qdb
-- widx <- mapReaderT liftIO $ readWorkIndex
-- lift . modifyResponse $ addHeader "content-type" "application/json"
-- lift . writeLBS . A.encode $ mapKeys (^. address) widx
--
--payoutsHandler :: QDB IO a -> ReaderT a Snap ()
--payoutsHandler qdb = do
-- let QDB{..} = qdb
-- dep = linearDepreciation (Months 6) (Months 60)
-- ptime <- lift . liftIO $ getCurrentTime
-- widx <- mapReaderT liftIO $ readWorkIndex
-- lift . modifyResponse $ addHeader "content-type" "application/json"
-- lift . writeLBS . A.encode . PayoutsResponse $ payouts dep ptime widx