haskoin-wallet-0.2.0: Network/Haskoin/Wallet/Client.hs
module Network.Haskoin.Wallet.Client (clientMain) where
import System.FilePath ((</>))
import System.Directory (createDirectoryIfMissing)
import System.Posix.Directory (changeWorkingDirectory)
import System.Posix.Files
( setFileMode
, setFileCreationMask
, unionFileModes
, ownerModes
, groupModes
, otherModes
, fileExist
)
import System.Environment (getArgs, lookupEnv)
import System.Info (os)
import System.Console.GetOpt
( getOpt
, usageInfo
, OptDescr (Option)
, ArgDescr (NoArg, ReqArg)
, ArgOrder (Permute)
)
import Control.Monad (when, forM_)
import Control.Monad.Trans (liftIO)
import qualified Control.Monad.Reader as R (runReaderT)
import Data.Default (def)
import Data.FileEmbed (embedFile)
import qualified Data.Text as T (pack, unpack)
import Data.Yaml (decodeFileEither)
import Data.String.Conversions (cs)
import Network.Haskoin.Constants
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Client.Commands
import Network.Haskoin.Wallet.Types
import System.FilePath.Posix (isAbsolute)
usageHeader :: String
usageHeader = "Usage: hw [<options>] <command> [<args>]"
cmdHelp :: [String]
cmdHelp = lines $ cs $ $(embedFile "config/help")
warningMsg :: String
warningMsg = unwords
[ "!!!", "This software is experimental."
, "Use only small amounts of Bitcoins.", "!!!"
]
usage :: [String]
usage = warningMsg : usageInfo usageHeader options : cmdHelp
options :: [OptDescr (Config -> Config)]
options =
[ Option "k" ["keyring"]
(ReqArg (\s cfg -> cfg { configKeyRing = T.pack s }) "NAME") $
"Default: " ++ T.unpack (configKeyRing def)
, Option "c" ["count"]
(ReqArg (\s cfg -> cfg { configCount = read s }) "INT") $
"Items per page. Default: " ++ show (configCount def)
, Option "m" ["minconf"]
(ReqArg (\s cfg -> cfg { configMinConf = read s }) "INT") $
"Minimum confirmations. Default: "
++ show (configMinConf def)
, Option "f" ["fee"]
(ReqArg (\s cfg -> cfg { configFee = read s }) "INT") $
"Fee per kilobyte. Default: " ++ show (configFee def)
, Option "R" ["rcptfee"]
(NoArg $ \cfg -> cfg { configRcptFee = True }) $
"Recipient pays fee. Default: " ++ show (configRcptFee def)
, Option "S" ["nosig"]
(NoArg $ \cfg -> cfg { configSignTx = False }) $
"Do not sign. Default: " ++ show (not $ configSignTx def)
, Option "i" ["internal"]
(NoArg $ \cfg -> cfg { configAddrType = AddressInternal }) $
"Internal addresses. Default: "
++ show (configAddrType def == AddressInternal)
, Option "o" ["offline"]
(NoArg $ \cfg -> cfg { configOffline = True }) $
"Offline balance. Default: " ++ show (configOffline def)
, Option "r" ["revpage"]
(NoArg $ \cfg -> cfg { configReversePaging = True }) $
"Reverse paging. Default: "
++ show (configReversePaging def)
, Option "p" ["pass"]
(ReqArg (\s cfg -> cfg { configPass = Just $ T.pack s }) "PASS")
"Mnemonic passphrase"
, Option "j" ["json"]
(NoArg $ \cfg -> cfg { configFormat = OutputJSON })
"Output JSON"
, Option "y" ["yaml"]
(NoArg $ \cfg -> cfg { configFormat = OutputYAML })
"Output YAML"
, Option "s" ["socket"]
(ReqArg (\s cfg -> cfg { configConnect = s }) "URI") $
"Server socket. Default: " ++ configConnect def
, Option "d" ["detach"]
(NoArg $ \cfg -> cfg { configDetach = True }) $
"Detach server. Default: " ++ show (configDetach def)
, Option "t" ["testnet"]
(NoArg $ \cfg -> cfg { configTestnet = True }) "Testnet3 network"
, Option "g" ["config"]
(ReqArg (\s cfg -> cfg { configFile = s }) "FILE") $
"Config file. Default: " ++ configFile def
, Option "w" ["workdir"]
(ReqArg (\s cfg -> cfg { configDir = s }) "DIR")
"Working directory. OS-dependent default"
, Option "v" ["verbose"]
(NoArg $ \cfg -> cfg { configVerbose = True }) "Verbose output"
]
-- Create and change current working directory
setWorkDir :: Config -> IO ()
setWorkDir cfg = do
let workDir = configDir cfg </> networkName
_ <- setFileCreationMask $ otherModes `unionFileModes` groupModes
createDirectoryIfMissing True workDir
setFileMode workDir ownerModes
changeWorkingDirectory workDir
-- Build application configuration
getConfig :: [Config -> Config] -> IO Config
getConfig fs = do
-- Create initial configuration from defaults and command-line arguments
let initCfg = foldr ($) def fs
-- If working directory set in initial configuration, use it
dir <- case configDir initCfg of "" -> appDir
d -> return d
-- Make configuration file relative to working directory
let cfgFile = if isAbsolute (configFile initCfg)
then configFile initCfg
else dir </> configFile initCfg
-- Get configuration from file, if it exists
e <- fileExist cfgFile
if e then do
cfgE <- decodeFileEither cfgFile
case cfgE of
Left x -> error $ show x
-- Override settings from file using command-line
Right cfg -> return $ fixConfigDir (foldr ($) cfg fs) dir
else return $ fixConfigDir initCfg dir
where
-- If working directory not set, use default
fixConfigDir cfg dir = case configDir cfg of "" -> cfg{ configDir = dir }
_ -> cfg
clientMain :: IO ()
clientMain = getArgs >>= \args -> case getOpt Permute options args of
(fs, commands, []) -> do
cfg <- getConfig fs
when (configTestnet cfg) switchToTestnet3
setWorkDir cfg
dispatchCommand cfg commands
(_, _, msgs) -> forM_ (msgs ++ usage) putStrLn
dispatchCommand :: Config -> [String] -> IO ()
dispatchCommand cfg args = flip R.runReaderT cfg $ case args of
["start"] -> cmdStart
["stop"] -> cmdStop
"newkeyring" : mnemonic -> cmdNewKeyRing mnemonic
["keyring"] -> cmdKeyRing
["keyrings"] -> cmdKeyRings
"newacc" : [name] -> cmdNewAcc name
"newms" : name : m : n : ks -> cmdNewMS False name m n ks
"newread" : [name, key] -> cmdNewRead name key
"newreadms" : name : m : n : ks -> cmdNewMS True name m n ks
"addkeys" : name : ks -> cmdAddKeys name ks
"setgap" : [name, gap] -> cmdSetGap name gap
"account" : [name] -> cmdAccount name
["accounts"] -> cmdAccounts
"list" : name : page -> cmdList name page
"unused" : [name] -> cmdUnused name
"label" : [name, index, label] -> cmdLabel name index label
"txs" : name : page -> cmdTxs name page
"addrtxs" : name : index : page -> cmdAddrTxs name index page
"genaddrs" : [name, i] -> cmdGenAddrs name i
"send" : [name, add, amnt] -> cmdSend name add amnt
"sendmany" : name : xs -> cmdSendMany name xs
"import" : [name, tx] -> cmdImport name tx
"sign" : [name, txid] -> cmdSign name txid
"gettx" : [name, txid] -> cmdGetTx name txid
"balance" : [name] -> cmdBalance name
"getoffline" : [name, txid] -> cmdGetOffline name txid
"signoffline" : [name, tx, dat] -> cmdSignOffline name tx dat
"rescan" : rescantime -> cmdRescan rescantime
"decodetx" : [tx] -> cmdDecodeTx tx
["status"] -> cmdStatus
["version"] -> cmdVersion
["help"] -> liftIO $ forM_ usage putStrLn
[] -> liftIO $ forM_ usage putStrLn
_ -> liftIO $ forM_ ("Invalid command" : usage) putStrLn
appDir :: IO FilePath
appDir = case os of "mingw" -> windows
"mingw32" -> windows
"mingw64" -> windows
"darwin" -> osx
"linux" -> unix
_ -> unix
where
windows = do
localAppData <- lookupEnv "LOCALAPPDATA"
dirM <- case localAppData of
Nothing -> lookupEnv "APPDATA"
Just l -> return $ Just l
case dirM of
Just d -> return $ d </> "Haskoin Wallet"
Nothing -> return "."
osx = do
homeM <- lookupEnv "HOME"
case homeM of
Just home -> return $ home </> "Library"
</> "Application Support"
</> "Haskoin Wallet"
Nothing -> return "."
unix = do
homeM <- lookupEnv "HOME"
case homeM of
Just home -> return $ home </> ".hw"
Nothing -> return "."