diff --git a/.gitignore b/.gitignore index 1c231fa..00967d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .stack-work/ *~ dist-newstyle/ +zenith.db +zenith.log +zenith.db-shm +zenith.db-wal diff --git a/CHANGELOG.md b/CHANGELOG.md index ca58dc4..ed06a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,8 +29,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Validation of input of amount for sending in TUI +## [0.5.3.1-beta] + +### Added + +- Docker image + ## [0.5.3.0-beta] +### Added + +- Address Book functionality. Allows users to store frequently used zcash addresses and + generate transactions using them. + ### Changed - Improved formatting of sync progress diff --git a/README.md b/README.md index efabca0..bce5523 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Zenith is a wallet for the [Zebra](https://zfnd.org/zebra/) Zcash node . It has - Listing transactions for specific addresses, decoding memos for easy reading. - Copying addresses to the clipboard. - Sending transactions with shielded memo support. +- Address Book for storing frequently used zcash addresses ## Installation diff --git a/app/Main.hs b/app/Main.hs index daade67..2a2dcd6 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -221,8 +221,8 @@ main = do " ______ _ _ _ \n |___ / (_) | | | \n / / ___ _ __ _| |_| |__ \n / / / _ \\ '_ \\| | __| '_ \\ \n / /_| __/ | | | | |_| | | |\n /_____\\___|_| |_|_|\\__|_| |_|\n Zcash Full Node CLI v0.4.0" } (root nodeUser nodePwd) - "cli" -> runZenithCLI myConfig "gui" -> runZenithGUI myConfig + "tui" -> runZenithTUI myConfig "rescan" -> clearSync myConfig _ -> printUsage else printUsage @@ -232,5 +232,5 @@ printUsage = do putStrLn "zenith [command] [parameters]\n" putStrLn "Available commands:" putStrLn "legacy\tLegacy CLI for zcashd" - putStrLn "cli\tCLI for zebrad" + putStrLn "tui\tTUI for zebrad" putStrLn "rescan\tRescan the existing wallet(s)" diff --git a/src/Zenith/CLI.hs b/src/Zenith/CLI.hs index a1ef217..e7498fe 100644 --- a/src/Zenith/CLI.hs +++ b/src/Zenith/CLI.hs @@ -10,8 +10,10 @@ import qualified Brick.BChan as BC import qualified Brick.Focus as F import Brick.Forms ( Form(..) + , FormFieldState , (@@=) , allFieldsValid + , editShowableField , editShowableFieldWithValidate , editTextField , focusedFormInputAttr @@ -40,6 +42,9 @@ import Brick.Widgets.Core , joinBorders , padAll , padBottom + , padLeft + , padTop + , setAvailableSize , str , strWrap , strWrapWith @@ -49,6 +54,7 @@ import Brick.Widgets.Core , updateAttrMap , vBox , vLimit + , viewport , withAttr , withBorderStyle ) @@ -116,6 +122,10 @@ data Name | RecField | AmtField | MemoField + | ABViewport + | ABList + | DescripField + | AddressField deriving (Eq, Show, Ord) data DialogInput = DialogInput @@ -132,6 +142,13 @@ data SendInput = SendInput makeLenses ''SendInput +data AdrBookEntry = AdrBookEntry + { _descrip :: !T.Text + , _address :: !T.Text + } deriving (Show) + +makeLenses ''AdrBookEntry + data DialogType = WName | AName @@ -140,6 +157,10 @@ data DialogType | ASelect | SendTx | Blank + | AdrBook + | AdrBookForm + | AdrBookUpdForm + | AdrBookDelForm data DisplayType = AddrDisplay @@ -149,6 +170,7 @@ data DisplayType | TxIdDisplay | SyncDisplay | SendDisplay + | AdrBookEntryDisplay | BlankDisplay data Tick @@ -179,6 +201,9 @@ data State = State , _eventDispatch :: !(BC.BChan Tick) , _timer :: !Int , _txForm :: !(Form SendInput () Name) + , _abAddresses :: !(L.List Name (Entity AddressBook)) + , _abForm :: !(Form AdrBookEntry () Name) + , _abCurAdrs :: !T.Text -- used for address book CRUD operations , _sentTx :: !(Maybe HexString) , _unconfBalance :: !Integer } @@ -194,14 +219,15 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] withBorderStyle unicode $ B.borderWithLabel (str - ("Zenith - " <> + (" Zenith - " <> show (st ^. network) <> " - " <> - T.unpack - (maybe - "(None)" - (\(_, w) -> zcashWalletName $ entityVal w) - (L.listSelectedElement (st ^. wallets))))) + (T.unpack + (maybe + "(None)" + (\(_, w) -> zcashWalletName $ entityVal w) + (L.listSelectedElement (st ^. wallets)))) ++ + " ")) (C.hCenter (str ("Account: " ++ @@ -224,15 +250,18 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] else displayTaz (st ^. unconfBalance))) <=> listAddressBox "Addresses" (st ^. addresses) <+> B.vBorder <+> - (C.hCenter (str ("Last block seen: " ++ show (st ^. syncBlock))) <=> - listTxBox "Transactions" (st ^. network) (st ^. transactions))) <=> + (C.hCenter + (str ("Last block seen: " ++ show (st ^. syncBlock) ++ "\n")) <=> + listTxBox " Transactions " (st ^. network) (st ^. transactions))) <=> C.hCenter (hBox [ capCommand "W" "allets" , capCommand "A" "ccounts" , capCommand "V" "iew address" , capCommand "S" "end Tx" + , capCommand2 "Address " "B" "ook" , capCommand "Q" "uit" + , capCommand "?" " Help" , str $ show (st ^. timer) ]) listBox :: Show e => String -> L.List Name e -> Widget Name @@ -271,7 +300,7 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] (hBox [ capCommand "↑↓ " "move" , capCommand "↲ " "select" - , capCommand "Tab " "->" + , capCommand3 "" "Tab" " ->" ]) ] listTxBox :: @@ -287,19 +316,20 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] (hBox [ capCommand "↑↓ " "move" , capCommand "T" "x Display" - , capCommand "Tab " "<-" + , capCommand3 "" "Tab" " <-" ]) ] helpDialog :: State -> Widget Name helpDialog st = if st ^. helpBox then D.renderDialog - (D.dialog (Just (str "Commands")) Nothing 55) + (D.dialog (Just (str " Commands ")) Nothing 55) (vBox ([C.hCenter $ str "Key", B.hBorder] <> keyList) <+> vBox ([str "Actions", B.hBorder] <> actionList)) else emptyWidget where - keyList = map (C.hCenter . str) ["?", "Esc", "w", "a", "v", "q"] + keyList = + map (C.hCenter . str) ["?", "Esc", "w", "a", "v", "s", "b", "q"] actionList = map (hLimit 40 . str) @@ -308,6 +338,8 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] , "Switch wallets" , "Switch accounts" , "View address" + , "Send Tx" + , "Address Book" , "Quit" ] inputDialog :: State -> Widget Name @@ -315,20 +347,20 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] case st ^. dialogBox of WName -> D.renderDialog - (D.dialog (Just (str "Create Wallet")) Nothing 50) + (D.dialog (Just (str " Create Wallet ")) Nothing 50) (renderForm $ st ^. inputForm) AName -> D.renderDialog - (D.dialog (Just (str "Create Account")) Nothing 50) + (D.dialog (Just (str " Create Account ")) Nothing 50) (renderForm $ st ^. inputForm) AdName -> D.renderDialog - (D.dialog (Just (str "Create Address")) Nothing 50) + (D.dialog (Just (str " Create Address ")) Nothing 50) (renderForm $ st ^. inputForm) WSelect -> D.renderDialog - (D.dialog (Just (str "Select Wallet")) Nothing 50) - (selectListBox "Wallets" (st ^. wallets) listDrawWallet <=> + (D.dialog (Just (str " Select Wallet ")) Nothing 50) + (selectListBox " Wallets " (st ^. wallets) listDrawWallet <=> C.hCenter (hBox [ capCommand "↑↓ " "move" @@ -339,8 +371,8 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] ])) ASelect -> D.renderDialog - (D.dialog (Just (str "Select Account")) Nothing 50) - (selectListBox "Accounts" (st ^. accounts) listDrawAccount <=> + (D.dialog (Just (str " Select Account ")) Nothing 50) + (selectListBox " Accounts " (st ^. accounts) listDrawAccount <=> C.hCenter (hBox [ capCommand "↑↓ " "move" @@ -350,11 +382,63 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] ])) SendTx -> D.renderDialog - (D.dialog (Just (str "Send Transaction")) Nothing 50) + (D.dialog (Just (str " Send Transaction ")) Nothing 50) (renderForm (st ^. txForm) <=> C.hCenter (hBox [capCommand "↲ " "Send", capCommand " " "Cancel"])) Blank -> emptyWidget + -- Address Book List + AdrBook -> + D.renderDialog + (D.dialog (Just $ str " Address Book ") Nothing 60) + (withAttr abDefAttr $ + setAvailableSize (50, 20) $ + viewport ABViewport BT.Vertical $ + vLimit 20 $ + hLimit 50 $ + vBox + [ vLimit 16 $ + hLimit 50 $ + vBox $ [L.renderList listDrawAB True (s ^. abAddresses)] + , padTop Max $ + vLimit 4 $ + hLimit 50 $ + withAttr abMBarAttr $ + vBox $ + [ C.hCenter $ + (capCommand "N" "ew Address" <+> + capCommand "E" "dit Address" <+> + capCommand3 "" "C" "opy Address") + , C.hCenter $ + (capCommand "D" "elete Address" <+> + capCommand "S" "end Zcash" <+> capCommand3 "E" "x" "it") + ] + ]) + -- Address Book new entry form + AdrBookForm -> + D.renderDialog + (D.dialog (Just $ str " New Address Book Entry ") Nothing 50) + (renderForm (st ^. abForm) <=> + C.hCenter + (hBox [capCommand "↲" " Save", capCommand3 "" "" " Cancel"])) + -- Address Book edit/update entry form + AdrBookUpdForm -> + D.renderDialog + (D.dialog (Just $ str " Edit Address Book Entry ") Nothing 50) + (renderForm (st ^. abForm) <=> + C.hCenter + (hBox [capCommand "↲" " Save", capCommand3 "" "" " Cancel"])) + -- Address Book edit/update entry form + AdrBookDelForm -> + D.renderDialog + (D.dialog (Just $ str " Delete Address Book Entry ") Nothing 50) + (renderForm (st ^. abForm) <=> + C.hCenter + (hBox + [ capCommand "C" "onfirm delete" + , capCommand3 "" "" " Cancel" + ])) + -- splashDialog :: State -> Widget Name splashDialog st = if st ^. splashBox @@ -366,9 +450,14 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] (str " _____ _ _ _ \n|__ /___ _ __ (_) |_| |__\n / // _ \\ '_ \\| | __| '_ \\\n / /| __/ | | | | |_| | | |\n/____\\___|_| |_|_|\\__|_| |_|") <=> C.hCenter - (withAttr titleAttr (str "Zcash Wallet v0.5.3.0-beta")) <=> + (withAttr titleAttr (str "Zcash Wallet v0.5.3.1-beta")) <=> C.hCenter (withAttr blinkAttr $ str "Press any key...")) else emptyWidget + capCommand3 :: String -> String -> String -> Widget Name + capCommand3 l h e = hBox [str l, withAttr titleAttr (str h), str e] + capCommand2 :: String -> String -> String -> Widget Name + capCommand2 l h e = + hBox [str l, withAttr titleAttr (str h), str e, str " | "] capCommand :: String -> String -> Widget Name capCommand k comm = hBox [withAttr titleAttr (str k), str comm, str " | "] xCommand :: Widget Name @@ -419,7 +508,7 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] Just (_, w) -> withBorderStyle unicodeBold $ D.renderDialog - (D.dialog (Just $ txt "Seed Phrase") Nothing 50) + (D.dialog (Just $ txt " Seed Phrase ") Nothing 50) (padAll 1 $ txtWrap $ E.decodeUtf8Lenient $ @@ -428,12 +517,12 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] MsgDisplay -> withBorderStyle unicodeBold $ D.renderDialog - (D.dialog (Just $ txt "Message") Nothing 50) + (D.dialog (Just $ txt " Message ") Nothing 50) (padAll 1 $ strWrap $ st ^. msg) TxIdDisplay -> withBorderStyle unicodeBold $ D.renderDialog - (D.dialog (Just $ txt "Success") Nothing 50) + (D.dialog (Just $ txt " Success ") Nothing 50) (padAll 1 $ (txt "Tx ID: " <+> txtWrapWith @@ -446,7 +535,7 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] Just (_, tx) -> withBorderStyle unicodeBold $ D.renderDialog - (D.dialog (Just $ txt "Transaction") Nothing 50) + (D.dialog (Just $ txt " Transaction ") Nothing 50) (padAll 1 (str @@ -472,7 +561,7 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] SyncDisplay -> withBorderStyle unicodeBold $ D.renderDialog - (D.dialog (Just $ txt "Sync") Nothing 50) + (D.dialog (Just $ txt " Sync ") Nothing 50) (padAll 1 (updateAttrMap @@ -486,12 +575,28 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] SendDisplay -> withBorderStyle unicodeBold $ D.renderDialog - (D.dialog (Just $ txt "Sending Transaction") Nothing 50) + (D.dialog (Just $ txt " Sending Transaction ") Nothing 50) (padAll 1 (strWrapWith (WrapSettings False True NoFill FillAfterFirst) (st ^. msg))) + AdrBookEntryDisplay -> do + case L.listSelectedElement $ st ^. abAddresses of + Just (_, a) -> do + let abentry = + T.pack $ + " Descr: " ++ + T.unpack (addressBookAbdescrip (entityVal a)) ++ + "\n Address: " ++ + T.unpack (addressBookAbaddress (entityVal a)) + withBorderStyle unicodeBold $ + D.renderDialog + (D.dialog (Just $ txt " Address Book Entry ") Nothing 60) + (padAll 1 $ + txtWrapWith (WrapSettings False True NoFill FillAfterFirst) $ + abentry) + _ -> emptyWidget BlankDisplay -> emptyWidget mkInputForm :: DialogInput -> Form DialogInput e Name @@ -516,6 +621,29 @@ mkSendForm bal = label s w = padBottom (Pad 1) $ vLimit 1 (hLimit 15 $ str s <+> fill ' ') <+> w +mkNewABForm :: AdrBookEntry -> Form AdrBookEntry e Name +mkNewABForm = + newForm + [ label "Descrip: " @@= editTextField descrip DescripField (Just 1) + , label "Address: " @@= editTextField address AddressField (Just 1) + ] + where + label s w = + padBottom (Pad 1) $ vLimit 1 (hLimit 10 $ str s <+> fill ' ') <+> w + +isRecipientValid :: T.Text -> Bool +isRecipientValid a = + case isValidUnifiedAddress (E.encodeUtf8 a) of + Just _a1 -> True + Nothing -> + isValidShieldedAddress (E.encodeUtf8 a) || + (case decodeTransparentAddress (E.encodeUtf8 a) of + Just _a3 -> True + Nothing -> + case decodeExchangeAddress a of + Just _a4 -> True + Nothing -> False) + listDrawElement :: (Show a) => Bool -> a -> Widget Name listDrawElement sel a = let selStr s = @@ -572,6 +700,14 @@ listDrawTx znet sel tx = then withAttr customAttr (txt $ "> " <> s) else txt $ " " <> s +listDrawAB :: Bool -> Entity AddressBook -> Widget Name +listDrawAB sel ab = + let selStr s = + if sel + then withAttr abSelAttr (txt $ " " <> s) + else txt $ " " <> s + in selStr $ addressBookAbdescrip (entityVal ab) + customAttr :: A.AttrName customAttr = L.listSelectedAttr <> A.attrName "custom" @@ -590,6 +726,18 @@ barDoneAttr = A.attrName "done" barToDoAttr :: A.AttrName barToDoAttr = A.attrName "remaining" +abDefAttr :: A.AttrName +abDefAttr = A.attrName "abdefault" + +abSelAttr :: A.AttrName +abSelAttr = A.attrName "abselected" + +abMBarAttr :: A.AttrName +abMBarAttr = A.attrName "menubar" + +validBarValue :: Float -> Float +validBarValue = clamp 0 1 + scanZebra :: T.Text -> T.Text -> Int -> Int -> BC.BChan Tick -> IO () scanZebra dbP zHost zPort b eChan = do _ <- liftIO $ initDb dbP @@ -627,8 +775,7 @@ scanZebra dbP zHost zPort b eChan = do "getblock" [Data.Aeson.String $ T.pack $ show bl, jsonNumber 1] case r of - Left e1 -> do - liftIO $ BC.writeBChan eChan $ TickMsg e1 + Left e1 -> liftIO $ BC.writeBChan eChan $ TickMsg e1 Right blk -> do r2 <- liftIO $ @@ -638,8 +785,7 @@ scanZebra dbP zHost zPort b eChan = do "getblock" [Data.Aeson.String $ T.pack $ show bl, jsonNumber 0] case r2 of - Left e2 -> do - liftIO $ BC.writeBChan eChan $ TickMsg e2 + Left e2 -> liftIO $ BC.writeBChan eChan $ TickMsg e2 Right hb -> do let blockTime = getBlockTime hb mapM_ (runNoLoggingT . processTx zHost zPort blockTime pool) $ @@ -666,8 +812,8 @@ appEvent (BT.AppEvent t) = do TxDisplay -> return () TxIdDisplay -> return () SyncDisplay -> return () - SendDisplay -> do - BT.modify $ set msg m + SendDisplay -> BT.modify $ set msg m + AdrBookEntryDisplay -> return () BlankDisplay -> return () TickTx txid -> do BT.modify $ set sentTx (Just txid) @@ -680,6 +826,7 @@ appEvent (BT.AppEvent t) = do TxDisplay -> return () TxIdDisplay -> return () SendDisplay -> return () + AdrBookEntryDisplay -> return () SyncDisplay -> do if s ^. barValue == 1.0 then do @@ -712,6 +859,10 @@ appEvent (BT.AppEvent t) = do WSelect -> return () ASelect -> return () SendTx -> return () + AdrBook -> return () + AdrBookForm -> return () + AdrBookUpdForm -> return () + AdrBookDelForm -> return () Blank -> do if s ^. timer == 90 then do @@ -729,8 +880,7 @@ appEvent (BT.AppEvent t) = do (s ^. eventDispatch) BT.modify $ set timer 0 return () - else do - BT.modify $ set timer $ 1 + s ^. timer + else BT.modify $ set timer $ 1 + s ^. timer appEvent (BT.VtyEvent e) = do r <- F.focusGetCurrent <$> use focusRing s <- BT.get @@ -739,8 +889,7 @@ appEvent (BT.VtyEvent e) = do else if s ^. helpBox then do case e of - V.EvKey V.KEsc [] -> do - BT.modify $ set helpBox False + V.EvKey V.KEsc [] -> BT.modify $ set helpBox False _ev -> return () else do case s ^. displayBox of @@ -811,6 +960,7 @@ appEvent (BT.VtyEvent e) = do _ev -> return () SendDisplay -> BT.modify $ set displayBox BlankDisplay SyncDisplay -> BT.modify $ set displayBox BlankDisplay + AdrBookEntryDisplay -> BT.modify $ set displayBox BlankDisplay BlankDisplay -> do case s ^. dialogBox of WName -> do @@ -873,7 +1023,7 @@ appEvent (BT.VtyEvent e) = do V.EvKey (V.KChar 'n') [] -> do BT.modify $ set inputForm $ - updateFormState (DialogInput "New Wallet") $ + updateFormState (DialogInput " New Wallet ") $ s ^. inputForm BT.modify $ set dialogBox WName V.EvKey (V.KChar 's') [] -> @@ -890,7 +1040,7 @@ appEvent (BT.VtyEvent e) = do V.EvKey (V.KChar 'n') [] -> do BT.modify $ set inputForm $ - updateFormState (DialogInput "New Account") $ + updateFormState (DialogInput " New Account ") $ s ^. inputForm BT.modify $ set dialogBox AName ev -> BT.zoom accounts $ L.handleListEvent ev @@ -951,7 +1101,7 @@ appEvent (BT.VtyEvent e) = do BT.modify $ set msg "Invalid inputs" BT.modify $ set displayBox MsgDisplay BT.modify $ set dialogBox Blank - ev -> do + ev -> BT.zoom txForm $ do handleFormEvent (BT.VtyEvent ev) fs <- BT.gets formState @@ -959,6 +1109,189 @@ appEvent (BT.VtyEvent e) = do setFieldValid (isRecipientValid (fs ^. sendTo)) RecField + AdrBook -> do + case e of + V.EvKey (V.KChar 'x') [] -> + BT.modify $ set dialogBox Blank + V.EvKey (V.KChar 'c') [] + -- Copy Address to Clipboard + -> do + case L.listSelectedElement $ s ^. abAddresses of + Just (_, a) -> do + liftIO $ + setClipboard $ + T.unpack $ addressBookAbaddress (entityVal a) + BT.modify $ + set msg $ + "Address copied to Clipboard from >>\n" ++ + T.unpack (addressBookAbdescrip (entityVal a)) + BT.modify $ set displayBox MsgDisplay + _ -> do + BT.modify $ + set msg "Error while copying the address!!" + BT.modify $ set displayBox MsgDisplay + -- Send Zcash transaction + V.EvKey (V.KChar 's') [] -> do + case L.listSelectedElement $ s ^. abAddresses of + Just (_, a) -> do + BT.modify $ + set txForm $ + mkSendForm + (s ^. balance) + (SendInput + (addressBookAbaddress (entityVal a)) + 0.0 + "") + BT.modify $ set dialogBox SendTx + _ -> do + BT.modify $ + set msg "No receiver address available!!" + BT.modify $ set displayBox MsgDisplay + -- Edit an entry in Address Book + V.EvKey (V.KChar 'e') [] -> do + case L.listSelectedElement $ s ^. abAddresses of + Just (_, a) -> do + BT.modify $ + set + abCurAdrs + (addressBookAbaddress (entityVal a)) + BT.modify $ + set abForm $ + mkNewABForm + (AdrBookEntry + (addressBookAbdescrip (entityVal a)) + (addressBookAbaddress (entityVal a))) + BT.modify $ set dialogBox AdrBookUpdForm + _ -> do + BT.modify $ set dialogBox Blank + -- Delete an entry from Address Book + V.EvKey (V.KChar 'd') [] -> do + case L.listSelectedElement $ s ^. abAddresses of + Just (_, a) -> do + BT.modify $ + set + abCurAdrs + (addressBookAbaddress (entityVal a)) + BT.modify $ + set abForm $ + mkNewABForm + (AdrBookEntry + (addressBookAbdescrip (entityVal a)) + (addressBookAbaddress (entityVal a))) + BT.modify $ set dialogBox AdrBookDelForm + _ -> do + BT.modify $ set dialogBox Blank + -- Create a new entry in Address Book + V.EvKey (V.KChar 'n') [] -> do + BT.modify $ + set abForm $ mkNewABForm (AdrBookEntry "" "") + BT.modify $ set dialogBox AdrBookForm + -- Show AddressBook entry data + V.EvKey V.KEnter [] -> do + BT.modify $ set displayBox AdrBookEntryDisplay + -- Process any other event + ev -> BT.zoom abAddresses $ L.handleListEvent ev + -- Process new address book entry + AdrBookForm -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox AdrBook + V.EvKey V.KEnter [] -> do + pool <- liftIO $ runNoLoggingT $ initPool $ s ^. dbPath + fs <- BT.zoom abForm $ BT.gets formState + let idescr = T.unpack $ T.strip (fs ^. descrip) + let iabadr = fs ^. address + if not (null idescr) && isRecipientValid iabadr + then do + res <- + liftIO $ + saveAdrsInAdrBook pool $ + AddressBook + (ZcashNetDB (s ^. network)) + (fs ^. descrip) + (fs ^. address) + case res of + Nothing -> do + BT.modify $ + set + msg + ("AddressBook Entry already exists: " ++ + T.unpack (fs ^. address)) + BT.modify $ set displayBox MsgDisplay + Just _ -> do + BT.modify $ + set + msg + ("New AddressBook entry created!!\n" ++ + T.unpack (fs ^. address)) + BT.modify $ set displayBox MsgDisplay + -- case end + s' <- liftIO $ refreshAddressBook s + BT.put s' + BT.modify $ set dialogBox AdrBook + else do + BT.modify $ set msg "Invalid or missing data!!: " + BT.modify $ set displayBox MsgDisplay + BT.modify $ set dialogBox AdrBookForm + ev -> + BT.zoom abForm $ do + handleFormEvent (BT.VtyEvent ev) + fs <- BT.gets formState + BT.modify $ + setFieldValid + (isRecipientValid (fs ^. address)) + AddressField + AdrBookUpdForm -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox AdrBook + V.EvKey V.KEnter [] -> do + pool <- liftIO $ runNoLoggingT $ initPool $ s ^. dbPath + fs <- BT.zoom abForm $ BT.gets formState + let idescr = T.unpack $ T.strip (fs ^. descrip) + let iabadr = fs ^. address + if not (null idescr) && isRecipientValid iabadr + then do + res <- + liftIO $ + updateAdrsInAdrBook + pool + (fs ^. descrip) + (fs ^. address) + (s ^. abCurAdrs) + BT.modify $ + set + msg + ("AddressBook entry modified!!\n" ++ + T.unpack (fs ^. address)) + BT.modify $ set displayBox MsgDisplay + -- case end + s' <- liftIO $ refreshAddressBook s + BT.put s' + BT.modify $ set dialogBox AdrBook + else do + BT.modify $ set msg "Invalid or missing data!!: " + BT.modify $ set displayBox MsgDisplay + BT.modify $ set dialogBox AdrBookForm + ev -> + BT.zoom abForm $ do + handleFormEvent (BT.VtyEvent ev) + fs <- BT.gets formState + BT.modify $ + setFieldValid + (isRecipientValid (fs ^. address)) + AddressField + -- Process delete AddresBook entry + AdrBookDelForm -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox AdrBook + V.EvKey (V.KChar 'c') [] -> do + pool <- liftIO $ runNoLoggingT $ initPool $ s ^. dbPath + fs <- BT.zoom abForm $ BT.gets formState + res <- liftIO $ deleteAdrsFromAB pool (fs ^. address) + s' <- liftIO $ refreshAddressBook s + BT.put s' + BT.modify $ set dialogBox AdrBook + ev -> BT.modify $ set dialogBox AdrBookDelForm + -- Process any other event Blank -> do case e of V.EvKey (V.KChar '\t') [] -> focusRing %= F.focusNext @@ -982,12 +1315,16 @@ appEvent (BT.VtyEvent e) = do set txForm $ mkSendForm (s ^. balance) (SendInput "" 0.0 "") BT.modify $ set dialogBox SendTx + V.EvKey (V.KChar 'b') [] -> + BT.modify $ set dialogBox AdrBook ev -> case r of Just AList -> BT.zoom addresses $ L.handleListEvent ev Just TList -> BT.zoom transactions $ L.handleListEvent ev + Just ABList -> + BT.zoom abAddresses $ L.handleListEvent ev _anyName -> return () where printMsg :: String -> BT.EventM Name State () @@ -1007,11 +1344,14 @@ theMap = , (blinkAttr, style V.blink) , (focusedFormInputAttr, V.white `on` V.blue) , (invalidFormInputAttr, V.red `on` V.black) - , (E.editAttr, V.white `on` V.blue) - , (E.editFocusedAttr, V.blue `on` V.white) + , (E.editAttr, V.white `on` V.black) + , (E.editFocusedAttr, V.black `on` V.white) , (baseAttr, bg V.brightBlack) , (barDoneAttr, V.white `on` V.blue) , (barToDoAttr, V.white `on` V.black) + , (abDefAttr, V.white `on` V.blue) + , (abSelAttr, V.black `on` V.white) + , (abMBarAttr, V.white `on` V.black) ] theApp :: M.App State Tick Name @@ -1024,8 +1364,8 @@ theApp = , M.appAttrMap = const theMap } -runZenithCLI :: Config -> IO () -runZenithCLI config = do +runZenithTUI :: Config -> IO () +runZenithTUI config = do let host = c_zebraHost config let port = c_zebraPort config let dbFilePath = c_dbPath config @@ -1057,6 +1397,7 @@ runZenithCLI config = do if not (null walList) then zcashWalletLastSync $ entityVal $ head walList else 0 + abookList <- getAdrBook pool $ zgb_net chainInfo bal <- if not (null accList) then getBalance pool $ entityKey $ head accList @@ -1101,6 +1442,9 @@ runZenithCLI config = do eventChan 0 (mkSendForm 0 $ SendInput "" 0.0 "") + (L.list ABList (Vec.fromList abookList) 1) + (mkNewABForm (AdrBookEntry "" "")) + "" Nothing uBal Left e -> do @@ -1163,14 +1507,13 @@ addNewWallet n s = do let netName = s ^. network r <- saveWallet pool $ ZcashWallet n (ZcashNetDB netName) (PhraseDB sP) bH 0 case r of - Nothing -> do - return $ s & msg .~ ("Wallet already exists: " ++ T.unpack n) + Nothing -> return $ s & msg .~ ("Wallet already exists: " ++ T.unpack n) Just _ -> do wL <- getWallets pool netName let aL = L.listFindBy (\x -> zcashWalletName (entityVal x) == n) $ L.listReplace (Vec.fromList wL) (Just 0) (s ^. wallets) - return $ (s & wallets .~ aL) & msg .~ "Created new wallet: " ++ T.unpack n + return $ s & wallets .~ aL & msg .~ "Created new wallet: " ++ T.unpack n addNewAccount :: T.Text -> State -> IO State addNewAccount n s = do @@ -1189,19 +1532,18 @@ addNewAccount n s = do try $ createZcashAccount n (aL' + 1) selWallet :: IO (Either IOError ZcashAccount) case zA of - Left e -> return $ s & msg .~ ("Error: " ++ show e) + Left e -> return $ s & msg .~ "Error: " ++ show e Right zA' -> do r <- saveAccount pool zA' case r of - Nothing -> - return $ s & msg .~ ("Account already exists: " ++ T.unpack n) + Nothing -> return $ s & msg .~ "Account already exists: " ++ T.unpack n Just x -> do aL <- runNoLoggingT $ getAccounts pool (entityKey selWallet) let nL = L.listMoveToElement x $ L.listReplace (Vec.fromList aL) (Just 0) (s ^. accounts) return $ - (s & accounts .~ nL) & msg .~ "Created new account: " ++ T.unpack n + s & accounts .~ nL & msg .~ "Created new account: " ++ T.unpack n refreshAccount :: State -> IO State refreshAccount s = do @@ -1259,6 +1601,21 @@ refreshTxs s = do let tL' = L.listReplace (Vec.fromList tList) (Just 0) (s ^. transactions) return $ s & transactions .~ tL' +refreshAddressBook :: State -> IO State +refreshAddressBook s = do + pool <- runNoLoggingT $ initPool $ s ^. dbPath + selAddress <- + do case L.listSelectedElement $ s ^. abAddresses of + Nothing -> do + let fAdd = + L.listSelectedElement $ + L.listMoveToBeginning $ s ^. abAddresses + return fAdd + Just a2 -> return $ Just a2 + abookList <- getAdrBook pool (s ^. network) + let tL' = L.listReplace (Vec.fromList abookList) (Just 0) (s ^. abAddresses) + return $ s & abAddresses .~ tL' + addNewAddress :: T.Text -> Scope -> State -> IO State addNewAddress n scope s = do pool <- runNoLoggingT $ initPool $ s ^. dbPath @@ -1276,19 +1633,18 @@ addNewAddress n scope s = do try $ createWalletAddress n (maxAddr + 1) (s ^. network) scope selAccount :: IO (Either IOError WalletAddress) case uA of - Left e -> return $ s & msg .~ ("Error: " ++ show e) + Left e -> return $ s & msg .~ "Error: " ++ show e Right uA' -> do nAddr <- saveAddress pool uA' case nAddr of - Nothing -> - return $ s & msg .~ ("Address already exists: " ++ T.unpack n) + Nothing -> return $ s & msg .~ "Address already exists: " ++ T.unpack n Just x -> do addrL <- runNoLoggingT $ getAddresses pool (entityKey selAccount) let nL = L.listMoveToElement x $ L.listReplace (Vec.fromList addrL) (Just 0) (s ^. addresses) return $ - (s & addresses .~ nL) & msg .~ "Created new address: " ++ + s & addresses .~ nL & msg .~ "Created new address: " ++ T.unpack n ++ "(" ++ T.unpack (showAddress $ walletAddressUAddress $ entityVal x) ++ ")" diff --git a/src/Zenith/Core.hs b/src/Zenith/Core.hs index a8dc6f2..abfb476 100644 --- a/src/Zenith/Core.hs +++ b/src/Zenith/Core.hs @@ -24,7 +24,7 @@ import Data.Binary.Get hiding (getBytes) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.Digest.Pure.MD5 -import Data.HexString (HexString, hexString, toBytes) +import Data.HexString (HexString, hexString, toBytes, toText) import Data.List import Data.Maybe (fromJust) import Data.Pool (Pool) @@ -574,6 +574,7 @@ prepareTx pool zebraHost zebraPort zn za bh amt ua memo = do zn (bh + 3) True + logDebugN $ T.pack $ show tx return tx where makeOutgoing :: diff --git a/src/Zenith/DB.hs b/src/Zenith/DB.hs index 384f3bb..aea3c5a 100644 --- a/src/Zenith/DB.hs +++ b/src/Zenith/DB.hs @@ -254,6 +254,12 @@ share name T.Text UniqueQr address version deriving Show Eq + AddressBook + network ZcashNetDB + abdescrip T.Text + abaddress T.Text + UniqueABA abaddress + deriving Show Eq |] -- * Database functions @@ -1666,5 +1672,55 @@ readUnifiedAddressDB :: WalletAddress -> Maybe UnifiedAddress readUnifiedAddressDB = isValidUnifiedAddress . TE.encodeUtf8 . getUA . walletAddressUAddress +-- | Get list of external zcash addresses from database +getAdrBook :: ConnectionPool -> ZcashNet -> IO [Entity AddressBook] +getAdrBook pool n = + runNoLoggingT $ + PS.retryOnBusy $ + flip PS.runSqlPool pool $ do + select $ do + adrbook <- from $ table @AddressBook + where_ (adrbook ^. AddressBookNetwork ==. val (ZcashNetDB n)) + pure adrbook + +-- | Save a new address into AddressBook +saveAdrsInAdrBook :: + ConnectionPool -- ^ The database path to use + -> AddressBook -- ^ The address to add to the database + -> IO (Maybe (Entity AddressBook)) +saveAdrsInAdrBook pool a = + runNoLoggingT $ + PS.retryOnBusy $ flip PS.runSqlPool pool $ insertUniqueEntity a + +-- | Update an existing address into AddressBook +updateAdrsInAdrBook :: ConnectionPool -> T.Text -> T.Text -> T.Text -> IO () +updateAdrsInAdrBook pool d a ia = do + runNoLoggingT $ + PS.retryOnBusy $ + flip PS.runSqlPool pool $ do + update $ \ab -> do + set ab [AddressBookAbdescrip =. val d, AddressBookAbaddress =. val a] + where_ $ ab ^. AddressBookAbaddress ==. val ia + +-- | Get one AddrssBook record using the Address as a key +-- getABookRec :: ConnectionPool -> T.Tex t -> IO (Maybe (Entity AddressBook)) +-- getABookRec pool a = do +-- runNoLoggingT $ +-- PS.retryOnBusy $ +-- flip PS.runSqlPool pool $ +-- select $ do +-- adrbook <- from $ table @AddressBook +-- where_ ((adrbook ^. AddressBookAbaddress) ==. val a) +-- return adrbook +-- | delete an existing address from AddressBook +deleteAdrsFromAB :: ConnectionPool -> T.Text -> IO () +deleteAdrsFromAB pool ia = do + runNoLoggingT $ + PS.retryOnBusy $ + flip PS.runSqlPool pool $ do + delete $ do + ab <- from $ table @AddressBook + where_ (ab ^. AddressBookAbaddress ==. val ia) + rmdups :: Ord a => [a] -> [a] rmdups = map head . group . sort diff --git a/src/Zenith/Utils.hs b/src/Zenith/Utils.hs index 6ad1816..eedf02d 100644 --- a/src/Zenith/Utils.hs +++ b/src/Zenith/Utils.hs @@ -37,18 +37,18 @@ jsonNumber i = Number $ scientific (fromIntegral i) 0 -- | Helper function to display small amounts of ZEC displayZec :: Integer -> String displayZec s - | abs s < 100 = show s ++ " zats " - | abs s < 100000 = show (fromIntegral s / 100) ++ " μZEC " - | abs s < 100000000 = show (fromIntegral s / 100000) ++ " mZEC " + | abs s < 100 = show s ++ " zats" + | abs s < 100000 = show (fromIntegral s / 100) ++ " μZEC" + | abs s < 100000000 = show (fromIntegral s / 100000) ++ " mZEC" | otherwise = show (fromIntegral s / 100000000) ++ " ZEC " -- | Helper function to display small amounts of ZEC displayTaz :: Integer -> String displayTaz s - | abs s < 100 = show s ++ " tazs " - | abs s < 100000 = show (fromIntegral s / 100) ++ " μTAZ " - | abs s < 100000000 = show (fromIntegral s / 100000) ++ " mTAZ " - | otherwise = show (fromIntegral s / 100000000) ++ " TAZ " + | abs s < 100 = show s ++ " tazs" + | abs s < 100000 = show (fromIntegral s / 100) ++ " μTAZ" + | abs s < 100000000 = show (fromIntegral s / 100000) ++ " mTAZ" + | otherwise = show (fromIntegral s / 100000000) ++ " TAZ" displayAmount :: ZcashNet -> Integer -> T.Text displayAmount n a = diff --git a/zenith.cabal b/zenith.cabal index 32ef24e..e90d4eb 100644 --- a/zenith.cabal +++ b/zenith.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: zenith -version: 0.5.3.0-beta +version: 0.5.3.1-beta license: MIT license-file: LICENSE author: Rene Vergara diff --git a/zenith_er.bmp b/zenith_er.bmp new file mode 100644 index 0000000..d248b8f Binary files /dev/null and b/zenith_er.bmp differ diff --git a/zenith_er.png b/zenith_er.png new file mode 100644 index 0000000..5d6d21c Binary files /dev/null and b/zenith_er.png differ