packages feed

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

-- | Namecoin utility library
module Namecoin where

import Control.Applicative         (many, (<|>))
import Control.Lens                (set, view)
import Control.Exception           (SomeException, try)
import Prelude              hiding (error)
import Data.Attoparsec.Text hiding (try)
import Data.Maybe                  (fromJust)
import Data.Text                   (Text, unpack)
import Data.Aeson
import Data.Aeson.TH
import Network.Wreq


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


-- * Namecoin config parser

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

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

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

-- | Parse the namecoin config format
config :: 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     <- 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
  }

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

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

-- | 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)

deriveJSON defaultOptions ''RPCRequest
deriveJSON defaultOptions ''RPCResponse
deriveJSON defaultOptions ''RPCError
deriveJSON defaultOptions ''Name

-- | Turn an Aeson AST object into a 'fromJSON' type
decodeValue :: FromJSON a => Value -> Error a
decodeValue = eitherDecode . 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 <- try (view responseBody <$> (asJSON =<< postWith options uri req))
  return $ case req of
    Left  err -> Left ("RPC error: "++show (err :: SomeException))
    Right res -> case (error res) of
      Nothing  -> Right (result res)
      Just err -> Left  ("API error "++show (code err)++": "++message err)
  where
    req = toJSON (RPCRequest "namecoin-update" method params)
    options = set checkResponse (Just $ \_ _ -> return ()) defaults


-- * Name operations

-- | Return 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 1