{-# LANGUAGE OverloadedStrings #-} module Zenith.Zcashd where import Control.Concurrent (threadDelay) import Control.Monad (when) import Data.Aeson import qualified Data.Array as A import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as C import qualified Data.ByteString.Lazy as LBS import Data.Maybe import qualified Data.Scientific as Scientific import qualified Data.Text as T import qualified Data.Text.Encoding as E import Data.Text.Encoding.Error (lenientDecode) import qualified Data.Text.IO as TIO import qualified Data.Vector as V import Network.HTTP.Simple import System.Clipboard import System.Exit import System.IO import Text.Read (readMaybe) import Text.Regex import Text.Regex.Base import ZcashHaskell.Types (RpcCall(..), RpcResponse(..)) import Zenith.Types ( AddressGroup , AddressSource(..) , NodeVersion(..) , OpResult(..) , UABalance(..) , ZcashAddress(..) , ZcashPool(..) , ZcashTx , encodeHexText' ) import Zenith.Utils (displayZec, getAddresses, validateAddress) -- * RPC methods -- | List addresses listAddresses :: BS.ByteString -> BS.ByteString -> IO [ZcashAddress] listAddresses user pwd = do response <- makeZcashCall user pwd "listaddresses" [] let rpcResp = decode response :: Maybe (RpcResponse [AddressGroup]) case rpcResp of Nothing -> fail "Couldn't parse node response" Just res -> do let addys = result res case addys of Nothing -> fail "Empty response" Just addys' -> do let addList = concatMap getAddresses addys' return addList -- | Get address balance getBalance :: BS.ByteString -> BS.ByteString -> ZcashAddress -> IO [Integer] getBalance user pwd zadd = do let a = account zadd case a of Nothing -> do response <- makeZcashCall user pwd "z_getbalance" [ String (addy zadd) , Number (Scientific.scientific 1 0) , Data.Aeson.Bool True ] let rpcResp = decode response :: Maybe (RpcResponse Integer) case rpcResp of Nothing -> fail "Couldn't parse node response" Just res -> do case result res of Nothing -> return [] Just r -> return [r] Just acct -> do response <- makeZcashCall user pwd "z_getbalanceforaccount" [Number (Scientific.scientific acct 0)] let rpcResp = decode response :: Maybe (RpcResponse UABalance) case rpcResp of Nothing -> fail "Couldn't parse node response" Just res -> do case result res of Nothing -> return [0, 0, 0] Just r -> return $ readUABalance r where readUABalance ua = [uatransparent ua, uasapling ua, uaorchard ua] -- | List transactions listTxs :: BS.ByteString -> BS.ByteString -> ZcashAddress -> IO [ZcashTx] listTxs user pwd zaddy = do response <- makeZcashCall user pwd "z_listreceivedbyaddress" [String $ addy zaddy] let rpcResp = decode response :: Maybe (RpcResponse [ZcashTx]) case rpcResp of Nothing -> fail "listTxs: Couldn't parse node response" Just res -> do case result res of Nothing -> fail "listTxs: Empty response" Just res' -> return res' -- | Send Tx sendTx :: BS.ByteString -> BS.ByteString -> ZcashAddress -> T.Text -> Double -> Maybe T.Text -> IO () sendTx user pwd fromAddy toAddy amount memo = do bal <- getBalance user pwd fromAddy let valAdd = validateAddress toAddy if sum bal - floor (amount * 100000000) >= 1000 then do if source fromAddy /= ImportedWatchOnly then do let privacyPolicy | valAdd == Just TransparentPool = "AllowRevealedRecipients" | isNothing (account fromAddy) && elem TransparentPool (pool fromAddy) = "AllowRevealedSenders" | otherwise = "AllowRevealedAmounts" let pd = case memo of Nothing -> [ Data.Aeson.String (addy fromAddy) , Data.Aeson.Array (V.fromList [object ["address" .= toAddy, "amount" .= amount]]) , Data.Aeson.Number $ Scientific.scientific 1 1 , Data.Aeson.Null , Data.Aeson.String privacyPolicy ] Just memo' -> [ Data.Aeson.String (addy fromAddy) , Data.Aeson.Array (V.fromList [ object [ "address" .= toAddy , "amount" .= amount , "memo" .= encodeHexText' memo' ] ]) , Data.Aeson.Number $ Scientific.scientific 1 1 , Data.Aeson.Null , Data.Aeson.String privacyPolicy ] response <- makeZcashCall user pwd "z_sendmany" pd let rpcResp = decode response :: Maybe (RpcResponse T.Text) case rpcResp of Nothing -> fail "Couldn't parse node response" Just res -> do putStr " Sending." checkOpResult user pwd (fromMaybe "" $ result res) else putStrLn "Error: Source address is view-only." else putStrLn "Error: Insufficient balance in source address." -- | Check Zcash full node server checkServer :: BS.ByteString -> BS.ByteString -> IO () checkServer user pwd = do resp <- makeZcashCall user pwd "getinfo" [] let rpcResp = decode resp :: Maybe (RpcResponse NodeVersion) case rpcResp of Nothing -> fail "Couldn't parse node response" Just myResp -> do let r = result myResp case r of Nothing -> fail "Empty node response" Just r' -> do if isNodeValid r' then putStrLn $ "Connected to Zcash Full Node (" <> show r <> ") :)" else do putStrLn "Deprecated Zcash Full Node version found. Exiting" exitFailure where isNodeValid (NodeVersion i) = i >= 5000000 -- | Check for accounts checkAccounts :: BS.ByteString -> BS.ByteString -> IO Bool checkAccounts user pwd = do response <- makeZcashCall user pwd "z_listaccounts" [] let rpcResp = decode response :: Maybe (RpcResponse [Object]) case rpcResp of Nothing -> fail "Couldn't parse node response" Just res -> do let r = result res return $ not (null r) -- | Add account to node createAccount :: BS.ByteString -> BS.ByteString -> IO () createAccount user pwd = do response <- makeZcashCall user pwd "z_getnewaccount" [] let rpcResp = decode response :: Maybe (RpcResponse Object) case rpcResp of Nothing -> fail "Couldn't parse node response" Just res -> do let r = result res putStrLn " Account created!" -- | Create new Unified Address createUnifiedAddress :: BS.ByteString -> BS.ByteString -> Bool -> Bool -> IO () createUnifiedAddress user pwd tRec sRec = do let recs = getReceivers tRec sRec let pd = [Data.Aeson.Number $ Scientific.scientific 0 1, recs] newResp <- makeZcashCall user pwd "z_getaddressforaccount" pd let rpcResp = decode newResp :: Maybe (RpcResponse Object) case rpcResp of Nothing -> fail "Couldn't parse node response" Just res -> do let r = result res putStrLn " New UA created!" where getReceivers t s | t && s = Data.Aeson.Array (V.fromList [ Data.Aeson.String "p2pkh" , Data.Aeson.String "sapling" , Data.Aeson.String "orchard" ]) | t = Data.Aeson.Array (V.fromList [Data.Aeson.String "p2pkh", Data.Aeson.String "orchard"]) | s = Data.Aeson.Array (V.fromList [Data.Aeson.String "sapling", Data.Aeson.String "orchard"]) | otherwise = Data.Aeson.Array (V.fromList [Data.Aeson.String "orchard"]) -- | Verify operation result checkOpResult :: BS.ByteString -> BS.ByteString -> T.Text -> IO () checkOpResult user pwd opid = do response <- makeZcashCall user pwd "z_getoperationstatus" [Data.Aeson.Array (V.fromList [Data.Aeson.String opid])] let rpcResp = decode response :: Maybe (RpcResponse [OpResult]) case rpcResp of Nothing -> fail "Couldn't parse node response" Just res -> do let r = result res case r of Nothing -> fail "Empty node response" Just r' -> mapM_ showResult r' where showResult t = case opsuccess t of "success" -> putStrLn $ " Success! Tx ID: " ++ maybe "" T.unpack (optxid t) "executing" -> do putStr "." hFlush stdout threadDelay 1000000 >> checkOpResult user pwd opid _ -> putStrLn $ " Failed :( " ++ maybe "" T.unpack (opmessage t) -- | Make a Zcash RPC call makeZcashCall :: BS.ByteString -> BS.ByteString -> T.Text -> [Data.Aeson.Value] -> IO LBS.ByteString makeZcashCall username password m p = do let payload = RpcCall "1.0" "test" m p let myRequest = setRequestBodyJSON payload $ setRequestPort 8232 $ setRequestBasicAuth username password $ setRequestMethod "POST" defaultRequest response <- httpLBS myRequest let respStatus = getResponseStatusCode response let body = getResponseBody response case respStatus of 500 -> do let rpcResp = decode body :: Maybe (RpcResponse String) case rpcResp of Nothing -> fail $ "Unknown server error " ++ show response Just x -> fail (fromMaybe "" $ result x) 401 -> fail "Incorrect full node credentials" 200 -> return body _ -> fail "Unknown error" -- | Read ZIP-321 URI sendWithUri :: BS.ByteString -> BS.ByteString -> ZcashAddress -> String -> Bool -> IO () sendWithUri user pwd fromAddy uri repTo = do let uriRegex = mkRegex "^zcash:(\\w+)\\?amount=(.*)\\&memo=(.*)$" if matchTest uriRegex uri then do let reg = matchAllText uriRegex uri let parsedAddress = fst $ head reg A.! 1 let parsedAmount = fst $ head reg A.! 2 let parsedEncodedMemo = fst $ head reg A.! 3 let addType = validateAddress $ T.pack parsedAddress case addType of Nothing -> putStrLn " Invalid address" Just TransparentPool -> do putStrLn $ " Address is valid: " ++ parsedAddress case (readMaybe parsedAmount :: Maybe Double) of Nothing -> putStrLn " Invalid amount." Just amt -> do putStrLn $ " Valid ZEC amount: " ++ show amt sendTx user pwd fromAddy (T.pack parsedAddress) amt Nothing Just _ -> do putStrLn $ " Address is valid: " ++ parsedAddress case (readMaybe parsedAmount :: Maybe Double) of Nothing -> putStrLn " Invalid amount." Just amt -> do putStrLn $ " Valid ZEC amount: " ++ show amt let decodedMemo = E.decodeUtf8With lenientDecode $ B64.decodeLenient $ C.pack parsedEncodedMemo TIO.putStrLn $ " Memo: " <> decodedMemo sendTx user pwd fromAddy (T.pack parsedAddress) amt (if repTo then Just $ T.concat [decodedMemo, "\nReply-To:\n", addy fromAddy] else Just decodedMemo) else putStrLn "URI is not compliant with ZIP-321" -- | Display an address displayZcashAddress :: BS.ByteString -> BS.ByteString -> (Int, ZcashAddress) -> IO () displayZcashAddress user pwd (idx, zaddy) = do zats <- getBalance user pwd zaddy putStr $ show idx ++ ": " putStr $ show zaddy when (source zaddy == ImportedWatchOnly) (putStr "[VK]") putStr " Balance: " mapM_ (putStr . displayZec) zats putStrLn ""