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
25 import Crypto
.Random
(SystemRandom
, newGenIO
)
26 import Crypto
.Types
.PubKey
.ECDSA
(PrivateKey
)
28 import Data
.Aeson
.Types
29 import Data
.Base58Address
(RippleAddress
)
30 import qualified Data
.Binary
as B
31 import Data
.Bits
((.|
.))
32 import qualified Data
.ByteString
.Base16
.Lazy
as H
33 import qualified Data
.ByteString
.Char8
as BS
34 import qualified Data
.ByteString
.Lazy
.Char8
as BSL8
35 import Data
.Foldable
(forM_
, toList
)
36 import Data
.List
(find, intersperse)
38 import qualified Data
.Map
as Map
39 import Data
.Maybe (catMaybes, fromJust, fromMaybe, isJust, listToMaybe)
41 import qualified Data
.Set
as Set
42 import Data
.Text
(Text
)
43 import qualified Data
.Text
as T
44 import qualified Data
.Text
.IO as T
45 import Data
.Time
.Clock
46 import Data
.Word
(Word32
)
47 import Database
.Esqueleto
hiding ((=.), get
, update
)
48 import Database
.Persist
.Postgresql
hiding ((==.), (<=.), (!=.), get
, update
)
49 import qualified Database
.Persist
.Postgresql
as P
50 import Database
.Persist
.TH
52 import qualified Network
.WebSockets
as WS
53 import Numeric
(showFFloat)
54 import qualified Ripple
.Amount
as RH
55 import Ripple
.Seed
(getSecret
)
56 import Ripple
.Sign
(signTransaction
)
57 import Ripple
.Transaction
58 import Ripple
.WebSockets
(RippleResult
(RippleResult
))
59 import RootstockException
(RootstockException
(..))
60 import System
.Environment
(getArgs)
61 import Util
.ApproxEq
((~~
=))
62 import Util
.Either (doLeft
, isRight
)
63 import Util
.Error
(throwIf
)
64 import Util
.Foldable
(sumWith
)
65 import Util
.Function
((.!))
66 import Util
.Monad
((>>=*), buildMap
)
67 import Util
.Persist
(insertReturnEntity
)
68 import Util
.Set
(distinctPairs
, distinctPairsOneWay
)
72 --------------------------------------------------------------------------------
73 data AccountInfo
= AccountInfo
74 { dropsBalance
:: Integer
75 , currentSequence
:: Word32
76 , transferRate
:: Double
79 data IOUAmount
= IOUAmount
81 , iouQuantity
:: Double
85 newtype AccountLines
= AccountLines
[IOUAmount
]
95 , offerSequence
:: Word32
98 newtype Offers
= Offers
[Offer
]
100 data BookOffer
= BookOffer
101 { bookOfferTakerGets
:: RH
.Amount
102 , bookOfferTakerPays
:: RH
.Amount
103 , bookOfferQuality
:: Double
104 , bookOfferTakerGetsFunded
:: Maybe RH
.Amount
105 , bookOfferTakerPaysFunded
:: Maybe RH
.Amount
108 newtype BookOffers
= BookOffers
[BookOffer
]
111 { ledgerIndex
:: Integer
115 data RecordedTransaction
= RecordedTransaction
117 share
[mkPersist sqlSettings
, mkMigrate
"migrateAll"] [persistLowerCase|
127 FundStatusUnique fundId time
133 HalfLinkUnique root branch time
145 type NodeEntity
= Entity Node
146 type ValueSimplexND
= ValueSimplex NodeEntity
Double
148 data Rootstock
= Rootstock
149 { secret
:: PrivateKey
150 , websocket
:: WS
.Connection
152 , valueSimplex
:: ValueSimplexND
153 , nextSequence
:: Word32
154 , rsAction
:: ActionLogId
155 , randGen
:: SystemRandom
158 type RootstockIO
= StateT Rootstock
IO
159 type ExceptionalRootstock
= ErrorT RootstockException RootstockIO
162 --------------------------------------------------------------------------------
163 instance ToJSON Amount
where
164 toJSON
(Drops numDrops
) = toJSON
$ show numDrops
165 toJSON
(IOU iou
) = object
166 [ "currency" .= lineCurrency
(iouLine iou
)
167 , "issuer" .= peerAccount
(iouLine iou
)
168 , "value" .= showFFloat Nothing
(iouQuantity iou
) ""
171 instance FromJSON AccountInfo
where
172 parseJSON
(Object obj
) = do
173 accountData
<- obj
.: "account_data"
175 <$> (accountData
.: "Balance" >>= return . read)
176 <*> accountData
.: "Sequence"
177 <*> (maybe 1 (/1000000000) <$> accountData
.:?
"TransferRate")
178 parseJSON
value = fail $
179 "Not an account info response:\n" ++ (BSL8
.unpack
$ encode
value)
181 instance FromJSON IOUAmount
where
182 parseJSON
(Object obj
) = IOUAmount
185 <*> obj
.: "currency")
186 <*> (obj
.: "balance" >>= return . read)
187 parseJSON
value = fail $
188 "Not an account line:\n" ++ (BSL8
.unpack
$ encode
value)
190 instance FromJSON AccountLines
where
191 parseJSON
(Object obj
) = AccountLines
<$> obj
.: "lines"
192 parseJSON
value = fail $
193 "Not a list of account lines:\n" ++ (BSL8
.unpack
$ encode
value)
195 instance FromJSON Amount
where
196 parseJSON
(Object obj
) = IOU
<$> (IOUAmount
199 <*> obj
.: "currency")
200 <*> (obj
.: "value" >>= return . read))
201 parseJSON
(String str
) = return $ Drops
$ read $ T
.unpack str
202 parseJSON
value = fail $
203 "Not an Amount:\n" ++ (BSL8
.unpack
$ encode
value)
205 instance FromJSON Offer
where
206 parseJSON
(Object obj
) = Offer
207 <$> obj
.: "taker_gets"
208 <*> obj
.: "taker_pays"
210 parseJSON
value = fail $
211 "Not an offer:\n" ++ (BSL8
.unpack
$ encode
value)
213 instance FromJSON Offers
where
214 parseJSON
(Object obj
) = Offers
<$> obj
.: "offers"
215 parseJSON
value = fail $
216 "Not a list of offers:\n" ++ (BSL8
.unpack
$ encode
value)
218 instance FromJSON BookOffer
where
219 parseJSON
(Object obj
) = BookOffer
220 <$> obj
.: "TakerGets"
221 <*> obj
.: "TakerPays"
223 <*> obj
.:?
"taker_gets_funded"
224 <*> obj
.:?
"taker_pays_funded"
225 parseJSON
value = fail $ "Not a book offer:\n" ++ (BSL8
.unpack
$ encode
value)
227 instance FromJSON BookOffers
where
228 parseJSON
(Object obj
) = BookOffers
<$> obj
.: "offers"
229 parseJSON
value = fail $
230 "Not a list of book offers:\n" ++ (BSL8
.unpack
$ encode
value)
232 instance FromJSON Ledger
where
233 parseJSON
(Object obj
) = Ledger
234 <$> obj
.: "ledger_index"
236 parseJSON
value = fail $
237 "Not a ledger:\n" ++ (BSL8
.unpack
$ encode
value)
239 instance FromJSON RecordedTransaction
where
240 parseJSON
(Object obj
) = do
241 objType
<- obj
.: "type"
242 if objType
== ("transaction" :: Text
)
243 then return RecordedTransaction
245 "Not a recorded transaction:\n" ++ (BSL8
.unpack
$ encode
$ Object obj
)
246 parseJSON
value = fail $
247 "Not a recorded transaction:\n" ++ (BSL8
.unpack
$ encode
value)
250 --------------------------------------------------------------------------------
251 secretFile
, sqlPassFile
:: FilePath
252 secretFile
= "/media/mishael/ripple-secret"
253 sqlPassFile
= "/media/mishael/sql-password"
255 connString
:: BS
.ByteString
256 connString
= BS
.concat
257 [ "host=localhost port=5432 dbname=rootstock-test"
258 , " user=rootstock password="
262 account
= "rpY4wdftAiEH5y5uzxC2XAvy3G27UyeQKS"
264 accountAddress
:: RippleAddress
265 accountAddress
= read $ T
.unpack account
267 feeInDrops
:: Integer
271 fee
= RH
.Amount
(toRational feeInDrops
/ 1000000) RH
.XRP
276 generosity
, halfSpread
:: Double
280 noAction
:: ActionLogId
281 noAction
= Key PersistNull
283 lookupXRP
:: AccountInfo
-> Amount
284 lookupXRP acInfo
= Drops
$ dropsBalance acInfo
- reserve
286 lookupLine
:: AccountLines
-> IOULine
-> Maybe Amount
287 lookupLine
(AccountLines
lines) fundLine
= do
288 foundLine
<- find ((fundLine
==) . iouLine
) lines
289 return $ IOU foundLine
291 lookupFund
:: AccountInfo
-> AccountLines
-> Fund
-> Maybe Amount
292 lookupFund acInfo _ XRP
= Just
$ lookupXRP acInfo
293 lookupFund _ acLines
(IOUFund fundLine
) = lookupLine acLines fundLine
295 getQuantity
:: Amount
-> Double
296 getQuantity
(Drops n
) = fromInteger n
297 getQuantity
(IOU iou
) = iouQuantity iou
299 firstSequence
:: [Field
] -> Word32
301 firstSequence
(SequenceNumber x
: _
) = x
302 firstSequence
(_
:fs
) = firstSequence fs
304 getSequence
:: Transaction
-> Word32
305 getSequence
(Transaction fs
) = firstSequence fs
307 nodeEntityFund
:: NodeEntity
-> Fund
308 nodeEntityFund
= nodeFund
. entityVal
310 lookupGetQuantity
:: AccountInfo
-> AccountLines
-> NodeEntity
-> Double
311 lookupGetQuantity acInfo acLines
=
312 fromMaybe 0 . liftM getQuantity
.
313 lookupFund acInfo acLines
. nodeEntityFund
315 fromNodeEntity
:: a
-> (IOULine
-> a
) -> NodeEntity
-> a
316 fromNodeEntity d f x
= case nodeEntityFund x
of
320 amount
:: Double -> NodeEntity
-> Amount
322 fromNodeEntity
(Drops
$ round q
) $ \l
->
323 IOU
$ IOUAmount
{iouLine
= l
, iouQuantity
= q
}
325 peerOfNodeEntity
:: NodeEntity
-> Maybe Text
326 peerOfNodeEntity
= fromNodeEntity Nothing
$ Just
. peerAccount
328 actionFinished
:: ActionLog
-> Bool
329 actionFinished
= isJust . actionLogEnd
331 actionEntityFinished
:: Entity ActionLog
-> Bool
332 actionEntityFinished
= actionFinished
. entityVal
334 actionRunning
:: Entity ActionLog
-> Bool
335 actionRunning acEnt
=
336 actionLogAction
(entityVal acEnt
) == Running
337 && not (actionEntityFinished acEnt
)
339 updatedValueSimplexWithGenerosity
::
340 Double -> ValueSimplexND
-> AccountInfo
-> AccountLines
-> ValueSimplexND
341 updatedValueSimplexWithGenerosity gen vs acInfo acLines
=
342 multiUpdate vs
$ \nodeEnt
->
343 let actual
= lookupGetQuantity acInfo acLines nodeEnt
in
344 case nodeEntityFund nodeEnt
of
348 updatedValueSimplex
::
349 ValueSimplexND
-> AccountInfo
-> AccountLines
-> ValueSimplexND
350 updatedValueSimplex
= updatedValueSimplexWithGenerosity
0
352 toRHAmount
:: Amount
-> RH
.Amount
353 toRHAmount
(Drops x
) = RH
.Amount
(toRational x
/ 1000000) RH
.XRP
354 toRHAmount
(IOU x
) = let
356 [a
, b
, c
] = T
.unpack
$ lineCurrency line
358 RH
.Amount
(toRational $ iouQuantity x
)
359 $ RH
.Currency
(a
, b
, c
) $ read $ T
.unpack
$ peerAccount line
361 commonTransactionStuff
:: Word32
-> [Transaction
] -> [Transaction
]
362 commonTransactionStuff nextSeq
= zipWith
363 (\sequ
(Transaction fs
) -> Transaction
$
364 [ Account accountAddress
366 , SequenceNumber sequ
372 ValueSimplexND
-> Double -> NodeEntity
-> Maybe (Double, ValueSimplexND
)
373 pivotFee vs feeDrops x
=
376 fromJust $ find ((XRP
==) . nodeEntityFund
) $ toList
$ nodes vs
378 if x
== xrpNodeEntity
379 then Just
(feeDrops
, vs
)
380 else if feeDrops
>= supremumSellable vs xrpNodeEntity x
382 else let f
= breakEven vs xrpNodeEntity
(-feeDrops
) x
in
383 Just
(f
, update vs xrpNodeEntity
(-feeDrops
) x f
)
390 -> (NodeEntity
-> Double)
394 maybeOfferCreate vs vs
' x0 x1 trf f immediate
= do
396 linkBreakEvenAtPriceWithFee vs
' x0 x1
(halfSpread
* price vs x0 x1
) f
398 [ TransactionType OfferCreate
399 , Flags
$ tfSell
.|
. (if immediate
then tfImmediateOrCancel
else 0)
400 , TakerPays
$ toRHAmount
$ amount q1 x1
401 , TakerGets
$ toRHAmount
$ amount
(-q0
/ trf x0
) x0
405 ValueSimplexND
-> (NodeEntity
-> Double) -> Word32
-> [Transaction
]
406 makeTransactions vs trf nextSeq
= commonTransactionStuff nextSeq
$ do
407 let xs
= toList
$ nodes vs
408 n
= toInteger $ length xs
411 (pivotFee vs
(fromInteger $ (n
* (n
- 1) * 2 - 1) * feeInDrops
) x1
)
416 else toList
$ maybeOfferCreate vs vs
' x0 x1 trf f
False
418 --------------------------------------------------------------------------------
419 getSqlConnection
:: RootstockIO Connection
420 getSqlConnection
= gets sql
422 runSqlQuery
:: SqlPersistM a
-> RootstockIO a
423 runSqlQuery query
= do
424 sqlConn
<- getSqlConnection
425 lift
$ runSqlPersistM query sqlConn
427 getNodeEntities
:: SqlPersistM
[NodeEntity
]
428 getNodeEntities
= select
$ from
return
430 readValueSimplexAt
:: UTCTime
-> SqlPersistM ValueSimplexND
431 readValueSimplexAt time
= do
432 nodeSet
<- Set
.fromList
<$> getNodeEntities
433 qMap
<- buildMap
(toList
$ distinctPairs nodeSet
) $ \(x
, y
) -> do
434 [Value q
] <- select
$ from
$ \hl
-> do
436 $ hl ^
. HalfLinkRoot
==. val
(entityKey x
)
437 &&. hl ^
. HalfLinkBranch
==. val
(entityKey y
)
438 &&. hl ^
. HalfLinkTime
<=. val time
439 orderBy
[desc
$ hl ^
. HalfLinkTime
]
441 return $ hl ^
. HalfLinkQuantity
443 return $ fromFunction
(curry $ flip (Map
.findWithDefault
0) qMap
) nodeSet
445 readValueSimplex
:: SqlPersistM ValueSimplexND
446 readValueSimplex
= liftIO getCurrentTime
>>= readValueSimplexAt
449 AccountInfo
-> AccountLines
-> ValueSimplexND
-> SqlPersistM
()
450 writeValueSimplex acInfo acLines vs
= do
451 time
<- liftIO getCurrentTime
452 insertMany
$ flip map (toList
$ nodes vs
) $ \nodeEnt
-> FundStatus
453 { fundStatusFundId
= entityKey nodeEnt
454 , fundStatusQuantity
= lookupGetQuantity acInfo acLines nodeEnt
455 , fundStatusTime
= time
457 forM_
(distinctPairs
$ nodes vs
) $ \(x
, y
) -> insert_
$ HalfLink
458 { halfLinkRoot
= entityKey x
459 , halfLinkBranch
= entityKey y
460 , halfLinkQuantity
= vsLookup vs x y
461 , halfLinkTime
= time
464 warn
:: Text
-> SqlPersistM
()
466 now
<- liftIO getCurrentTime
468 { warningWarning
= warning
472 getCurrentAction
:: SqlPersistM
(Maybe (Entity ActionLog
))
473 getCurrentAction
= liftM listToMaybe $ select
$ from
$ \ac
-> do
474 orderBy
[desc
$ ac ^
. ActionLogStart
]
478 startAction
:: Action
-> SqlPersistM ActionLogId
479 startAction action
= do
480 start
<- liftIO getCurrentTime
482 { actionLogAction
= action
483 , actionLogStart
= start
484 , actionLogEnd
= Nothing
485 , actionLogSuccess
= Nothing
488 endAction
:: ActionLogId
-> Bool -> SqlPersistM
()
489 endAction actionId success
= do
490 end
<- liftIO getCurrentTime
492 [ ActionLogEnd
=. Just end
493 , ActionLogSuccess
=. Just success
496 putAction
:: ActionLogId
-> RootstockIO
()
497 putAction actionId
= modify
$ \rs
-> rs
{rsAction
= actionId
}
499 intervene
:: Action
-> ExceptionalRootstock
() -> RootstockIO
()
500 intervene action intervention
= do
501 actionId
<- runSqlQuery
$ do
502 awhenM getCurrentAction
$ \curAc
->
503 unless (actionEntityFinished curAc
) $
504 if actionLogAction
(entityVal curAc
) == Running
505 then endAction
(entityKey curAc
) True
506 else error "Another intervention appears to be running"
509 result
<- runErrorT intervention
510 doLeft
(lift
. putStrLn . show) result
511 runSqlQuery
$ endAction actionId
$ isRight result
514 --------------------------------------------------------------------------------
515 runWebsocket
:: WS
.ClientApp a
-> RootstockIO a
516 runWebsocket app
= gets websocket
>>= lift
. app
518 receiveData
:: WS
.WebSocketsData a
=> RootstockIO a
519 receiveData
= runWebsocket WS
.receiveData
521 sendTextData
:: WS
.WebSocketsData a
=> a
-> RootstockIO
()
522 sendTextData x
= runWebsocket
$ flip WS
.sendTextData x
524 waitForType
:: FromJSON a
=> RootstockIO a
526 encoded
<- receiveData
527 case decode encoded
of
529 lift
$ putStrLn ("Skipping:\n" ++ (BSL8
.unpack encoded
))
532 lift
$ putStrLn ("Using:\n" ++ (BSL8
.unpack encoded
))
535 waitForResponseWithId
:: (Eq
id, FromJSON
id, FromJSON a
)
536 => id -> RootstockIO
(Maybe a
)
537 waitForResponseWithId idSought
= do
538 RippleResult i x
<- waitForType
539 if i
== Just idSought
540 then return $ either (const Nothing
) Just x
541 else waitForResponseWithId idSought
543 askUntilAnswered
:: FromJSON a
=> [Pair
] -> RootstockIO a
544 askUntilAnswered question
= do
545 qTime
<- show <$> liftIO getCurrentTime
546 sendTextData
$ encode
$ object
$ ("id" .= qTime
) : question
547 aifM
(waitForResponseWithId qTime
) return $ do
548 waitForType
:: RootstockIO Ledger
549 askUntilAnswered question
551 signAndSubmit
:: Transaction
-> RootstockIO
()
552 signAndSubmit tx
= do
553 Right
(txSigned
, rGen
) <- signTransaction tx
<$> gets secret
<*> gets randGen
554 modify
$ \rs
-> rs
{randGen
= rGen
}
555 sendTextData
$ encode
$ object
556 [ "command" .= ("submit" :: Text
)
557 , "tx_blob" .= BSL8
.unpack
(H
.encode
$ B
.encode txSigned
)
560 subscribe
:: [Pair
] -> WS
.ClientApp
()
562 flip WS
.sendTextData
$ encode
$ object
$
563 ["command" .= ("subscribe" :: Text
)] ++ options
565 subscribeLedger
:: WS
.ClientApp
()
566 subscribeLedger
= subscribe
["streams" .= ["ledger" :: Text
]]
568 subscribeAccount
:: WS
.ClientApp
()
569 subscribeAccount
= subscribe
["accounts" .= [account
]]
571 subscribeLedgerAndAccount
:: WS
.ClientApp
()
572 subscribeLedgerAndAccount
= subscribe
573 [ "streams" .= ["ledger" :: Text
]
574 , "accounts" .= [account
]
577 queryOwnAccount
:: FromJSON a
=> Text
-> RootstockIO a
578 queryOwnAccount command
= askUntilAnswered
579 [ "command" .= command
580 , "account" .= account
581 , "ledger_index" .= ("validated" :: Text
)
584 getAccountInfo
:: RootstockIO AccountInfo
585 getAccountInfo
= queryOwnAccount
"account_info"
587 getAccountLines
:: RootstockIO AccountLines
588 getAccountLines
= queryOwnAccount
"account_lines"
590 getAccountOffers
:: RootstockIO Offers
591 getAccountOffers
= queryOwnAccount
"account_offers"
593 getCurrentAccountInfo
:: Text
-> RootstockIO AccountInfo
594 getCurrentAccountInfo peer
= askUntilAnswered
595 [ "command" .= ("account_info" :: Text
)
597 , "ledger_index" .= ("current" :: Text
)
600 valueSimplexEmpty
:: RootstockIO
Bool
601 valueSimplexEmpty
= isEmpty
<$> gets valueSimplex
603 putValueSimplex
:: ValueSimplexND
-> RootstockIO
()
604 putValueSimplex vs
= modify
$ \rs
-> rs
{valueSimplex
= vs
}
606 putSequence
:: Word32
-> RootstockIO
()
607 putSequence nextSeq
= modify
$ \rs
-> rs
{nextSequence
= nextSeq
}
609 getAndPutSequence
:: RootstockIO
()
611 currentSequence
<$> getCurrentAccountInfo account
>>= putSequence
613 ownActionGoingQuery
:: RootstockIO
(SqlPersistM
Bool)
614 ownActionGoingQuery
= do
615 actId
<- gets rsAction
616 return $ maybe False (not . actionFinished
) <$> P
.get actId
618 ifRunning
:: SqlPersistM
() -> ExceptionalRootstock
()
620 goingQ
<- lift ownActionGoingQuery
621 mapErrorT runSqlQuery
$ do
622 going
<- lift
$ goingQ
623 throwIf NotRunning
$ not going
626 checkRunning
:: ExceptionalRootstock
()
627 checkRunning
= ifRunning
$ return ()
629 submitUntilSequenceCatchup
' :: [Transaction
] -> ExceptionalRootstock
()
630 submitUntilSequenceCatchup
' txs
= unless (null txs
) $ do
632 forM_ txs
$ lift
. signAndSubmit
633 lift
(waitForType
:: RootstockIO Ledger
)
634 curSeq
<- currentSequence
<$> lift getAccountInfo
635 submitUntilSequenceCatchup
' $ dropWhile ((curSeq
>) . getSequence
) txs
637 submitUntilSequenceCatchup
:: [Transaction
] -> ExceptionalRootstock
()
638 submitUntilSequenceCatchup txs
= do
639 lift
$ putSequence
=<< (fromIntegral (length txs
) +) <$> gets nextSequence
640 submitUntilSequenceCatchup
' txs
642 clearAndUpdate
:: ExceptionalRootstock
()
643 {- Must have subscribed to ledger updates for this to work -}
645 Offers offerList
<- lift getAccountOffers
648 acInfo
<- lift getAccountInfo
649 acLines
<- lift getAccountLines
650 vs
<- lift
$ gets valueSimplex
651 let vs
' = updatedValueSimplex vs acInfo acLines
652 when (status
(~~
=) vs
' /= OK
) $ error "Invalid updated ValueSimplex!"
654 unless (strictlySuperior
(~~
=) vs
' vs
) $ do
656 vs
'' = updatedValueSimplexWithGenerosity generosity vs acInfo acLines
658 = " non-superior ValueSimplex (generosity: "
659 `T
.append` T
.pack
(show generosity
)
661 if strictlySuperior
(~~
=) vs
'' vs
662 then warn
$ "Slightly" `T
.append` warning
663 else error $ "Seriously" ++ T
.unpack warning
664 writeValueSimplex acInfo acLines vs
'
665 lift
$ putValueSimplex vs
'
667 curSeq
<- lift
$ gets nextSequence
668 submitUntilSequenceCatchup
$ commonTransactionStuff curSeq
$
669 flip map offerList
$ \off
-> Transaction
670 [ TransactionType OfferCancel
671 , OfferSequence
$ offerSequence off
675 strictlySuperiorToCurrent
:: ValueSimplexND
-> RootstockIO
Bool
676 strictlySuperiorToCurrent vs
' = strictlySuperior
(~~
=) vs
' <$> gets valueSimplex
678 waitForImprovement
:: ExceptionalRootstock
()
679 waitForImprovement
= do
681 Offers offerList
<- lift getAccountOffers
683 (lift
$ strictlySuperiorToCurrent
=<<
684 (updatedValueSimplexWithGenerosity
685 (fromInteger $ (negate feeInDrops
*) $ toInteger $ length offerList
)
686 <$> gets valueSimplex
690 lift
(waitForType
:: RootstockIO Ledger
)
691 lift
(waitForType
:: RootstockIO RecordedTransaction
)
694 submitAndWait
:: [Transaction
] -> ExceptionalRootstock
()
695 submitAndWait txs
= do
696 submitUntilSequenceCatchup txs
699 getTransitRates
:: RootstockIO
(NodeEntity
-> Double)
701 peers
<- catMaybes . toList
. Set
.map peerOfNodeEntity
. nodes
702 <$> gets valueSimplex
703 trm
<- buildMap peers
$ \peer
-> transferRate
<$> getCurrentAccountInfo peer
704 return $ \x
-> fromMaybe 1 $ peerOfNodeEntity x
>>= flip Map
.lookup trm
706 startRunning
:: RootstockIO
()
708 mavs
<- runSqlQuery
$ do
709 mcurAc
<- getCurrentAction
711 Nothing
-> error $ show DatabaseNotSetUp
713 if actionEntityFinished curAc
715 actId
<- startAction Running
716 vs
<- readValueSimplex
717 return $ Just
(actId
, vs
)
721 waitForType
:: RootstockIO Ledger
723 Just
(actId
, vs
) -> do
728 ensureRunning
:: RootstockIO
()
730 unlessM
(join $ runSqlQuery
<$> ownActionGoingQuery
)
733 marketMakerLoop
:: RootstockIO
()
739 <$> gets valueSimplex
741 <*> gets nextSequence
748 --------------------------------------------------------------------------------
749 getLineBal
:: AccountLines
-> IOULine
-> ExceptionalRootstock
Double
750 getLineBal acLines fundLine
= do
751 lineBal
<- case lookupLine acLines fundLine
of
752 Nothing
-> throwError LineNotFound
753 Just amount
-> return $ getQuantity amount
754 throwIf NonPositiveLine
$ lineBal
<= 0
757 setupDatabase
:: IOULine
-> ExceptionalRootstock
()
758 setupDatabase fundLine
= do
759 isEmpt
<- lift
$ valueSimplexEmpty
760 throwIf DatabaseExists
$ not isEmpt
761 lift
$ runWebsocket subscribeLedger
762 acInfo
<- lift getAccountInfo
763 let dropsBal
= getQuantity
$ lookupXRP acInfo
764 throwIf InsufficientForReserve
$ dropsBal
<= 0
765 acLines
<- lift getAccountLines
766 lineBal
<- getLineBal acLines fundLine
767 lift
$ runSqlQuery
$ do
768 xrpNodeEntity
<- insertReturnEntity
$ Node
{nodeFund
= XRP
}
769 lineNodeEntity
<- insertReturnEntity
$ Node
{nodeFund
= IOUFund fundLine
}
770 writeValueSimplex acInfo acLines
$
771 flip fromFunction
(Set
.fromList
[xrpNodeEntity
, lineNodeEntity
]) $ \x _
->
772 if x
== xrpNodeEntity
776 addCurrency
:: IOULine
-> Double -> ExceptionalRootstock
()
777 addCurrency fundLine priceInDrops
= do
778 mxrpNodeEntity
<- lift
$ runSqlQuery
$ getBy
$ NodeUnique XRP
779 xrpNodeEntity
<- maybe (throwError DatabaseNotSetUp
) return mxrpNodeEntity
780 throwIf NonPositivePrice
$ priceInDrops
<= 0
781 let lineFund
= IOUFund fundLine
783 isJust <$> (lift
$ runSqlQuery
$ getBy
$ NodeUnique lineFund
)
784 throwIf CurrencyAlreadyPresent alreadyPresent
785 lift
$ runWebsocket subscribeLedgerAndAccount
786 lift
$ getAndPutSequence
788 acLines
<- lift getAccountLines
789 lineBal
<- getLineBal acLines fundLine
790 vs
<- lift
$ gets valueSimplex
791 throwIf NewOutweighsOld
$
792 priceInDrops
* lineBal
>= totalValue vs xrpNodeEntity
793 acInfo
<- lift getAccountInfo
794 lift
$ runSqlQuery
$ do
795 lineNodeEntity
<- insertReturnEntity
$ Node
{nodeFund
= lineFund
}
796 writeValueSimplex acInfo acLines
$
797 addNode vs lineNodeEntity lineBal xrpNodeEntity priceInDrops
799 report
:: RootstockIO
()
801 now
<- liftIO getCurrentTime
802 (vs
, lastInterventionTime
) <- runSqlQuery
$ do
803 [Value
(Just lastInterventionTime
)] <- select
$ from
$ \acEnt
-> do
804 where_
$ acEnt ^
. ActionLogAction
!=. val Running
805 orderBy
[desc
$ acEnt ^
. ActionLogStart
]
807 return $ acEnt ^
. ActionLogEnd
808 vs
<- readValueSimplexAt lastInterventionTime
809 return (vs
, lastInterventionTime
)
810 vs
' <- gets valueSimplex
813 let xys
= distinctPairsOneWay xs
815 v
' = halfLinkValue vs
'
818 / (fromRational $ toRational $
819 diffUTCTime now lastInterventionTime
)
821 x0Value
= totalValue vs
' x0
822 x0Gain
= flip sumWith xys
$ \(x
, y
) ->
823 2 * hybridPrice vs
' x y x0
* (v
' x y
- v x y
)
824 forM_ xys
$ \(x
, y
) -> mapM_ putStrLn
825 [ show $ nodeEntityFund x
826 , show $ nodeEntityFund y
830 , show $ 2 * hybridPrice vs
' x y x0
* v
' x y
/ x0Value
833 forM_ xs
$ \x
-> mapM_ putStrLn
834 [ show $ nodeEntityFund x
835 , show $ totalValue vs
' x
836 , show $ x0Gain
/ price vs
' x x0
837 , show $ nodeValue vs
' x
/ totalValue vs
' x
840 putStrLn $ show $ (1 + x0Gain
/ x0Value
) ** recipYears
842 promptDeposit
:: ExceptionalRootstock
()
844 liftIO
$ putStrLn "Please wait while I cancel all offers ..."
845 lift
$ runWebsocket subscribeLedger
846 lift getAndPutSequence
848 liftIO
$ mapM_ putStrLn
850 , "Please deposit into account "
852 ++ " and press Enter to continue."
855 lift
(waitForType
:: RootstockIO Ledger
)
856 vs
<- lift
$ gets valueSimplex
857 acInfo
<- lift getAccountInfo
858 acLines
<- lift getAccountLines
859 lift
$ runSqlQuery
$ writeValueSimplex acInfo acLines
$
860 deposit vs
$ lookupGetQuantity acInfo acLines
863 --------------------------------------------------------------------------------
864 runRootstock
:: RootstockIO a
-> Rootstock
-> IO a
865 runRootstock
= evalStateT
867 marketMaker
:: RootstockIO
()
869 isEmpt
<- valueSimplexEmpty
870 when isEmpt
$ error $ show DatabaseNotSetUp
871 runWebsocket subscribeLedgerAndAccount
874 liftIO
$ catch (runRootstock marketMakerLoop rs
) $ \e
-> do
875 flip runSqlPersistM
(sql rs
) $ do
876 curAc
<- fromJust <$> getCurrentAction
877 if actionRunning curAc
881 $ fromException e `
elem`
map Just
[ThreadKilled
, UserInterrupt
]
883 putStrLn $ "Exiting on: " ++ show e
885 rippleInteract
:: WS
.ClientApp
()
886 rippleInteract conn
= do
887 -- Fork a thread that writes WS data to stdout
888 _
<- forkIO
$ forever
$ do
889 msg
<- WS
.receiveData conn
890 liftIO
$ T
.putStrLn msg
892 runRipple subscribeAccount
894 -- Read from stdin and write to WS
897 unless (T
.null line
) $ WS
.sendTextData conn line
>> loop
900 WS
.sendClose conn
("Bye!" :: Text
)
902 readSecret
:: IO String
903 readSecret
= readFile secretFile
905 readSqlPass
:: IO BS
.ByteString
906 readSqlPass
= BS
.pack
<$> readFile sqlPassFile
908 runRipple
:: WS
.ClientApp a
-> IO a
909 runRipple app
= WS
.runClient
"127.0.0.1" 5006 "/" app
911 runRippleWithSecret
:: RootstockIO a
-> IO a
912 runRippleWithSecret app
= do
914 sqlPass
<- readSqlPass
916 withPostgresqlConn
(BS
.concat [connString
, sqlPass
]) $ \sqlConn
-> do
917 vs
<- flip runSqlPersistM sqlConn
$ do
918 runMigration migrateAll
920 runRipple
$ \wsConn
->
921 runRootstock app
$ Rootstock
923 , secret
= getSecret
$ read sec
927 , rsAction
= noAction
935 ["setup", currency
, peer
] -> runRippleWithSecret
$ intervene InitialSetup
$
936 setupDatabase
$ IOULine
937 { peerAccount
= T
.pack peer
938 , lineCurrency
= T
.pack currency
940 ["run"] -> runRippleWithSecret marketMaker
941 ["addCurrency", currency
, peer
, priceInXRP
] ->
942 runRippleWithSecret
$ intervene AddNode
$ addCurrency
944 { peerAccount
= T
.pack peer
945 , lineCurrency
= T
.pack currency
948 $ read priceInXRP
* 1000000
949 ["report"] -> runRippleWithSecret report
950 ["interact"] -> runRipple rippleInteract
951 ["deposit"] -> runRippleWithSecret
$ intervene Deposit
$ promptDeposit
952 _
-> putStrLn "Command not understood"