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 [] []" 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 "."