packages feed

namecoin-update-0.2.3.0: src/lib/Namecoin.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric  #-}

-- | Namecoin utility library
module Namecoin
-- * JSON-RPC client
( RPCRequest(..)
, RPCResponse(..)
, RPCError(..)
, rpcRequest

-- * Name operations
, Name(..)
, nameList
, nameUpdate

-- * Miscellanea
, uri
) where

import Control.Applicative  (many, (<|>))
import Control.Lens         (set, view)
import Data.Maybe           (fromJust)
import Data.Text            (Text, unpack)
import Data.Aeson           (ToJSON, FromJSON, Value)
import GHC.Generics         (Generic)

import Network.Wreq                   as W
import qualified Data.Aeson           as J
import qualified Data.Attoparsec.Text as P
import qualified Control.Exception    as E


-- | Alias for types with an error message
type Error = Either String


-- * Namecoin config parser

-- | Parse a comment (line beggining with a "#")
comment :: P.Parser ()
comment = do
  P.char '#' >> P.takeTill P.isEndOfLine
  P.endOfLine
  return ()


-- | Parse an option of form
--
-- > key=value
setting :: P.Parser (Text, Text)
setting = do
  name <- P.takeTill (== '=')
  P.char '='
  value <- P.takeTill P.isEndOfLine
  P.endOfLine
  return (name, value)


-- | Parse a line (either a comment or an option)
line :: P.Parser (Text, Text)
line = (comment >> line) <|> setting


-- | Parse the namecoin config format
config :: P.Parser [(Text, Text)]
config = many line


-- | Takes the content of a namecoin config file
-- and gives the URI to connect to the JSON-RPC server
uri :: Text -> Error String
uri content = do
  dict     <- P.parseOnly config content
  username <- get dict "rpcuser"
  password <- get dict "rpcpassword"
  address  <- get dict "rpcbind"
  port     <- get dict "rpcport"
  return ("http://"++username++":"++password++"@"++address++":"++port)
  where
    get dict key = maybe (missing key) (Right . unpack) (lookup key dict)
    missing key  = Left ("option '"++unpack key++"' is missing.")


-- * JSON-RPC client

-- | JSON-RPC 1.0 request record
data RPCRequest = RPCRequest
  { id      :: String    -- ^ a string identificating the client
  , method  :: String    -- ^ the name of the method
  , params  :: [String]  -- ^ a list of parameters for the method
  } deriving (Generic, FromJSON, ToJSON)


-- | JSON-RPC 1.0 response record
data RPCResponse = RPCResponse
  { id       :: String          -- ^ the same identificative string 
  , result   :: Value           -- ^ result if the method call succeded
  , rpcError :: Maybe RPCError  -- ^ error in case the method call failed
  } deriving (Generic, FromJSON, ToJSON)


-- | Namecoin API error record
data RPCError = RPCError
  { code    :: Int     -- ^ a number indicating the kind of error
  , message :: String  -- ^ a detailed explanation of the error
  } deriving (Generic, FromJSON, ToJSON)


-- | Namecoin API Value record
data Name = Name
  { name       :: String  -- ^ the namecoin name
  , value      :: String  -- ^ its value
  , expires_in :: Int     -- ^ number of blocks before the name expires
  } deriving (Show, Generic, FromJSON)


-- | Turn an Aeson AST object into a 'fromJSON' type
decodeValue :: FromJSON a => Value -> Error a
decodeValue = J.eitherDecode . J.encode


-- | Execute an RPC method
rpcRequest
  :: String            -- ^ the URI of the JSON-RPC endpoint
  -> String            -- ^ the method name
  -> [String]          -- ^ the method parameters
  -> IO (Error Value)  -- ^ and error or the wanted result
rpcRequest uri method params = do
  req <- E.try (view W.responseBody <$> (W.asJSON =<< W.postWith options uri req))
  return $ case req of
    Left  err -> Left ("RPC error: "++show (err :: E.SomeException))
    Right res -> case (rpcError res) of
      Nothing  -> Right (result res)
      Just err -> Left  ("API error "++show (code err)++": "++message err)
  where
    req = J.toJSON (RPCRequest "namecoin-update" method params)
    options = set checkResponse (Just $ \_ _ -> return ()) defaults


-- * Name operations

-- | Returns the list of currently registered names
nameList :: String -> IO (Error [Name])
nameList uri = fmap (decodeValue =<<) (rpcRequest uri "name_list" [])


-- | Issue an udpate for a name (confirming its current value)
nameUpdate :: String -> Name -> IO Int
nameUpdate uri (Name {..}) = do
  putStr ("Updating name "++name++"... ")
  req <- rpcRequest uri "name_update" [ name, value ]
  case req of
    Left  err -> putStrLn "failed" >> putStrLn err >> return 1
    Right _   -> putStrLn "ok" >> return 0