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
, join, liftM, unless, when)
21 import Control
.Monad
.IfElse
(aifM
, awhenM
, unlessM
)
22 import Control
.Monad
.Trans
(lift
, liftIO
)
23 import Control
.Monad
.Trans
.Error
(ErrorT
(..), mapErrorT
, throwError
)
24 import Control
.Monad
.Trans
.State
26 import Data
.Aeson
.Types
27 import qualified Data
.ByteString
.Char8
as BS
28 import qualified Data
.ByteString
.Lazy
.Char8
as BSL8
29 import Data
.Foldable
(forM_
)
30 import Data
.List
(find, intersperse)
32 import qualified Data
.Map
as Map
33 import Data
.Maybe (catMaybes, fromJust, fromMaybe, isJust, listToMaybe)
35 import qualified Data
.Set
as Set
36 import Data
.Text
(Text
)
37 import qualified Data
.Text
as T
38 import qualified Data
.Text
.IO as T
39 import Data
.Time
.Clock
40 import Database
.Esqueleto
hiding ((=.), get
, update
)
41 import Database
.Persist
.Postgresql
hiding ((==.), (<=.), (!=.), get
, update
)
42 import qualified Database
.Persist
.Postgresql
as P
43 import Database
.Persist
.TH
45 import qualified Network
.WebSockets
as WS
46 import Numeric
(showFFloat)
47 import Ripple
.WebSockets
(RippleResult
(RippleResult
))
48 import RootstockException
(RootstockException
(..))
49 import System
.Environment
(getArgs)
50 import Util
.ApproxEq
((~~
=))
51 import Util
.Either (doLeft
, isRight
)
52 import Util
.Error
(throwIf
)
53 import Util
.Foldable
(sumWith
)
54 import Util
.Function
((.!))
55 import Util
.Monad
((>>=*), buildMap
)
56 import Util
.Persist
(insertReturnEntity
)
57 import Util
.Set
(distinctPairs
, distinctPairsOneWay
)
61 --------------------------------------------------------------------------------
62 data AccountInfo
= AccountInfo
63 { dropsBalance
:: Integer
64 , currentSequence
:: Integer
65 , transferRate
:: Double
68 data IOUAmount
= IOUAmount
70 , iouQuantity
:: Double
74 newtype AccountLines
= AccountLines
[IOUAmount
]
82 = OfferCreate Amount Amount
Integer (Maybe Integer)
83 | OfferCancel
Integer Integer
89 , offerSequence
:: Integer
92 newtype Offers
= Offers
[Offer
]
95 { ledgerIndex
:: Integer
99 data RecordedTransaction
= RecordedTransaction
101 share
[mkPersist sqlSettings
, mkMigrate
"migrateAll"] [persistLowerCase|
111 FundStatusUnique fundId time
117 HalfLinkUnique root branch time
129 type NodeEntity
= Entity Node
130 type ValueSimplexND
= ValueSimplex NodeEntity
Double
132 data Rootstock
= Rootstock
134 , websocket
:: WS
.Connection
136 , valueSimplex
:: ValueSimplexND
137 , nextSequence
:: Integer
138 , rsAction
:: ActionLogId
141 type RootstockIO
= StateT Rootstock
IO
142 type ExceptionalRootstock
= ErrorT RootstockException RootstockIO
145 --------------------------------------------------------------------------------
146 instance ToJSON Amount
where
147 toJSON
(Drops numDrops
) = toJSON
$ show numDrops
148 toJSON
(IOU iou
) = object
149 [ "currency" .= lineCurrency
(iouLine iou
)
150 , "issuer" .= peerAccount
(iouLine iou
)
151 , "value" .= showFFloat Nothing
(iouQuantity iou
) ""
154 instance ToJSON Transaction
where
155 toJSON
(OfferCreate toSell toBuy curSeq maybeOldOfferSequence
) = object
$
156 [ "TransactionType" .= ("OfferCreate" :: Text
)
157 , "Account" .= account
159 , "Sequence" .= curSeq
161 , "TakerPays" .= toBuy
162 , "TakerGets" .= toSell
165 (\oldOfferSequence
-> ["OfferSequence" .= show oldOfferSequence
])
166 maybeOldOfferSequence
167 toJSON
(OfferCancel curSeq oldOfferSequence
) = object
$
168 [ "TransactionType" .= ("OfferCancel" :: Text
)
169 , "Account" .= account
171 , "Sequence" .= curSeq
172 , "OfferSequence" .= oldOfferSequence
175 instance FromJSON AccountInfo
where
176 parseJSON
(Object obj
) = do
177 accountData
<- obj
.: "account_data"
179 <$> (accountData
.: "Balance" >>= return . read)
180 <*> accountData
.: "Sequence"
181 <*> (maybe 1 (/1000000000) <$> accountData
.:?
"TransferRate")
182 parseJSON
value = fail $
183 "Not an account info response:\n" ++ (BSL8
.unpack
$ encode
value)
185 instance FromJSON IOUAmount
where
186 parseJSON
(Object obj
) = IOUAmount
189 <*> obj
.: "currency")
190 <*> (obj
.: "balance" >>= return . read)
191 parseJSON
value = fail $
192 "Not an account line:\n" ++ (BSL8
.unpack
$ encode
value)
194 instance FromJSON AccountLines
where
195 parseJSON
(Object obj
) = AccountLines
<$> obj
.: "lines"
196 parseJSON
value = fail $
197 "Not a list of account lines:\n" ++ (BSL8
.unpack
$ encode
value)
199 instance FromJSON Amount
where
200 parseJSON
(Object obj
) = IOU
<$> (IOUAmount
203 <*> obj
.: "currency")
204 <*> (obj
.: "value" >>= return . read))
205 parseJSON
(String str
) = return $ Drops
$ read $ T
.unpack str
206 parseJSON
value = fail $
207 "Not an Amount:\n" ++ (BSL8
.unpack
$ encode
value)
209 instance FromJSON Offer
where
210 parseJSON
(Object obj
) = Offer
211 <$> obj
.: "taker_gets"
212 <*> obj
.: "taker_pays"
214 parseJSON
value = fail $
215 "Not an offer:\n" ++ (BSL8
.unpack
$ encode
value)
217 instance FromJSON Offers
where
218 parseJSON
(Object obj
) = Offers
<$> obj
.: "offers"
219 parseJSON
value = fail $
220 "Not a list of offers:\n" ++ (BSL8
.unpack
$ encode
value)
222 instance FromJSON Ledger
where
223 parseJSON
(Object obj
) = Ledger
224 <$> obj
.: "ledger_index"
226 parseJSON
value = fail $
227 "Not a ledger:\n" ++ (BSL8
.unpack
$ encode
value)
229 instance FromJSON RecordedTransaction
where
230 parseJSON
(Object obj
) = do
231 objType
<- obj
.: "type"
232 if objType
== ("transaction" :: Text
)
233 then return RecordedTransaction
235 "Not a recorded transaction:\n" ++ (BSL8
.unpack
$ encode
$ Object obj
)
236 parseJSON
value = fail $
237 "Not a recorded transaction:\n" ++ (BSL8
.unpack
$ encode
value)
240 --------------------------------------------------------------------------------
241 secretFile
, sqlPassFile
:: FilePath
242 secretFile
= "/media/mishael/ripple-secret"
243 sqlPassFile
= "/media/mishael/sql-password"
245 connString
:: BS
.ByteString
246 connString
= BS
.concat
247 [ "host=localhost port=5432 dbname=rootstock-test"
248 , " user=rootstock password="
252 account
= "rpY4wdftAiEH5y5uzxC2XAvy3G27UyeQKS"
254 fee
, tfSell
, reserve
:: Integer
259 generosity
, halfSpread
:: Double
263 noAction
:: ActionLogId
264 noAction
= Key PersistNull
266 lookupXRP
:: AccountInfo
-> Amount
267 lookupXRP acInfo
= Drops
$ dropsBalance acInfo
- reserve
269 lookupLine
:: AccountLines
-> IOULine
-> Maybe Amount
270 lookupLine
(AccountLines
lines) fundLine
= do
271 foundLine
<- find ((fundLine
==) . iouLine
) lines
272 return $ IOU foundLine
274 lookupFund
:: AccountInfo
-> AccountLines
-> Fund
-> Maybe Amount
275 lookupFund acInfo _ XRP
= Just
$ lookupXRP acInfo
276 lookupFund _ acLines
(IOUFund fundLine
) = lookupLine acLines fundLine
278 getQuantity
:: Amount
-> Double
279 getQuantity
(Drops n
) = fromInteger n
280 getQuantity
(IOU iou
) = iouQuantity iou
282 getSequence
:: Transaction
-> Integer
283 getSequence
(OfferCreate _ _ curSeq _
) = curSeq
284 getSequence
(OfferCancel curSeq _
) = curSeq
286 lookupGetQuantity
:: AccountInfo
-> AccountLines
-> NodeEntity
-> Double
287 lookupGetQuantity acInfo acLines
=
288 fromMaybe 0 . liftM getQuantity
.
289 lookupFund acInfo acLines
. nodeFund
. entityVal
291 fromNodeEntity
:: a
-> (IOULine
-> a
) -> NodeEntity
-> a
292 fromNodeEntity d f x
= case nodeFund
$ entityVal x
of
296 amount
:: Double -> NodeEntity
-> Amount
298 fromNodeEntity
(Drops
$ round q
) $ \l
->
299 IOU
$ IOUAmount
{iouLine
= l
, iouQuantity
= q
}
301 peerOfNodeEntity
:: NodeEntity
-> Maybe Text
302 peerOfNodeEntity
= fromNodeEntity Nothing
$ Just
. peerAccount
304 actionFinished
:: ActionLog
-> Bool
305 actionFinished
= isJust . actionLogEnd
307 actionEntityFinished
:: Entity ActionLog
-> Bool
308 actionEntityFinished
= actionFinished
. entityVal
310 actionRunning
:: Entity ActionLog
-> Bool
311 actionRunning acEnt
=
312 actionLogAction
(entityVal acEnt
) == Running
313 && not (actionEntityFinished acEnt
)
315 updatedValueSimplexWithGenerosity
::
316 Double -> ValueSimplexND
-> AccountInfo
-> AccountLines
-> ValueSimplexND
317 updatedValueSimplexWithGenerosity gen vs acInfo acLines
=
318 multiUpdate vs
$ \nodeEnt
->
319 let actual
= lookupGetQuantity acInfo acLines nodeEnt
in
320 case nodeFund
$ entityVal nodeEnt
of
324 updatedValueSimplex
::
325 ValueSimplexND
-> AccountInfo
-> AccountLines
-> ValueSimplexND
326 updatedValueSimplex
= updatedValueSimplexWithGenerosity
0
329 ValueSimplexND
-> (NodeEntity
-> Double) -> Integer -> [Transaction
]
330 makeTransactions vs trf nextSeq
=
332 (flip zipWith $ Set
.toList
$ distinctPairs
$ nodes vs
)
334 $ \(x0
, x1
) curSeq
->
335 let (q0
, q1
) = linkOptimumAtPrice vs x0 x1
$ halfSpread
* price vs x0 x1
in
336 OfferCreate
(amount
(-q0
/ trf x0
) x0
) (amount q1 x1
) curSeq Nothing
338 --------------------------------------------------------------------------------
339 getSqlConnection
:: RootstockIO Connection
340 getSqlConnection
= gets sql
342 runSqlQuery
:: SqlPersistM a
-> RootstockIO a
343 runSqlQuery query
= do
344 sqlConn
<- getSqlConnection
345 lift
$ runSqlPersistM query sqlConn
347 getNodeEntities
:: SqlPersistM
[NodeEntity
]
348 getNodeEntities
= select
$ from
return
350 readValueSimplexAt
:: UTCTime
-> SqlPersistM ValueSimplexND
351 readValueSimplexAt time
= do
352 nodeSet
<- Set
.fromList
<$> getNodeEntities
353 qMap
<- buildMap
(Set
.toList
$ distinctPairs nodeSet
) $ \(x
, y
) -> do
354 [Value q
] <- select
$ from
$ \hl
-> do
356 $ hl ^
. HalfLinkRoot
==. val
(entityKey x
)
357 &&. hl ^
. HalfLinkBranch
==. val
(entityKey y
)
358 &&. hl ^
. HalfLinkTime
<=. val time
359 orderBy
[desc
$ hl ^
. HalfLinkTime
]
361 return $ hl ^
. HalfLinkQuantity
363 return $ fromFunction
(curry $ flip (Map
.findWithDefault
0) qMap
) nodeSet
365 readValueSimplex
:: SqlPersistM ValueSimplexND
366 readValueSimplex
= liftIO getCurrentTime
>>= readValueSimplexAt
369 AccountInfo
-> AccountLines
-> ValueSimplexND
-> SqlPersistM
()
370 writeValueSimplex acInfo acLines vs
= do
371 time
<- liftIO getCurrentTime
372 insertMany
$ flip map (Set
.toList
$ nodes vs
) $ \nodeEnt
-> FundStatus
373 { fundStatusFundId
= entityKey nodeEnt
374 , fundStatusQuantity
= lookupGetQuantity acInfo acLines nodeEnt
375 , fundStatusTime
= time
377 forM_
(distinctPairs
$ nodes vs
) $ \(x
, y
) -> insert_
$ HalfLink
378 { halfLinkRoot
= entityKey x
379 , halfLinkBranch
= entityKey y
380 , halfLinkQuantity
= vsLookup vs x y
381 , halfLinkTime
= time
384 warn
:: Text
-> SqlPersistM
()
386 now
<- liftIO getCurrentTime
388 { warningWarning
= warning
392 getCurrentAction
:: SqlPersistM
(Maybe (Entity ActionLog
))
393 getCurrentAction
= liftM listToMaybe $ select
$ from
$ \ac
-> do
394 orderBy
[desc
$ ac ^
. ActionLogStart
]
398 startAction
:: Action
-> SqlPersistM ActionLogId
399 startAction action
= do
400 start
<- liftIO getCurrentTime
402 { actionLogAction
= action
403 , actionLogStart
= start
404 , actionLogEnd
= Nothing
405 , actionLogSuccess
= Nothing
408 endAction
:: ActionLogId
-> Bool -> SqlPersistM
()
409 endAction actionId success
= do
410 end
<- liftIO getCurrentTime
412 [ ActionLogEnd
=. Just end
413 , ActionLogSuccess
=. Just success
416 putAction
:: ActionLogId
-> RootstockIO
()
417 putAction actionId
= modify
$ \rs
-> rs
{rsAction
= actionId
}
419 intervene
:: Action
-> ExceptionalRootstock
() -> RootstockIO
()
420 intervene action intervention
= do
421 actionId
<- runSqlQuery
$ do
422 awhenM getCurrentAction
$ \curAc
->
423 unless (actionEntityFinished curAc
) $
424 if actionLogAction
(entityVal curAc
) == Running
425 then endAction
(entityKey curAc
) True
426 else error "Another intervention appears to be running"
429 result
<- runErrorT intervention
430 doLeft
(lift
. putStrLn . show) result
431 runSqlQuery
$ endAction actionId
$ isRight result
434 --------------------------------------------------------------------------------
435 runWebsocket
:: WS
.ClientApp a
-> RootstockIO a
436 runWebsocket app
= gets websocket
>>= lift
. app
438 receiveData
:: WS
.WebSocketsData a
=> RootstockIO a
439 receiveData
= runWebsocket WS
.receiveData
441 sendTextData
:: WS
.WebSocketsData a
=> a
-> RootstockIO
()
442 sendTextData x
= runWebsocket
$ flip WS
.sendTextData x
444 waitForType
:: FromJSON a
=> RootstockIO a
446 encoded
<- receiveData
447 case decode encoded
of
449 lift
$ putStrLn ("Skipping:\n" ++ (BSL8
.unpack encoded
))
452 lift
$ putStrLn ("Using:\n" ++ (BSL8
.unpack encoded
))
455 waitForResponseWithId
:: (Eq
id, FromJSON
id, FromJSON a
)
456 => id -> RootstockIO
(Maybe a
)
457 waitForResponseWithId idSought
= do
458 RippleResult i x
<- waitForType
459 if i
== Just idSought
460 then return $ either (const Nothing
) Just x
461 else waitForResponseWithId idSought
463 askUntilAnswered
:: FromJSON a
=> [Pair
] -> RootstockIO a
464 askUntilAnswered question
= do
465 qTime
<- show <$> liftIO getCurrentTime
466 sendTextData
$ encode
$ object
$ ("id" .= qTime
) : question
467 aifM
(waitForResponseWithId qTime
) return $ do
468 waitForType
:: RootstockIO Ledger
469 askUntilAnswered question
471 submitToTrustedServer
:: Transaction
-> RootstockIO
()
472 submitToTrustedServer tx
= do
474 sendTextData
$ encode
$ object
475 [ "command" .= ("submit" :: Text
)
480 subscribe
:: [Pair
] -> WS
.ClientApp
()
482 flip WS
.sendTextData
$ encode
$ object
$
483 ["command" .= ("subscribe" :: Text
)] ++ options
485 subscribeLedger
:: WS
.ClientApp
()
486 subscribeLedger
= subscribe
["streams" .= ["ledger" :: Text
]]
488 subscribeAccount
:: WS
.ClientApp
()
489 subscribeAccount
= subscribe
["accounts" .= [account
]]
491 subscribeLedgerAndAccount
:: WS
.ClientApp
()
492 subscribeLedgerAndAccount
= subscribe
493 [ "streams" .= ["ledger" :: Text
]
494 , "accounts" .= [account
]
497 queryOwnAccount
:: FromJSON a
=> Text
-> RootstockIO a
498 queryOwnAccount command
= askUntilAnswered
499 [ "command" .= command
500 , "account" .= account
501 , "ledger_index" .= ("validated" :: Text
)
504 getAccountInfo
:: RootstockIO AccountInfo
505 getAccountInfo
= queryOwnAccount
"account_info"
507 getAccountLines
:: RootstockIO AccountLines
508 getAccountLines
= queryOwnAccount
"account_lines"
510 getAccountOffers
:: RootstockIO Offers
511 getAccountOffers
= queryOwnAccount
"account_offers"
513 getCurrentAccountInfo
:: Text
-> RootstockIO AccountInfo
514 getCurrentAccountInfo peer
= askUntilAnswered
515 [ "command" .= ("account_info" :: Text
)
517 , "ledger_index" .= ("current" :: Text
)
520 valueSimplexEmpty
:: RootstockIO
Bool
521 valueSimplexEmpty
= isEmpty
<$> gets valueSimplex
523 putValueSimplex
:: ValueSimplexND
-> RootstockIO
()
524 putValueSimplex vs
= modify
$ \rs
-> rs
{valueSimplex
= vs
}
526 putSequence
:: Integer -> RootstockIO
()
527 putSequence nextSeq
= modify
$ \rs
-> rs
{nextSequence
= nextSeq
}
529 getAndPutSequence
:: RootstockIO
()
531 currentSequence
<$> getCurrentAccountInfo account
>>= putSequence
533 ownActionGoingQuery
:: RootstockIO
(SqlPersistM
Bool)
534 ownActionGoingQuery
= do
535 actId
<- gets rsAction
536 return $ maybe False (not . actionFinished
) <$> P
.get actId
538 ifRunning
:: SqlPersistM
() -> ExceptionalRootstock
()
540 goingQ
<- lift ownActionGoingQuery
541 mapErrorT runSqlQuery
$ do
542 going
<- lift
$ goingQ
543 throwIf NotRunning
$ not going
546 checkRunning
:: ExceptionalRootstock
()
547 checkRunning
= ifRunning
$ return ()
549 submitUntilSequenceCatchup
' :: [Transaction
] -> ExceptionalRootstock
()
550 submitUntilSequenceCatchup
' txs
= unless (null txs
) $ do
552 forM_ txs
$ lift
. submitToTrustedServer
553 lift
(waitForType
:: RootstockIO Ledger
)
554 curSeq
<- currentSequence
<$> lift getAccountInfo
555 submitUntilSequenceCatchup
' $ dropWhile ((curSeq
>) . getSequence
) txs
557 submitUntilSequenceCatchup
:: [Transaction
] -> ExceptionalRootstock
()
558 submitUntilSequenceCatchup txs
= do
559 lift
$ putSequence
=<< (toInteger (length txs
) +) <$> gets nextSequence
560 submitUntilSequenceCatchup
' txs
562 clearAndUpdate
:: ExceptionalRootstock
()
563 {- Must have subscribed to ledger updates for this to work -}
565 Offers offerList
<- lift getAccountOffers
568 acInfo
<- lift getAccountInfo
569 acLines
<- lift getAccountLines
570 vs
<- lift
$ gets valueSimplex
571 let vs
' = updatedValueSimplex vs acInfo acLines
572 when (status
(~~
=) vs
' /= OK
) $ error "Invalid updated ValueSimplex!"
574 unless (strictlySuperior
(~~
=) vs
' vs
) $ do
576 vs
'' = updatedValueSimplexWithGenerosity generosity vs acInfo acLines
578 = " non-superior ValueSimplex (generosity: "
579 `T
.append` T
.pack
(show generosity
)
581 if strictlySuperior
(~~
=) vs
'' vs
582 then warn
$ "Slightly" `T
.append` warning
583 else error $ "Seriously" ++ T
.unpack warning
584 writeValueSimplex acInfo acLines vs
'
585 lift
$ putValueSimplex vs
'
587 curSeq
<- lift
$ gets nextSequence
588 submitUntilSequenceCatchup
$ zipWith
589 (\off sequ
-> OfferCancel sequ
$ offerSequence off
)
594 getUpdatedValueSimplexWithAccountInfo
::
595 AccountInfo
-> RootstockIO ValueSimplexND
596 getUpdatedValueSimplexWithAccountInfo acInfo
=
597 updatedValueSimplex
<$> gets valueSimplex
<*> pure acInfo
<*> getAccountLines
599 getUpdatedValueSimplex
:: RootstockIO ValueSimplexND
600 getUpdatedValueSimplex
=
601 getUpdatedValueSimplexWithAccountInfo
=<< getAccountInfo
603 strictlySuperiorToCurrent
:: ValueSimplexND
-> RootstockIO
Bool
604 strictlySuperiorToCurrent vs
' = strictlySuperior
(~~
=) vs
' <$> gets valueSimplex
606 waitForImprovement
:: ExceptionalRootstock
()
607 waitForImprovement
= do
609 unlessM
(lift
$ strictlySuperiorToCurrent
=<< getUpdatedValueSimplex
) $ do
610 lift
(waitForType
:: RootstockIO Ledger
)
611 lift
(waitForType
:: RootstockIO RecordedTransaction
)
614 submitAndWait
:: [Transaction
] -> ExceptionalRootstock
()
615 submitAndWait txs
= do
616 submitUntilSequenceCatchup txs
619 getTransitRates
:: RootstockIO
(NodeEntity
-> Double)
621 peers
<- catMaybes . Set
.toList
. Set
.map peerOfNodeEntity
. nodes
622 <$> gets valueSimplex
623 trm
<- buildMap peers
$ \peer
-> transferRate
<$> getCurrentAccountInfo peer
624 return $ \x
-> fromMaybe 1 $ peerOfNodeEntity x
>>= flip Map
.lookup trm
626 startRunning
:: RootstockIO
()
628 mavs
<- runSqlQuery
$ do
629 mcurAc
<- getCurrentAction
631 Nothing
-> error $ show DatabaseNotSetUp
633 if actionEntityFinished curAc
635 actId
<- startAction Running
636 vs
<- readValueSimplex
637 return $ Just
(actId
, vs
)
641 waitForType
:: RootstockIO Ledger
643 Just
(actId
, vs
) -> do
648 ensureRunning
:: RootstockIO
()
650 unlessM
(join $ runSqlQuery
<$> ownActionGoingQuery
)
653 marketMakerLoop
:: RootstockIO
()
659 <$> gets valueSimplex
661 <*> gets nextSequence
668 --------------------------------------------------------------------------------
669 getLineBal
:: AccountLines
-> IOULine
-> ExceptionalRootstock
Double
670 getLineBal acLines fundLine
= do
671 lineBal
<- case lookupLine acLines fundLine
of
672 Nothing
-> throwError LineNotFound
673 Just amount
-> return $ getQuantity amount
674 throwIf NonPositiveLine
$ lineBal
<= 0
677 setupDatabase
:: IOULine
-> ExceptionalRootstock
()
678 setupDatabase fundLine
= do
679 isEmpt
<- lift
$ valueSimplexEmpty
680 throwIf DatabaseExists
$ not isEmpt
681 lift
$ runWebsocket subscribeLedger
682 acInfo
<- lift getAccountInfo
683 let dropsBal
= getQuantity
$ lookupXRP acInfo
684 throwIf InsufficientForReserve
$ dropsBal
<= 0
685 acLines
<- lift getAccountLines
686 lineBal
<- getLineBal acLines fundLine
687 lift
$ runSqlQuery
$ do
688 xrpNodeEntity
<- insertReturnEntity
$ Node
{nodeFund
= XRP
}
689 lineNodeEntity
<- insertReturnEntity
$ Node
{nodeFund
= IOUFund fundLine
}
690 writeValueSimplex acInfo acLines
$
691 flip fromFunction
(Set
.fromList
[xrpNodeEntity
, lineNodeEntity
]) $ \x _
->
692 if x
== xrpNodeEntity
696 addCurrency
:: IOULine
-> Double -> ExceptionalRootstock
()
697 addCurrency fundLine priceInDrops
= do
698 mxrpNodeEntity
<- lift
$ runSqlQuery
$ getBy
$ NodeUnique XRP
699 xrpNodeEntity
<- maybe (throwError DatabaseNotSetUp
) return mxrpNodeEntity
700 throwIf NonPositivePrice
$ priceInDrops
<= 0
701 let lineFund
= IOUFund fundLine
703 isJust <$> (lift
$ runSqlQuery
$ getBy
$ NodeUnique lineFund
)
704 throwIf CurrencyAlreadyPresent alreadyPresent
705 lift
$ runWebsocket subscribeLedgerAndAccount
706 lift
$ getAndPutSequence
708 acLines
<- lift getAccountLines
709 lineBal
<- getLineBal acLines fundLine
710 vs
<- lift
$ gets valueSimplex
711 throwIf NewOutweighsOld
$
712 priceInDrops
* lineBal
>= totalValue vs xrpNodeEntity
713 acInfo
<- lift getAccountInfo
714 lift
$ runSqlQuery
$ do
715 lineNodeEntity
<- insertReturnEntity
$ Node
{nodeFund
= lineFund
}
716 writeValueSimplex acInfo acLines
$
717 addNode vs lineNodeEntity lineBal xrpNodeEntity priceInDrops
719 report
:: RootstockIO
()
721 now
<- liftIO getCurrentTime
722 (vs
, lastInterventionTime
) <- runSqlQuery
$ do
723 [Value
(Just lastInterventionTime
)] <- select
$ from
$ \acEnt
-> do
724 where_
$ acEnt ^
. ActionLogAction
!=. val Running
725 orderBy
[desc
$ acEnt ^
. ActionLogStart
]
727 return $ acEnt ^
. ActionLogEnd
728 vs
<- readValueSimplexAt lastInterventionTime
729 return (vs
, lastInterventionTime
)
730 vs
' <- gets valueSimplex
733 let xys
= distinctPairsOneWay xs
734 let v
= sqrt .! linkValueSquared vs
735 let v
' = sqrt .! linkValueSquared vs
'
736 forM_ xys
$ \(x
, y
) -> mapM_ putStrLn
737 [ show $ nodeFund
$ entityVal x
738 , show $ nodeFund
$ entityVal y
741 ** ((60 * 60 * 24 * 365)
742 / (fromRational $ toRational $
743 diffUTCTime now lastInterventionTime
))
748 p
= flip (price vs
') x0
749 x0Gain
= flip sumWith xys
$ \(x
, y
) ->
750 2 * sqrt (p x
) * sqrt (p y
) * (v
' x y
- v x y
)
751 forM_ xs
$ \x
-> mapM_ putStrLn
752 [ show $ nodeFund
$ entityVal x
753 , show $ totalValue vs
' x
754 , show $ x0Gain
/ p x
759 --------------------------------------------------------------------------------
760 runRootstock
:: RootstockIO a
-> Rootstock
-> IO a
761 runRootstock
= evalStateT
763 marketMaker
:: RootstockIO
()
765 isEmpt
<- valueSimplexEmpty
766 when isEmpt
$ error $ show DatabaseNotSetUp
767 runWebsocket subscribeLedgerAndAccount
770 liftIO
$ catch (runRootstock marketMakerLoop rs
) $ \e
-> do
771 flip runSqlPersistM
(sql rs
) $ do
772 curAc
<- fromJust <$> getCurrentAction
773 if actionRunning curAc
777 $ fromException e `
elem`
map Just
[ThreadKilled
, UserInterrupt
]
779 putStrLn $ "Exiting on: " ++ show e
781 rippleInteract
:: WS
.ClientApp
()
782 rippleInteract conn
= do
783 -- Fork a thread that writes WS data to stdout
784 _
<- forkIO
$ forever
$ do
785 msg
<- WS
.receiveData conn
786 liftIO
$ T
.putStrLn msg
788 runRipple subscribeAccount
790 -- Read from stdin and write to WS
793 unless (T
.null line
) $ WS
.sendTextData conn line
>> loop
796 WS
.sendClose conn
("Bye!" :: Text
)
798 readSecret
:: IO String
799 readSecret
= readFile secretFile
801 readSqlPass
:: IO BS
.ByteString
802 readSqlPass
= BS
.pack
<$> readFile sqlPassFile
804 runRipple
:: WS
.ClientApp a
-> IO a
805 runRipple app
= WS
.runClient
"127.0.0.1" 5006 "/" app
807 runRippleWithSecret
:: RootstockIO a
-> IO a
808 runRippleWithSecret app
= do
810 sqlPass
<- readSqlPass
811 withPostgresqlConn
(BS
.concat [connString
, sqlPass
]) $ \sqlConn
-> do
812 vs
<- flip runSqlPersistM sqlConn
$ do
813 runMigration migrateAll
815 runRipple
$ \wsConn
->
816 runRootstock app
$ Rootstock
822 , rsAction
= noAction
829 ["setup", currency
, peer
] -> runRippleWithSecret
$ intervene InitialSetup
$
830 setupDatabase
$ IOULine
831 { peerAccount
= T
.pack peer
832 , lineCurrency
= T
.pack currency
834 ["run"] -> runRippleWithSecret marketMaker
835 ["addCurrency", currency
, peer
, priceInXRP
] ->
836 runRippleWithSecret
$ intervene AddNode
$ addCurrency
838 { peerAccount
= T
.pack peer
839 , lineCurrency
= T
.pack currency
842 $ read priceInXRP
* 1000000
843 ["report"] -> runRippleWithSecret report
844 ["interact"] -> runRipple rippleInteract
845 _
-> putStrLn "Command not understood"