packages feed

ripple-0.1: Ripple/WebSockets.hs

module Ripple.WebSockets (
	-- * Base WebSocket helpers
	receiveJSON,
	sendJSON,
	-- * Ripple JSON result parsing and error handling
	RippleError(..),
	RippleResult(..),
	-- * ripple_path_find
	CommandRipplePathFind(..),
	ResultRipplePathFind(..),
	Alternative(..),
	-- * account_tx
	CommandAccountTX(..),
	ResultAccountTX(..)
) where

import Numeric (readHex)
import Data.Maybe (fromMaybe)
import Data.Word (Word8)
import Control.Applicative ((<$>), (<*>))
import Control.Monad (forM)
import Control.Error (note, fmapL, readZ)
import Data.Base58Address (RippleAddress)
import Data.Binary (decodeOrFail)

import Data.Aeson ((.=), (.:), (.:?))
import qualified Data.Aeson as Aeson
import qualified Network.WebSockets as WS
import qualified Data.Text as T
import qualified Data.ByteString.Lazy as LZ

import Ripple.Transaction
import Ripple.Amount

-- Base WebSocket helpers

receiveJSON :: (Aeson.FromJSON j) => WS.Connection -> IO (Either String j)
receiveJSON = fmap Aeson.eitherDecode . WS.receiveData

sendJSON :: (Aeson.ToJSON j) => WS.Connection -> j -> IO ()
sendJSON conn = WS.sendTextData conn . Aeson.encode

-- Ripple JSON result parsing and error handling

-- | Ripple server error codes
data RippleError =
	UnknownCommand |
	OtherRippleError Int String String
	deriving (Show, Eq)

-- | The result of a WebSocket command -- either error or a response
newtype RippleResult a = RippleResult (Either RippleError a)
	deriving (Show, Eq)

instance (Aeson.FromJSON a) => Aeson.FromJSON (RippleResult a) where
	parseJSON (Aeson.Object o) = RippleResult <$> do
		status <- o .: T.pack "status"
		typ <- o .: T.pack "type"
		case (status, typ) of
			("success", "response") ->
				Right <$> (Aeson.parseJSON =<< o.: T.pack "result")
			("error", "response") -> do
				err  <- o .: T.pack "error"
				code <- o .: T.pack "error_code"
				msg  <- o .: T.pack "error_message"
				case code of
					27 -> return $ Left UnknownCommand
					_ -> return $ Left $ OtherRippleError code err msg
			_ -> fail "Invalid Ripple Result"
	parseJSON _ = fail "Ripple Result is always a JSON object"

-- ripple_path_find

data CommandRipplePathFind = CommandRipplePathFind {
		source_account :: RippleAddress,
		destination_account :: RippleAddress,
		destination_amount :: Amount
	} deriving (Show, Eq)

instance Aeson.ToJSON CommandRipplePathFind where
	toJSON (CommandRipplePathFind source dest amount) = Aeson.object [
			T.pack "command" .= T.pack "ripple_path_find",
			T.pack "source_account" .= show source,
			T.pack "destination_account" .= show dest,
			T.pack "destination_amount" .= amount
		]

data ResultRipplePathFind = ResultRipplePathFind {
		alternatives :: [Alternative],
		response_destination_account :: RippleAddress
	} deriving (Show, Eq)

instance Aeson.FromJSON ResultRipplePathFind where
	parseJSON (Aeson.Object o) = ResultRipplePathFind <$>
		o .: T.pack "alternatives" <*>
		(readZ =<< o .: T.pack "destination_account")
	parseJSON _ = fail "PathFindResponse is always a JSON object"

data Alternative = Alternative {
		source_amount :: Amount
	} deriving (Show, Eq)

instance Aeson.FromJSON Alternative where
	parseJSON (Aeson.Object o) = Alternative <$> o .: T.pack "source_amount"
	parseJSON _ = fail "Alternative is always a JSON object"

-- account_tx

data CommandAccountTX = CommandAccountTX {
		account        :: RippleAddress,
		limit          :: Int,
		ledgerIndexMin :: Maybe Int,
		ledgerIndexMax :: Maybe Int,
		descending     :: Bool,
		binary         :: Bool
	}

instance Aeson.ToJSON CommandAccountTX where
	toJSON (CommandAccountTX account lim min max desc bin) = Aeson.object [
			T.pack "command"          .= "account_tx",
			T.pack "account"          .= show account,
			T.pack "ledger_index_min" .= fromMaybe (-1) min,
			T.pack "ledger_index_max" .= fromMaybe (-1) max,
			T.pack "binary"           .= bin,
			T.pack "limit"            .= lim,
			T.pack "descending"       .= desc
		]

-- | [(ledger_index, transaction)]
data ResultAccountTX = ResultAccountTX [(Integer,Transaction)]
	deriving (Show, Eq)

instance Aeson.FromJSON ResultAccountTX where
	parseJSON (Aeson.Object o) = ResultAccountTX <$> do
		transactions <- o .: T.pack "transactions"
		forM transactions $ \transaction -> do
			True <- transaction .: T.pack "validated"
			ledger <- transaction .: T.pack "ledger_index"
			blob <- transaction .:? T.pack "tx_blob" -- binary transaction

			tr <- either fail return $ do
				hex <- note "JSON transactions not implemented yet" blob
				bytes <- note "Invalid Hexidecimal encoding" $ hex2bytes hex
				fmapL (\(_,_,e) -> e) $ fmap (\(_,_,r) -> r) $
					decodeOrFail (LZ.pack bytes)

			return (ledger, tr)
	parseJSON _ = fail "account_tx result is always a JSON object"

-- Helpers

hex2bytes :: String -> Maybe [Word8]
hex2bytes [] = Just []
hex2bytes (x:y:rest) = case readHex [x,y] of
	[(n, "")] -> fmap (n:) (hex2bytes rest)
	_ -> Nothing
hex2bytes _ = Nothing