1 --------------------------------------------------------------------------------
2 {-# LANGUAGE FlexibleContexts
14 --------------------------------------------------------------------------------
15 import Prelude
hiding (catch)
17 import Control
.Applicative
((<$>), (<*>), pure
)
18 import Control
.Concurrent
(forkIO
)
19 import Control
.Exception
(AsyncException
(..), catch, fromException
)
20 import Control
.Monad
(forever
, unless, liftM)
21 import Control
.Monad
.Trans
(lift
, liftIO
)
22 import Control
.Monad
.Trans
.Error
(Error
, ErrorT
(..), mapErrorT
, throwError
)
23 import Control
.Monad
.Trans
.State
25 import Data
.Aeson
.Types
26 import qualified Data
.ByteString
.Char8
as BS
27 import qualified Data
.ByteString
.Lazy
.Char8
as BSL8
28 import Data
.Foldable
(forM_
)
29 import Data
.List
(find, intersperse)
31 import qualified Data
.Map
as Map
32 import Data
.Maybe (catMaybes, fromMaybe, isJust, listToMaybe)
34 import qualified Data
.Set
as Set
35 import Data
.Text
(Text
)
36 import qualified Data
.Text
as T
37 import qualified Data
.Text
.IO as T
38 import Data
.Time
.Clock
39 import Database
.Esqueleto
hiding ((=.), get
, update
)
40 import Database
.Persist
.Postgresql
hiding ((==.), get
, update
)
41 import qualified Database
.Persist
.Postgresql
as P
42 import Database
.Persist
.TH
44 import qualified Network
.WebSockets
as WS
45 import Numeric
(showFFloat)
46 import System
.Environment
(getArgs)
47 import System
.Process
(readProcess
)
48 import Util
.ApproxEq
((~~
=))
49 import Util
.Either (doLeft
, isRight
)
50 import Util
.Monad
((>>=*), buildMap
)
51 import Util
.Set
(distinctPairs
)
55 --------------------------------------------------------------------------------
56 data AccountInfo
= AccountInfo
57 { dropsBalance
:: Integer
58 , currentSequence
:: Integer
59 , transferRate
:: Double
62 data IOUAmount
= IOUAmount
64 , iouQuantity
:: Double
68 newtype AccountLines
= AccountLines
[IOUAmount
]
76 = OfferCreate Amount Amount
Integer (Maybe Integer)
77 | OfferCancel
Integer Integer
83 , offerSequence
:: Integer
86 newtype Offers
= Offers
[Offer
]
89 { ledgerIndex
:: Integer
93 data RecordedTransaction
= RecordedTransaction
95 share
[mkPersist sqlSettings
, mkMigrate
"migrateAll"] [persistLowerCase|
105 FundStatusUnique fundId time
111 HalfLinkUnique root branch time
123 type NodeEntity
= Entity Node
124 type ValueSimplexND
= ValueSimplex NodeEntity
Double
126 data Rootstock
= Rootstock
128 , websocket
:: WS
.Connection
130 , valueSimplex
:: ValueSimplexND
133 type RootstockIO
= StateT Rootstock
IO
134 type ExceptionalRootstock
= ErrorT
String RootstockIO
()
137 --------------------------------------------------------------------------------
138 instance ToJSON Amount
where
139 toJSON
(Drops numDrops
) = toJSON
$ show numDrops
140 toJSON
(IOU iou
) = object
141 [ "currency" .= lineCurrency
(iouLine iou
)
142 , "issuer" .= peerAccount
(iouLine iou
)
143 , "value" .= showFFloat Nothing
(iouQuantity iou
) ""
146 instance ToJSON Transaction
where
147 toJSON
(OfferCreate toSell toBuy curSeq maybeOldOfferSequence
) = object
$
148 [ "TransactionType" .= ("OfferCreate" :: Text
)
149 , "Account" .= account
151 , "Sequence" .= curSeq
153 , "TakerPays" .= toBuy
154 , "TakerGets" .= toSell
157 (\oldOfferSequence
-> ["OfferSequence" .= show oldOfferSequence
])
158 maybeOldOfferSequence
159 toJSON
(OfferCancel curSeq oldOfferSequence
) = object
$
160 [ "TransactionType" .= ("OfferCancel" :: Text
)
161 , "Account" .= account
163 , "Sequence" .= curSeq
164 , "OfferSequence" .= oldOfferSequence
167 instance FromJSON AccountInfo
where
168 parseJSON
(Object obj
) = do
169 result
<- obj
.: "result"
170 accountData
<- result
.: "account_data"
172 <$> (accountData
.: "Balance" >>= return . read)
173 <*> accountData
.: "Sequence"
174 <*> (maybe 1 (/1000000000) <$> accountData
.:?
"TransferRate")
175 parseJSON
value = fail $
176 "Not an account info response:\n" ++ (BSL8
.unpack
$ encode
value)
178 instance FromJSON IOUAmount
where
179 parseJSON
(Object obj
) = IOUAmount
182 <*> obj
.: "currency")
183 <*> (obj
.: "balance" >>= return . read)
184 parseJSON
value = fail $
185 "Not an account line:\n" ++ (BSL8
.unpack
$ encode
value)
187 instance FromJSON AccountLines
where
188 parseJSON
(Object obj
) = do
189 result
<- obj
.: "result"
190 AccountLines
<$> result
.: "lines"
191 parseJSON
value = fail $
192 "Not a list of account lines:\n" ++ (BSL8
.unpack
$ encode
value)
194 instance FromJSON Amount
where
195 parseJSON
(Object obj
) = IOU
<$> (IOUAmount
198 <*> obj
.: "currency")
199 <*> (obj
.: "value" >>= return . read))
200 parseJSON
(String str
) = return $ Drops
$ read $ T
.unpack str
201 parseJSON
value = fail $
202 "Not an Amount:\n" ++ (BSL8
.unpack
$ encode
value)
204 instance FromJSON Offer
where
205 parseJSON
(Object obj
) = Offer
206 <$> obj
.: "taker_gets"
207 <*> obj
.: "taker_pays"
209 parseJSON
value = fail $
210 "Not an offer:\n" ++ (BSL8
.unpack
$ encode
value)
212 instance FromJSON Offers
where
213 parseJSON
(Object obj
) = do
214 result
<- obj
.: "result"
215 Offers
<$> result
.: "offers"
216 parseJSON
value = fail $
217 "Not a list of offers:\n" ++ (BSL8
.unpack
$ encode
value)
219 instance FromJSON Ledger
where
220 parseJSON
(Object obj
) = Ledger
221 <$> obj
.: "ledger_index"
223 parseJSON
value = fail $
224 "Not a ledger:\n" ++ (BSL8
.unpack
$ encode
value)
226 instance FromJSON RecordedTransaction
where
227 parseJSON
(Object obj
) = do
228 objType
<- obj
.: "type"
229 if objType
== ("transaction" :: Text
)
230 then return RecordedTransaction
232 "Not a recorded transaction:\n" ++ (BSL8
.unpack
$ encode
$ Object obj
)
233 parseJSON
value = fail $
234 "Not a recorded transaction:\n" ++ (BSL8
.unpack
$ encode
value)
237 --------------------------------------------------------------------------------
238 secretFile
, rsignPath
, sqlPassFile
:: FilePath
239 secretFile
= "/home/tim/Documents/passwords/ripple-secret.gpg"
241 "/home/tim/build/ripple/ripple-lib/node_modules/ripple-lib/bin/rsign.js"
242 sqlPassFile
= "sql-password.gpg"
244 connString
:: BS
.ByteString
245 connString
= "host=localhost port=5432 user=tim dbname=rootstock password="
248 account
= "rpY4wdftAiEH5y5uzxC2XAvy3G27UyeQKS"
250 fee
, tfSell
, reserve
:: Integer
255 generosity
, halfSpread
:: Double
259 lookupXRP
:: AccountInfo
-> Amount
260 lookupXRP acInfo
= Drops
$ dropsBalance acInfo
- reserve
262 lookupLine
:: AccountLines
-> IOULine
-> Maybe Amount
263 lookupLine
(AccountLines
lines) fundLine
= do
264 foundLine
<- find ((fundLine
==) . iouLine
) lines
265 return $ IOU foundLine
267 lookupFund
:: AccountInfo
-> AccountLines
-> Fund
-> Maybe Amount
268 lookupFund acInfo _ XRP
= Just
$ lookupXRP acInfo
269 lookupFund _ acLines
(IOUFund fundLine
) = lookupLine acLines fundLine
271 getQuantity
:: Amount
-> Double
272 getQuantity
(Drops n
) = fromInteger n
273 getQuantity
(IOU iou
) = iouQuantity iou
275 setQuantity
:: Amount
-> Double -> Amount
276 setQuantity
(Drops _
) q
= Drops
$ round q
277 setQuantity
(IOU iou
) q
= IOU
$ iou
{ iouQuantity
= q
}
279 getSequence
:: Transaction
-> Integer
280 getSequence
(OfferCreate _ _ curSeq _
) = curSeq
281 getSequence
(OfferCancel curSeq _
) = curSeq
283 lookupGetQuantity
:: AccountInfo
-> AccountLines
-> NodeEntity
-> Double
284 lookupGetQuantity acInfo acLines
=
285 fromMaybe 0 . liftM getQuantity
.
286 lookupFund acInfo acLines
. nodeFund
. entityVal
288 sellAtPrice
:: Amount
-> Amount
-> Double -> Double -> Double ->
290 sellAtPrice sellFrom buyTo sellFee buyFee p
=
292 q
= (getQuantity sellFrom
- sellFee
- (getQuantity buyTo
- buyFee
)/p
)/2
294 (setQuantity sellFrom q
, setQuantity buyTo
$ p
* q
)
296 sellAtHalfSpread
:: Amount
-> Amount
-> Double -> Double -> Double
298 sellAtHalfSpread sellFrom buyTo sellFee buyFee hSpread
=
299 sellAtPrice sellFrom buyTo sellFee buyFee
$
300 hSpread
* getQuantity buyTo
/ getQuantity sellFrom
302 validNoLoss
:: Amount
-> Amount
-> Double -> Double -> Amount
-> Amount
-> Bool
303 validNoLoss sellFrom buyTo sellFee buyFee toSell toBuy
=
305 sellFromQ
= getQuantity sellFrom
306 buyToQ
= getQuantity buyTo
307 toSellQ
= getQuantity toSell
308 toBuyQ
= getQuantity toBuy
310 toSellQ
> 0 && toBuyQ
> 0 &&
311 (sellFromQ
- toSellQ
- sellFee
) * (buyToQ
+ toBuyQ
- buyFee
) >=
314 fund
:: Amount
-> Fund
316 fund
(IOU iou
) = IOUFund
$ iouLine iou
318 fromNodeEntity
:: a
-> (IOULine
-> a
) -> NodeEntity
-> a
319 fromNodeEntity d f x
= case nodeFund
$ entityVal x
of
323 amount
:: Double -> NodeEntity
-> Amount
325 fromNodeEntity
(Drops
$ round q
) $ \l
->
326 IOU
$ IOUAmount
{iouLine
= l
, iouQuantity
= q
}
328 peerOfNodeEntity
:: NodeEntity
-> Maybe Text
329 peerOfNodeEntity
= fromNodeEntity Nothing
$ Just
. peerAccount
331 lookupOffer
:: Offers
-> Fund
-> Fund
-> Maybe Offer
332 lookupOffer
(Offers offers
) toSell toBuy
= find
333 (\offer
-> fund
(takerGets offer
) == toSell
&&
334 fund
(takerPays offer
) == toBuy
)
337 lookupOfferSequence
:: Offers
-> Fund
-> Fund
-> Maybe Integer
338 lookupOfferSequence offers toSell toBuy
= do
339 foundOffer
<- lookupOffer offers toSell toBuy
340 return $ offerSequence foundOffer
342 actionFinished
:: Entity ActionLog
-> Bool
343 actionFinished
= isJust . actionLogEnd
. entityVal
345 updatedValueSimplexWithGenerosity
::
346 Double -> ValueSimplexND
-> AccountInfo
-> AccountLines
-> ValueSimplexND
347 updatedValueSimplexWithGenerosity gen vs acInfo acLines
=
348 multiUpdate vs
$ \nodeEnt
->
349 let actual
= lookupGetQuantity acInfo acLines nodeEnt
in
350 case nodeFund
$ entityVal nodeEnt
of
354 updatedValueSimplex
::
355 ValueSimplexND
-> AccountInfo
-> AccountLines
-> ValueSimplexND
356 updatedValueSimplex
= updatedValueSimplexWithGenerosity
0
359 ValueSimplexND
-> (NodeEntity
-> Double) -> AccountInfo
-> [Transaction
]
360 makeTransactions vs trf acInfo
=
362 (flip zipWith $ Set
.toList
$ distinctPairs
$ nodes vs
)
363 [currentSequence acInfo
..]
364 $ \(x0
, x1
) curSeq
->
365 let (q0
, q1
) = linkOptimumAtPrice vs x0 x1
$ halfSpread
* price vs x0 x1
in
366 OfferCreate
(amount
(-q0
/ trf x0
) x0
) (amount q1 x1
) curSeq Nothing
368 --------------------------------------------------------------------------------
369 signTransaction
:: Transaction
-> RootstockIO
String
370 signTransaction tx
= do
372 blobNewLine
<- lift
$ readProcess
373 rsignPath
[sec
, BSL8
.unpack
$ encode tx
] ""
374 return $ init blobNewLine
377 --------------------------------------------------------------------------------
378 getSqlConnection
:: RootstockIO Connection
379 getSqlConnection
= gets sql
381 runSqlQuery
:: SqlPersistM a
-> RootstockIO a
382 runSqlQuery query
= do
383 sqlConn
<- getSqlConnection
384 lift
$ runSqlPersistM query sqlConn
386 getNodeEntities
:: SqlPersistM
[NodeEntity
]
387 getNodeEntities
= select
$ from
return
389 readValueSimplex
:: SqlPersistM ValueSimplexND
390 readValueSimplex
= do
391 nodeSet
<- Set
.fromList
<$> getNodeEntities
392 qMap
<- buildMap
(Set
.toList
$ distinctPairs nodeSet
) $ \(x
, y
) -> do
393 [Value q
] <- select
$ from
$ \hl
-> do
395 $ hl ^
. HalfLinkRoot
==. val
(entityKey x
)
396 &&. hl ^
. HalfLinkBranch
==. val
(entityKey y
)
397 orderBy
[desc
$ hl ^
. HalfLinkTime
]
399 return $ hl ^
. HalfLinkQuantity
401 return $ fromFunction
(curry $ flip (Map
.findWithDefault
0) qMap
) nodeSet
404 AccountInfo
-> AccountLines
-> ValueSimplexND
-> SqlPersistM
()
405 writeValueSimplex acInfo acLines vs
= do
406 time
<- liftIO getCurrentTime
407 insertMany
$ flip map (Set
.toList
$ nodes vs
) $ \nodeEnt
-> FundStatus
408 { fundStatusFundId
= entityKey nodeEnt
409 , fundStatusQuantity
= lookupGetQuantity acInfo acLines nodeEnt
410 , fundStatusTime
= time
412 forM_
(distinctPairs
$ nodes vs
) $ \(x
, y
) -> insert_
$ HalfLink
413 { halfLinkRoot
= entityKey x
414 , halfLinkBranch
= entityKey y
415 , halfLinkQuantity
= vsLookup vs x y
416 , halfLinkTime
= time
419 warn
:: Text
-> SqlPersistM
()
421 now
<- liftIO getCurrentTime
423 { warningWarning
= warning
427 getCurrentAction
:: SqlPersistM
(Entity ActionLog
)
428 getCurrentAction
= liftM head $ select
$ from
$ \ac
-> do
429 orderBy
[desc
$ ac ^
. ActionLogStart
]
433 startAction
:: Action
-> SqlPersistM ActionLogId
434 startAction action
= do
435 start
<- liftIO getCurrentTime
437 { actionLogAction
= action
438 , actionLogStart
= start
439 , actionLogEnd
= Nothing
440 , actionLogSuccess
= Nothing
443 endAction
:: ActionLogId
-> Bool -> SqlPersistM
()
444 endAction actionId success
= do
445 end
<- liftIO getCurrentTime
447 [ ActionLogEnd
=. Just end
448 , ActionLogSuccess
=. Just success
451 intervene
:: Action
-> ExceptionalRootstock
-> RootstockIO
()
452 intervene action intervention
= do
453 actionId
<- runSqlQuery
$ startAction action
454 result
<- runErrorT intervention
455 doLeft
(lift
. putStrLn) result
456 runSqlQuery
$ endAction actionId
$ isRight result
459 --------------------------------------------------------------------------------
460 runWebsocket
:: WS
.ClientApp a
-> RootstockIO a
461 runWebsocket app
= gets websocket
>>= lift
. app
463 receiveData
:: WS
.WebSocketsData a
=> RootstockIO a
464 receiveData
= runWebsocket WS
.receiveData
466 sendTextData
:: WS
.WebSocketsData a
=> a
-> RootstockIO
()
467 sendTextData x
= runWebsocket
$ flip WS
.sendTextData x
469 waitForType
:: FromJSON a
=> RootstockIO a
471 encoded
<- receiveData
472 case decode encoded
of
474 lift
$ putStrLn ("Skipping:\n" ++ (BSL8
.unpack encoded
))
477 lift
$ putStrLn ("Using:\n" ++ (BSL8
.unpack encoded
))
480 signAndSend
:: Transaction
-> RootstockIO
()
482 txBlob
<- signTransaction tx
483 sendTextData
$ encode
$ object
484 [ "command" .= ("submit" :: Text
)
485 , "tx_blob" .= txBlob
488 submitSellOffer
:: Amount
-> Amount
-> Double -> Double -> Double -> Offers
->
489 Integer -> RootstockIO
Integer
492 sellFee buyFee hSpread
495 (toSell
, toBuy
) = sellAtHalfSpread sellFrom buyTo sellFee buyFee hSpread
496 maybeOldOfferSequence
=
497 lookupOfferSequence offers
(fund sellFrom
) $ fund buyTo
498 tx
= OfferCreate toSell toBuy curSeq Nothing
499 lift
$ BSL8
.putStrLn $ encode tx
500 if validNoLoss sellFrom buyTo sellFee buyFee toSell toBuy
503 case maybeOldOfferSequence
of
504 Nothing
-> return $ curSeq
+ 1
505 Just oldOfferSequence
-> do
506 signAndSend
$ OfferCancel
(curSeq
+ 1) oldOfferSequence
509 lift
$ putStrLn "Skipping the above: either invalid or loss-making"
512 subscribe
:: [Pair
] -> WS
.ClientApp
()
514 flip WS
.sendTextData
$ encode
$ object
$
515 ["command" .= ("subscribe" :: Text
)] ++ options
517 subscribeAccount
:: WS
.ClientApp
()
518 subscribeAccount
= subscribe
["accounts" .= [account
]]
520 queryOwnAccount
:: FromJSON a
=> Text
-> RootstockIO a
521 queryOwnAccount command
= do
522 sendTextData
$ encode
$ object
523 [ "command" .= command
524 , "account" .= account
528 getAccountInfo
:: RootstockIO AccountInfo
529 getAccountInfo
= queryOwnAccount
"account_info"
531 getAccountLines
:: RootstockIO AccountLines
532 getAccountLines
= queryOwnAccount
"account_lines"
534 getAccountOffers
:: RootstockIO Offers
535 getAccountOffers
= queryOwnAccount
"account_offers"
537 getPeerAccountInfo
:: Text
-> RootstockIO AccountInfo
538 getPeerAccountInfo peer
= do
539 sendTextData
$ encode
$ object
540 [ "command" .= ("account_info" :: Text
)
545 clearAndUpdate
:: RootstockIO
()
546 {- Must have subscribed to ledger updates for this to work -}
548 Offers offerList
<- getAccountOffers
549 acInfo
<- getAccountInfo
552 acLines
<- getAccountLines
553 vs
<- gets valueSimplex
554 let vs
' = updatedValueSimplex vs acInfo acLines
555 if strictlySuperior
(~~
=) vs
' vs
559 vs
'' = updatedValueSimplexWithGenerosity generosity vs acInfo acLines
561 = " non-superior ValueSimplex (generosity: "
562 `T
.append` T
.pack
(show generosity
)
564 if strictlySuperior
(~~
=) vs
'' vs
565 then runSqlQuery
$ warn
$ "Slightly" `T
.append` warning
566 else error $ "Seriously" ++ T
.unpack warning
567 runSqlQuery
$ writeValueSimplex acInfo acLines vs
'
568 modify
$ \rs
-> rs
{valueSimplex
= vs
'}
570 forM_
(zip offerList
[currentSequence acInfo
..]) $ \(off
, seq) ->
571 signAndSend
$ OfferCancel
seq $ offerSequence off
572 waitForType
:: RootstockIO Ledger
575 getUpdatedValueSimplexWithAccountInfo
::
576 AccountInfo
-> RootstockIO ValueSimplexND
577 getUpdatedValueSimplexWithAccountInfo acInfo
=
578 updatedValueSimplex
<$> gets valueSimplex
<*> pure acInfo
<*> getAccountLines
580 getUpdatedValueSimplex
:: RootstockIO ValueSimplexND
581 getUpdatedValueSimplex
=
582 getUpdatedValueSimplexWithAccountInfo
=<< getAccountInfo
584 strictlySuperiorAndValid
:: ValueSimplexND
-> RootstockIO
Bool
585 strictlySuperiorAndValid vs
' = do
586 vs
<- gets valueSimplex
587 return $ strictlySuperior
(~~
=) vs
' vs
&& status
(~~
=) vs
' == OK
589 waitForImprovement
:: RootstockIO
()
590 waitForImprovement
= do
591 waitForType
:: RootstockIO Ledger
592 waitForType
:: RootstockIO RecordedTransaction
593 improvement
<- strictlySuperiorAndValid
=<< getUpdatedValueSimplex
596 else waitForImprovement
598 submitAndWait
:: [Transaction
] -> RootstockIO
()
599 submitAndWait txs
= do
600 forM_ txs signAndSend
601 waitForType
:: RootstockIO Ledger
602 acInfo
<- getAccountInfo
604 strictlySuperiorAndValid
=<< getUpdatedValueSimplexWithAccountInfo acInfo
607 else case dropWhile ((currentSequence acInfo
>) . getSequence
) txs
of
608 [] -> waitForImprovement
609 txs
' -> submitAndWait txs
'
611 getTransitRates
:: RootstockIO
(NodeEntity
-> Double)
613 peers
<- catMaybes . Set
.toList
. Set
.map peerOfNodeEntity
. nodes
614 <$> gets valueSimplex
615 trm
<- buildMap peers
$ \peer
-> transferRate
<$> getPeerAccountInfo peer
616 return $ \x
-> fromMaybe 1 $ peerOfNodeEntity x
>>= flip Map
.lookup trm
618 marketMakerLoop
:: RootstockIO
()
621 makeTransactions
<$> gets valueSimplex
<*> getTransitRates
<*> getAccountInfo
626 --------------------------------------------------------------------------------
627 throwIf
:: (Error e
, Monad m
) => Bool -> e
-> ErrorT e m
()
628 throwIf test err
= if test
then throwError err
else return ()
630 setupDatabase
:: IOULine
-> ExceptionalRootstock
631 setupDatabase fundLine
= do
632 existingNodes
<- lift
$ runSqlQuery getNodeEntities
633 throwIf
(not $ null existingNodes
) "Database already has nodes"
634 acInfo
<- lift getAccountInfo
635 let dropsBal
= getQuantity
$ lookupXRP acInfo
636 throwIf
(dropsBal
<= 0) "Not enough XRP to meet desired reserve"
637 acLines
<- lift getAccountLines
638 lineBal
<- case lookupLine acLines fundLine
of
639 Nothing
-> throwError
"Requested IOU line not found"
640 Just amount
-> return $ getQuantity amount
641 throwIf
(lineBal
<= 0) "Non-positive balance in requested IOU line"
642 let xrpNode
= Node
{nodeFund
= XRP
}
643 lineNode
= Node
{nodeFund
= IOUFund fundLine
}
644 lift
$ runSqlQuery
$ do
645 xrpId
<- insert xrpNode
646 lineId
<- insert lineNode
647 let xrpNodeEntity
= Entity
{entityKey
= xrpId
, entityVal
= xrpNode
}
648 lineNodeEntity
= Entity
{entityKey
= lineId
, entityVal
= lineNode
}
649 writeValueSimplex acInfo acLines
$
650 flip fromFunction
(Set
.fromList
[xrpNodeEntity
, lineNodeEntity
]) $ \x _
->
651 if x
== xrpNodeEntity
656 --------------------------------------------------------------------------------
657 {- getLinkEntities :: RootstockIO [Entity Link]
658 getLinkEntities = runSqlQuery $ select $ from return
660 getLinks :: RootstockIO [Link]
661 getLinks = liftM (map entityVal) getLinkEntities
663 waitForAction :: RootstockIO Offers
665 sendTextData $ encode $ object
666 [ "command" .= ("account_offers" :: Text)
667 , "account" .= account
669 offers <- waitForType
671 if all (bothOffersPresent offers) links
673 RecordedTransaction <- waitForType
677 currentLinkStatus :: LinkId -> RootstockIO (Maybe LinkStatus)
678 currentLinkStatus linkId = do
679 statusList <- runSqlQuery $ select $ from $ \status -> do
680 where_ $ status ^. LinkStatusLinkId ==. val linkId
681 orderBy [desc $ status ^. LinkStatusTimestamp]
684 return $ liftM entityVal $ listToMaybe statusList
686 submitLinkOffers :: AccountInfo -> AccountLines ->
687 Offers -> Entity Link -> Integer -> RootstockIO Integer
688 submitLinkOffers acInfo acLines offers (Entity linkId link) curSeq = do
689 if bothOffersPresent offers link
692 maybeHSpread <- liftM (liftM linkStatusHalfSpread) $
693 currentLinkStatus linkId
696 right = linkRight link
697 feeForCalcs XRP = fromInteger $ 3 * fee
699 leftFeeForCalcs = feeForCalcs left
700 rightFeeForCalcs = feeForCalcs right
702 ( lookupFund acInfo acLines left
703 , lookupFund acInfo acLines right
706 (Just leftTotal, Just rightTotal, Just hSpread) -> do
707 now <- lift getCurrentTime
709 insert $ HalfLink left right (getQuantity leftTotal) now
710 insert $ HalfLink right left (getQuantity rightTotal) now
711 nextSeq <- submitSellOffer
713 leftFeeForCalcs rightFeeForCalcs hSpread
717 rightFeeForCalcs leftFeeForCalcs hSpread
721 "The link status or one of the lines of credit wasn't found"
724 marketMakerLoop :: RootstockIO ()
726 offers <- waitForAction
727 sendTextData $ encode $ object
728 [ "command" .= ("account_info" :: Text)
729 , "account" .= account
731 acInfo <- waitForType
732 sendTextData $ encode $ object
733 [ "command" .= ("account_lines" :: Text)
734 , "account" .= account
736 acLines <- waitForType
737 linkEntities <- getLinkEntities
738 currentSequence acInfo >>=*
739 map (submitLinkOffers acInfo acLines offers) linkEntities
742 marketMaker :: RootstockIO ()
747 insertOldLink :: OldLink -> RootstockIO ()
748 insertOldLink link = do
749 now <- lift getCurrentTime
751 linkId <- insert $ Link (leftFund link) (rightFund link)
752 insert $ LinkStatus linkId (halfSpread link) now
756 --------------------------------------------------------------------------------
757 runRootstock
:: RootstockIO a
-> Rootstock
-> IO a
758 runRootstock
= evalStateT
760 marketMaker
:: RootstockIO
()
762 runWebsocket
$ subscribe
763 [ "streams" .= ["ledger" :: Text
]
764 , "accounts" .= [account
]
766 result
<- runErrorT
$ do
767 mapErrorT runSqlQuery
$ do
768 curAc
<- lift getCurrentAction
770 (not $ actionFinished curAc
)
771 "Another process hasn't yet cleanly finished with the database"
772 lift
$ startAction Running
774 liftIO
$ catch (runRootstock marketMakerLoop rs
) $ \e
-> do
775 flip runSqlPersistM
(sql rs
) $ do
776 curAc
<- getCurrentAction
777 if (actionLogAction
(entityVal curAc
) == Running
778 && not (actionFinished curAc
))
782 $ fromException e `
elem`
map Just
[ThreadKilled
, UserInterrupt
]
784 putStrLn $ "Exiting on: " ++ show e
785 doLeft
(lift
. putStrLn) result
787 rippleInteract
:: WS
.ClientApp
()
788 rippleInteract conn
= do
789 -- Fork a thread that writes WS data to stdout
790 _
<- forkIO
$ forever
$ do
791 msg
<- WS
.receiveData conn
792 liftIO
$ T
.putStrLn msg
794 runRipple subscribeAccount
796 -- Read from stdin and write to WS
799 unless (T
.null line
) $ WS
.sendTextData conn line
>> loop
802 WS
.sendClose conn
("Bye!" :: Text
)
804 readSecret
:: IO String
805 readSecret
= readProcess
"gpg" ["-o", "-", secretFile
] ""
807 readSqlPass
:: IO BS
.ByteString
808 readSqlPass
= readProcess
"gpg" ["-o", "-", sqlPassFile
] "" >>= return . BS
.pack
810 runRipple
:: WS
.ClientApp a
-> IO a
811 runRipple app
= WS
.runClient
"s1.ripple.com" 443 "/" app
813 runRippleWithSecret
:: RootstockIO a
-> IO a
814 runRippleWithSecret app
= do
816 sqlPass
<- readSqlPass
817 withPostgresqlConn
(BS
.concat [connString
, sqlPass
]) $ \sqlConn
-> do
818 vs
<- flip runSqlPersistM sqlConn
$ do
819 runMigration migrateAll
821 runRipple
$ \wsConn
->
822 runRootstock app
$ Rootstock
829 {- insertOldLinks :: IO ()
830 insertOldLinks = runRippleWithSecret $ sequence_ $ map insertOldLink oldLinks -}
836 ["setup", currency
, peer
] -> runRippleWithSecret
$ intervene InitialSetup
$
837 setupDatabase
$ IOULine
838 { peerAccount
= T
.pack peer
839 , lineCurrency
= T
.pack currency
841 _
-> putStrLn "Command not understood"