1 --------------------------------------------------------------------------------
2 {-# LANGUAGE FlexibleContexts
14 --------------------------------------------------------------------------------
15 import Control
.Applicative
((<$>), (<*>))
16 import Control
.Concurrent
(forkIO
)
17 import Control
.Monad
(forever
, unless, foldM, liftM)
18 import Control
.Monad
.Trans
(lift
, liftIO
)
19 import Control
.Monad
.Trans
.Reader
21 import Data
.Aeson
.Types
22 import qualified Data
.ByteString
.Char8
as BS
23 import qualified Data
.ByteString
.Lazy
.Char8
as BSL8
24 import Data
.List
(find, intersperse)
25 import Data
.Maybe (isJust, listToMaybe)
26 import Data
.Text
(Text
)
27 import qualified Data
.Text
as T
28 import qualified Data
.Text
.IO as T
29 import Data
.Time
.Clock
30 import Database
.Esqueleto
31 import Database
.Persist
.Postgresql
hiding ((==.))
32 import Database
.Persist
.TH
34 import qualified Network
.WebSockets
as WS
35 import Numeric
(showFFloat)
36 import System
.Process
(readProcess
)
39 --------------------------------------------------------------------------------
40 data AccountInfo
= AccountInfo
41 { dropsBalance
:: Integer
42 , currentSequence
:: Integer
45 data IOUAmount
= IOUAmount
47 , iouQuantity
:: Double
51 newtype AccountLines
= AccountLines
[IOUAmount
]
59 = OfferCreate Amount Amount
Integer (Maybe Integer)
60 | OfferCancel
Integer Integer
66 , offerSequence
:: Integer
69 newtype Offers
= Offers
[Offer
]
72 { ledgerIndex
:: Integer
76 data RecordedTransaction
= RecordedTransaction
78 share
[mkPersist sqlSettings
, mkMigrate
"migrateAll"] [persistLowerCase|
84 HalfLinkUnique root branch timestamp
93 LinkStatusUnique linkId timestamp
95 data OldLink
= OldLink
98 , halfSpread
:: Double
99 , logFileName
:: FilePath
102 data Rootstock
= Rootstock
104 , websocket
:: WS
.Connection
108 type RootstockIO
= ReaderT Rootstock
IO
111 --------------------------------------------------------------------------------
112 instance ToJSON Amount
where
113 toJSON
(Drops numDrops
) = toJSON
$ show numDrops
114 toJSON
(IOU iou
) = object
115 [ "currency" .= lineCurrency
(iouLine iou
)
116 , "issuer" .= peerAccount
(iouLine iou
)
117 , "value" .= showFFloat Nothing
(iouQuantity iou
) ""
120 instance ToJSON Transaction
where
121 toJSON
(OfferCreate toSell toBuy curSeq maybeOldOfferSequence
) = object
$
122 [ "TransactionType" .= ("OfferCreate" :: Text
)
123 , "Account" .= account
125 , "Sequence" .= curSeq
127 , "TakerPays" .= toBuy
128 , "TakerGets" .= toSell
131 (\oldOfferSequence
-> ["OfferSequence" .= show oldOfferSequence
])
132 maybeOldOfferSequence
133 toJSON
(OfferCancel curSeq oldOfferSequence
) = object
$
134 [ "TransactionType" .= ("OfferCancel" :: Text
)
135 , "Account" .= account
137 , "Sequence" .= curSeq
138 , "OfferSequence" .= oldOfferSequence
141 instance FromJSON AccountInfo
where
142 parseJSON
(Object obj
) = do
143 result
<- obj
.: "result"
144 accountData
<- result
.: "account_data"
146 <$> (accountData
.: "Balance" >>= return . read)
147 <*> accountData
.: "Sequence"
148 parseJSON
value = fail $
149 "Not an account info response:\n" ++ (BSL8
.unpack
$ encode
value)
151 instance FromJSON IOUAmount
where
152 parseJSON
(Object obj
) = IOUAmount
155 <*> obj
.: "currency")
156 <*> (obj
.: "balance" >>= return . read)
157 parseJSON
value = fail $
158 "Not an account line:\n" ++ (BSL8
.unpack
$ encode
value)
160 instance FromJSON AccountLines
where
161 parseJSON
(Object obj
) = do
162 result
<- obj
.: "result"
163 AccountLines
<$> result
.: "lines"
164 parseJSON
value = fail $
165 "Not a list of account lines:\n" ++ (BSL8
.unpack
$ encode
value)
167 instance FromJSON Amount
where
168 parseJSON
(Object obj
) = IOU
<$> (IOUAmount
171 <*> obj
.: "currency")
172 <*> (obj
.: "value" >>= return . read))
173 parseJSON
(String str
) = return $ Drops
$ read $ T
.unpack str
174 parseJSON
value = fail $
175 "Not an Amount:\n" ++ (BSL8
.unpack
$ encode
value)
177 instance FromJSON Offer
where
178 parseJSON
(Object obj
) = Offer
179 <$> obj
.: "taker_gets"
180 <*> obj
.: "taker_pays"
182 parseJSON
value = fail $
183 "Not an offer:\n" ++ (BSL8
.unpack
$ encode
value)
185 instance FromJSON Offers
where
186 parseJSON
(Object obj
) = do
187 result
<- obj
.: "result"
188 Offers
<$> result
.: "offers"
189 parseJSON
value = fail $
190 "Not a list of offers:\n" ++ (BSL8
.unpack
$ encode
value)
192 instance FromJSON Ledger
where
193 parseJSON
(Object obj
) = Ledger
194 <$> obj
.: "ledger_index"
196 parseJSON
value = fail $
197 "Not a ledger:\n" ++ (BSL8
.unpack
$ encode
value)
199 instance FromJSON RecordedTransaction
where
200 parseJSON
(Object obj
) = do
201 objType
<- obj
.: "type"
202 if objType
== ("transaction" :: Text
)
203 then return RecordedTransaction
205 "Not a recorded transaction:\n" ++ (BSL8
.unpack
$ encode
$ Object obj
)
206 parseJSON
value = fail $
207 "Not a recorded transaction:\n" ++ (BSL8
.unpack
$ encode
value)
210 --------------------------------------------------------------------------------
211 secretFile
, rsignPath
, sqlPassFile
:: FilePath
212 secretFile
= "/home/tim/Documents/passwords/ripple-secret.gpg"
214 "/home/tim/build/ripple/ripple-lib/node_modules/ripple-lib/bin/rsign.js"
215 sqlPassFile
= "sql-password.gpg"
217 connString
:: BS
.ByteString
218 connString
= "host=localhost port=5432 user=tim dbname=rootstock password="
220 account
, bitStamp
, rippleCN
:: Text
221 account
= "rpY4wdftAiEH5y5uzxC2XAvy3G27UyeQKS"
222 bitStamp
= "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B"
223 rippleCN
= "rnuF96W4SZoCJmbHYBFoJZpR8eCaxNvekK"
225 fee
, tfSell
, reserve
:: Integer
230 btcBitstamp
, usdBitstamp
, cnyRippleCN
:: Fund
231 btcBitstamp
= IOUFund
$ IOULine
{peerAccount
= bitStamp
, lineCurrency
= "BTC"}
232 usdBitstamp
= IOUFund
$ IOULine
{peerAccount
= bitStamp
, lineCurrency
= "USD"}
233 cnyRippleCN
= IOUFund
$ IOULine
{peerAccount
= rippleCN
, lineCurrency
= "CNY"}
235 xrpbtc
, usdcny
:: OldLink
238 , rightFund
= btcBitstamp
240 , logFileName
= "XRPBTClog.csv"
243 { leftFund
= usdBitstamp
244 , rightFund
= cnyRippleCN
246 , logFileName
= "USDCNYlog.csv"
249 oldLinks
:: [OldLink
]
250 oldLinks
= [xrpbtc
, usdcny
]
252 lookupFund
:: AccountInfo
-> AccountLines
-> Fund
-> Maybe Amount
253 lookupFund acInfo _ XRP
= Just
$ Drops
$ dropsBalance acInfo
- reserve
254 lookupFund _
(AccountLines
lines) (IOUFund fundLine
) = do
255 foundLine
<- find ((fundLine
==) . iouLine
) lines
256 return $ IOU foundLine
258 getQuantity
:: Amount
-> Double
259 getQuantity
(Drops n
) = fromInteger n
260 getQuantity
(IOU iou
) = iouQuantity iou
262 setQuantity
:: Amount
-> Double -> Amount
263 setQuantity
(Drops _
) q
= Drops
$ round q
264 setQuantity
(IOU iou
) q
= IOU
$ iou
{ iouQuantity
= q
}
266 sellAtPrice
:: Amount
-> Amount
-> Double -> Double -> Double ->
268 sellAtPrice sellFrom buyTo sellFee buyFee p
=
270 q
= (getQuantity sellFrom
- sellFee
- (getQuantity buyTo
- buyFee
)/p
)/2
272 (setQuantity sellFrom q
, setQuantity buyTo
$ p
* q
)
274 sellAtHalfSpread
:: Amount
-> Amount
-> Double -> Double -> Double
276 sellAtHalfSpread sellFrom buyTo sellFee buyFee hSpread
=
277 sellAtPrice sellFrom buyTo sellFee buyFee
$
278 hSpread
* getQuantity buyTo
/ getQuantity sellFrom
280 validNoLoss
:: Amount
-> Amount
-> Double -> Double -> Amount
-> Amount
-> Bool
281 validNoLoss sellFrom buyTo sellFee buyFee toSell toBuy
=
283 sellFromQ
= getQuantity sellFrom
284 buyToQ
= getQuantity buyTo
285 toSellQ
= getQuantity toSell
286 toBuyQ
= getQuantity toBuy
288 toSellQ
> 0 && toBuyQ
> 0 &&
289 (sellFromQ
- toSellQ
- sellFee
) * (buyToQ
+ toBuyQ
- buyFee
) >=
292 fund
:: Amount
-> Fund
294 fund
(IOU iou
) = IOUFund
$ iouLine iou
296 lookupOffer
:: Offers
-> Fund
-> Fund
-> Maybe Offer
297 lookupOffer
(Offers offers
) toSell toBuy
= find
298 (\offer
-> fund
(takerGets offer
) == toSell
&&
299 fund
(takerPays offer
) == toBuy
)
302 lookupOfferSequence
:: Offers
-> Fund
-> Fund
-> Maybe Integer
303 lookupOfferSequence offers toSell toBuy
= do
304 foundOffer
<- lookupOffer offers toSell toBuy
305 return $ offerSequence foundOffer
307 bothOffersPresent
:: Offers
-> Link
-> Bool
308 bothOffersPresent offers link
=
311 right
= linkRight link
313 (isJust $ lookupOffer offers left right
) &&
314 (isJust $ lookupOffer offers right left
)
317 --------------------------------------------------------------------------------
318 getRootstock
:: RootstockIO Rootstock
321 getSqlConnection
:: RootstockIO Connection
322 getSqlConnection
= getRootstock
>>= return . sql
324 runSqlQuery
:: SqlPersistM a
-> RootstockIO a
325 runSqlQuery query
= do
326 sqlConn
<- getSqlConnection
327 lift
$ runSqlPersistM query sqlConn
329 receiveData
:: WS
.WebSocketsData a
=> RootstockIO a
330 receiveData
= getRootstock
>>= return . websocket
>>= lift
. WS
.receiveData
332 sendTextData
:: WS
.WebSocketsData a
=> a
-> RootstockIO
()
334 getRootstock
>>= return . websocket
>>= lift
. flip WS
.sendTextData x
336 signTransaction
:: Transaction
-> RootstockIO
String
337 signTransaction tx
= do
339 blobNewLine
<- lift
$ readProcess
340 rsignPath
[secret rs
, BSL8
.unpack
$ encode tx
] ""
341 return $ init blobNewLine
343 waitForType
:: FromJSON a
=> RootstockIO a
345 encoded
<- receiveData
346 case decode encoded
of
348 lift
$ putStrLn ("Skipping:\n" ++ (BSL8
.unpack encoded
))
351 lift
$ putStrLn ("Using:\n" ++ (BSL8
.unpack encoded
))
354 signAndSend
:: Transaction
-> RootstockIO
()
356 txBlob
<- signTransaction tx
357 sendTextData
$ encode
$ object
358 [ "command" .= ("submit" :: Text
)
359 , "tx_blob" .= txBlob
362 submitSellOffer
:: Amount
-> Amount
-> Double -> Double -> Double -> Offers
->
363 Integer -> RootstockIO
Integer
366 sellFee buyFee hSpread
369 (toSell
, toBuy
) = sellAtHalfSpread sellFrom buyTo sellFee buyFee hSpread
370 maybeOldOfferSequence
=
371 lookupOfferSequence offers
(fund sellFrom
) $ fund buyTo
372 tx
= OfferCreate toSell toBuy curSeq Nothing
373 lift
$ BSL8
.putStrLn $ encode tx
374 if validNoLoss sellFrom buyTo sellFee buyFee toSell toBuy
377 case maybeOldOfferSequence
of
378 Nothing
-> return $ curSeq
+ 1
379 Just oldOfferSequence
-> do
380 signAndSend
$ OfferCancel
(curSeq
+ 1) oldOfferSequence
383 lift
$ putStrLn "Skipping the above: either invalid or loss-making"
386 subscribeAccount
:: RootstockIO
()
388 sendTextData
$ encode
$ object
389 [ "command" .= ("subscribe" :: Text
)
390 , "accounts" .= [account
]
393 marketMaker
:: RootstockIO
()
398 getLinkEntities
:: RootstockIO
[Entity Link
]
399 getLinkEntities
= runSqlQuery
$ select
$ from
return
401 getLinks
:: RootstockIO
[Link
]
402 getLinks
= liftM (map entityVal
) getLinkEntities
404 waitForAction
:: RootstockIO Offers
406 sendTextData
$ encode
$ object
407 [ "command" .= ("account_offers" :: Text
)
408 , "account" .= account
410 offers
<- waitForType
412 if all (bothOffersPresent offers
) links
414 RecordedTransaction
<- waitForType
418 currentLinkStatus
:: LinkId
-> RootstockIO
(Maybe LinkStatus
)
419 currentLinkStatus linkId
= do
420 statusList
<- runSqlQuery
$ select
$ from
$ \status
-> do
421 where_
$ status ^
. LinkStatusLinkId
==. val linkId
422 orderBy
[desc
$ status ^
. LinkStatusTimestamp
]
425 return $ liftM entityVal
$ listToMaybe statusList
427 submitLinkOffers
:: AccountInfo
-> AccountLines
->
428 Offers
-> Entity Link
-> Integer -> RootstockIO
Integer
429 submitLinkOffers acInfo acLines offers
(Entity linkId link
) curSeq
= do
430 if bothOffersPresent offers link
433 maybeHSpread
<- liftM (liftM linkStatusHalfSpread
) $
434 currentLinkStatus linkId
437 right
= linkRight link
438 feeForCalcs XRP
= fromInteger $ 3 * fee
440 leftFeeForCalcs
= feeForCalcs left
441 rightFeeForCalcs
= feeForCalcs right
443 ( lookupFund acInfo acLines left
444 , lookupFund acInfo acLines right
447 (Just leftTotal
, Just rightTotal
, Just hSpread
) -> do
448 now
<- lift getCurrentTime
450 insert $ HalfLink left right
(getQuantity leftTotal
) now
451 insert $ HalfLink right left
(getQuantity rightTotal
) now
452 nextSeq
<- submitSellOffer
454 leftFeeForCalcs rightFeeForCalcs hSpread
458 rightFeeForCalcs leftFeeForCalcs hSpread
462 "The link status or one of the lines of credit wasn't found"
465 marketMakerLoop
:: RootstockIO
()
467 offers
<- waitForAction
468 sendTextData
$ encode
$ object
469 [ "command" .= ("account_info" :: Text
)
470 , "account" .= account
472 acInfo
<- waitForType
473 sendTextData
$ encode
$ object
474 [ "command" .= ("account_lines" :: Text
)
475 , "account" .= account
477 acLines
<- waitForType
478 linkEntities
<- getLinkEntities
479 foldM (flip ($)) (currentSequence acInfo
) $
480 map (submitLinkOffers acInfo acLines offers
) linkEntities
483 insertOldLink
:: OldLink
-> RootstockIO
()
484 insertOldLink link
= do
485 now
<- lift getCurrentTime
487 linkId
<- insert $ Link
(leftFund link
) (rightFund link
)
488 insert $ LinkStatus linkId
(halfSpread link
) now
492 --------------------------------------------------------------------------------
493 runRootstock
:: RootstockIO a
-> Rootstock
-> IO a
494 runRootstock
= runReaderT
496 rippleInteract
:: WS
.ClientApp
()
497 rippleInteract conn
= do
498 -- Fork a thread that writes WS data to stdout
499 _
<- forkIO
$ forever
$ do
500 msg
<- WS
.receiveData conn
501 liftIO
$ T
.putStrLn msg
503 runRootstock subscribeAccount
$ Rootstock
509 -- Read from stdin and write to WS
512 unless (T
.null line
) $ WS
.sendTextData conn line
>> loop
515 WS
.sendClose conn
("Bye!" :: Text
)
517 readSecret
:: IO String
518 readSecret
= readProcess
"gpg" ["-o", "-", secretFile
] ""
520 readSqlPass
:: IO BS
.ByteString
521 readSqlPass
= readProcess
"gpg" ["-o", "-", sqlPassFile
] "" >>= return . BS
.pack
523 runRipple
:: WS
.ClientApp a
-> IO a
524 runRipple app
= WS
.runClient
"s1.ripple.com" 443 "/" app
526 runRippleWithSecret
:: RootstockIO a
-> IO a
527 runRippleWithSecret app
= do
529 sqlPass
<- readSqlPass
530 withPostgresqlConn
(BS
.concat [connString
, sqlPass
]) $ \sqlConn
-> do
531 runSqlPersistM
(runMigration migrateAll
) sqlConn
532 runRipple
$ \wsConn
->
533 runRootstock app
$ Rootstock
539 insertOldLinks
:: IO ()
540 insertOldLinks
= runRippleWithSecret
$ sequence_ $ map insertOldLink oldLinks
543 main
= runRippleWithSecret marketMaker