From b33ba29c91a21a46fcf760006fe93120d3216016 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Tue, 5 Mar 2024 12:34:30 -0600 Subject: [PATCH 1/8] Implement address creation --- src/Zenith/CLI.hs | 106 ++++++++++++++++++++++++++++++++++++++++++-- src/Zenith/Core.hs | 52 ---------------------- src/Zenith/DB.hs | 90 ++++++++++++++++++++++++++++++++----- src/Zenith/Utils.hs | 9 +++- zcash-haskell | 2 +- 5 files changed, 191 insertions(+), 68 deletions(-) diff --git a/src/Zenith/CLI.hs b/src/Zenith/CLI.hs index 447383e..87c31d9 100644 --- a/src/Zenith/CLI.hs +++ b/src/Zenith/CLI.hs @@ -57,6 +57,7 @@ import ZcashHaskell.Keys (generateWalletSeedPhrase) import ZcashHaskell.Types import Zenith.Core import Zenith.DB +import Zenith.Utils (showAddress) data Name = WList @@ -76,13 +77,14 @@ makeLenses ''DialogInput data DialogType = WName | AName + | AdName | Blank data State = State { _network :: !String , _wallets :: !(L.List Name (Entity ZcashWallet)) , _accounts :: !(L.List Name (Entity ZcashAccount)) - , _addresses :: !(L.List Name String) + , _addresses :: !(L.List Name (Entity WalletAddress)) , _transactions :: !(L.List Name String) , _msg :: !String , _helpBox :: !Bool @@ -121,10 +123,10 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] "(None)" (\(_, a) -> zcashAccountName $ entityVal a) (L.listSelectedElement (st ^. accounts))))) <=> - listBox "Addresses" (st ^. addresses) <+> + listAddressBox "Addresses" (st ^. addresses) <+> B.vBorder <+> C.center (listBox "Transactions" (st ^. transactions))) <=> msgBox (st ^. msg) - listBox :: String -> L.List Name String -> Widget Name + listBox :: Show e => String -> L.List Name e -> Widget Name listBox titleLabel l = C.vCenter $ vBox @@ -134,6 +136,17 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] , str " " , C.hCenter $ str "Select " ] + listAddressBox :: + String -> L.List Name (Entity WalletAddress) -> Widget Name + listAddressBox titleLabel a = + C.vCenter $ + vBox + [ C.hCenter + (B.borderWithLabel (str titleLabel) $ + hLimit 40 $ vLimit 15 $ L.renderList listDrawAddress True a) + , str " " + , C.hCenter $ str "Use arrows to select" + ] msgBox :: String -> Widget Name msgBox m = vBox @@ -163,6 +176,10 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] D.renderDialog (D.dialog (Just (str "Create Account")) Nothing 50) (renderForm $ st ^. inputForm) + AdName -> + D.renderDialog + (D.dialog (Just (str "Create Address")) Nothing 50) + (renderForm $ st ^. inputForm) Blank -> emptyWidget splashDialog :: State -> Widget Name splashDialog st = @@ -194,6 +211,33 @@ listDrawElement sel a = else str s in C.hCenter $ selStr $ show a +listDrawWallet :: Bool -> Entity ZcashWallet -> Widget Name +listDrawWallet sel w = + let selStr s = + if sel + then withAttr customAttr (txt $ "<" <> s <> ">") + else txt s + in C.hCenter $ selStr $ zcashWalletName (entityVal w) + +listDrawAccount :: Bool -> Entity ZcashAccount -> Widget Name +listDrawAccount sel w = + let selStr s = + if sel + then withAttr customAttr (txt $ "<" <> s <> ">") + else txt s + in C.hCenter $ selStr $ zcashAccountName (entityVal w) + +listDrawAddress :: Bool -> Entity WalletAddress -> Widget Name +listDrawAddress sel w = + let selStr s = + if sel + then withAttr customAttr (txt $ "<" <> s <> ">") + else txt s + in C.hCenter $ + selStr $ + walletAddressName (entityVal w) <> + ": " <> showAddress (walletAddressUAddress (entityVal w)) + customAttr :: A.AttrName customAttr = L.listSelectedAttr <> A.attrName "custom" @@ -238,6 +282,20 @@ appEvent (BT.VtyEvent e) = do fs <- BT.zoom inputForm $ BT.gets formState na <- liftIO $ addNewAccount (fs ^. dialogInput) s BT.put na + addrL <- use addresses + BT.modify $ + set dialogBox $ + if not (null $ L.listElements addrL) + then Blank + else AdName + ev -> BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) + AdName -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank + V.EvKey V.KEnter [] -> do + fs <- BT.zoom inputForm $ BT.gets formState + nAddr <- liftIO $ addNewAddress (fs ^. dialogInput) s + BT.put nAddr BT.modify $ set dialogBox Blank ev -> BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) Blank -> do @@ -306,13 +364,17 @@ runZenithCLI host port dbFilePath = do if not (null walList) then getAccounts dbFilePath $ entityKey $ head walList else return [] + addrList <- + if not (null accList) + then getAddresses dbFilePath $ entityKey $ head accList + else return [] void $ M.defaultMain theApp $ State ((show . zgb_net) chainInfo) (L.list WList (Vec.fromList walList) 1) (L.list AcList (Vec.fromList accList) 0) - (L.list AList (Vec.fromList ["utest...hn8zg", "utest...qfex8"]) 1) + (L.list AList (Vec.fromList addrList) 1) (L.list TList (Vec.fromList ["tx1", "tx2", "tx3"]) 1) ("Start up Ok! Connected to Zebra " ++ (T.unpack . zgi_build) zebra ++ " on port " ++ show port ++ ".") @@ -376,3 +438,39 @@ addNewAccount n s = do L.listReplace (Vec.fromList aL) (Just 0) (s ^. accounts) return $ (s & accounts .~ nL) & msg .~ "Created new account: " ++ T.unpack n + +addNewAddress :: T.Text -> State -> IO State +addNewAddress n s = do + selAccount <- + do case L.listSelectedElement $ s ^. accounts of + Nothing -> do + let fAcc = + L.listSelectedElement $ L.listMoveToBeginning $ s ^. accounts + case fAcc of + Nothing -> throw $ userError "Failed to select account" + Just (_j, a1) -> return a1 + Just (_k, a) -> return a + maxAddr <- getMaxAddress (s ^. dbPath) (entityKey selAccount) + nAddr <- + saveAddress (s ^. dbPath) $ + WalletAddress + (maxAddr + 1) + (entityKey selAccount) + n + (UnifiedAddress + MainNet + "fakeBstring" + "fakeBString" + (Just $ TransparentAddress P2PKH MainNet "fakeBString")) + case nAddr of + Nothing -> return $ s & msg .~ ("Address already exists: " ++ T.unpack n) + Just x -> do + addrL <- getAddresses (s ^. dbPath) (entityKey selAccount) + let nL = + L.listMoveToElement x $ + L.listReplace (Vec.fromList addrL) (Just 0) (s ^. addresses) + return $ + (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 7d6a4e7..d22f4ec 100644 --- a/src/Zenith/Core.hs +++ b/src/Zenith/Core.hs @@ -5,61 +5,9 @@ module Zenith.Core where import Data.Aeson import qualified Data.Text as T -import Database.Persist -import Database.Persist.Sqlite import Network.HTTP.Client import ZcashHaskell.Types import ZcashHaskell.Utils -import Zenith.DB - --- * Database functions --- | Initializes the database -initDb :: - T.Text -- ^ The database path to check - -> IO () -initDb dbName = do - runSqlite dbName $ do runMigration migrateAll - --- | Save a new wallet to the database -saveWallet :: - T.Text -- ^ The database path to use - -> ZcashWallet -- ^ The wallet to add to the database - -> IO (Maybe (Entity ZcashWallet)) -saveWallet dbFp w = runSqlite dbFp $ insertUniqueEntity w - --- | Returns a list of accounts associated with the given wallet -getAccounts :: - T.Text -- ^ The database path - -> ZcashWalletId -- ^ The wallet ID to check - -> IO [Entity ZcashAccount] -getAccounts dbFp w = runSqlite dbFp $ selectList [ZcashAccountWalletId ==. w] [] - --- | Returns the largest account index for the given wallet -getMaxAccount :: - T.Text -- ^ The database path - -> ZcashWalletId -- ^ The wallet ID to check - -> IO Int -getMaxAccount dbFp w = do - a <- - runSqlite dbFp $ - selectFirst [ZcashAccountWalletId ==. w] [Desc ZcashAccountIndex] - case a of - Nothing -> return $ -1 - Just x -> return $ zcashAccountIndex $ entityVal x - --- | Save a new account to the database -saveAccount :: - T.Text -- ^ The database path - -> ZcashAccount -- ^ The account to add to the database - -> IO (Maybe (Entity ZcashAccount)) -saveAccount dbFp a = runSqlite dbFp $ insertUniqueEntity a - --- | Returns a list of addresses associated with the given account -getAddresses :: - T.Text -- ^ The database path - -> ZcashAccountId -- ^ The account ID to check - -> IO [Entity WalletAddress] -getAddresses dbFp a = runSqlite dbFp $ selectList [WalletAddressAccId ==. a] [] -- * Zebra Node interaction -- | Checks the status of the `zebrad` node diff --git a/src/Zenith/DB.hs b/src/Zenith/DB.hs index ef6b324..9ead0e7 100644 --- a/src/Zenith/DB.hs +++ b/src/Zenith/DB.hs @@ -23,10 +23,12 @@ import qualified Data.Text as T import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH -import ZcashHaskell.Types (Phrase, ZcashNet) +import ZcashHaskell.Types (Phrase, UnifiedAddress(..), ZcashNet) derivePersistField "ZcashNet" +derivePersistField "UnifiedAddress" + share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| @@ -45,19 +47,87 @@ share sapSpendKey BS.ByteString tPrivateKey BS.ByteString UniqueAccount index walletId + UniqueAccName walletId name deriving Show Eq WalletAddress - accId ZcashAccountId index Int - orchRec BS.ByteString Maybe - sapRec BS.ByteString Maybe - tRec BS.ByteString Maybe - encoded T.Text + accId ZcashAccountId + name T.Text + uAddress UnifiedAddress + UniqueAddress index accId + UniqueAddName accId name deriving Show Eq |] +-- * Database functions +-- | Initializes the database +initDb :: + T.Text -- ^ The database path to check + -> IO () +initDb dbName = do + runSqlite dbName $ do runMigration migrateAll + +-- | Get existing wallets from database getWallets :: T.Text -> ZcashNet -> IO [Entity ZcashWallet] -getWallets dbFp n = - runSqlite dbFp $ do - s <- selectList [ZcashWalletNetwork ==. n] [] - liftIO $ return s +getWallets dbFp n = runSqlite dbFp $ selectList [ZcashWalletNetwork ==. n] [] + +-- | Save a new wallet to the database +saveWallet :: + T.Text -- ^ The database path to use + -> ZcashWallet -- ^ The wallet to add to the database + -> IO (Maybe (Entity ZcashWallet)) +saveWallet dbFp w = runSqlite dbFp $ insertUniqueEntity w + +-- | Returns a list of accounts associated with the given wallet +getAccounts :: + T.Text -- ^ The database path + -> ZcashWalletId -- ^ The wallet ID to check + -> IO [Entity ZcashAccount] +getAccounts dbFp w = runSqlite dbFp $ selectList [ZcashAccountWalletId ==. w] [] + +-- | Returns the largest account index for the given wallet +getMaxAccount :: + T.Text -- ^ The database path + -> ZcashWalletId -- ^ The wallet ID to check + -> IO Int +getMaxAccount dbFp w = do + a <- + runSqlite dbFp $ + selectFirst [ZcashAccountWalletId ==. w] [Desc ZcashAccountIndex] + case a of + Nothing -> return $ -1 + Just x -> return $ zcashAccountIndex $ entityVal x + +-- | Save a new account to the database +saveAccount :: + T.Text -- ^ The database path + -> ZcashAccount -- ^ The account to add to the database + -> IO (Maybe (Entity ZcashAccount)) +saveAccount dbFp a = runSqlite dbFp $ insertUniqueEntity a + +-- | Returns a list of addresses associated with the given account +getAddresses :: + T.Text -- ^ The database path + -> ZcashAccountId -- ^ The account ID to check + -> IO [Entity WalletAddress] +getAddresses dbFp a = runSqlite dbFp $ selectList [WalletAddressAccId ==. a] [] + +-- | Returns the largest address index for the given account +getMaxAddress :: + T.Text -- ^ The database path + -> ZcashAccountId -- ^ The wallet ID to check + -> IO Int +getMaxAddress dbFp w = do + a <- + runSqlite dbFp $ + selectFirst [WalletAddressAccId ==. w] [Desc WalletAddressIndex] + case a of + Nothing -> return $ -1 + Just x -> return $ walletAddressIndex $ entityVal x + +-- | Save a new address to the database +saveAddress :: + T.Text -- ^ the database path + -> WalletAddress -- ^ The wallet to add to the database + -> IO (Maybe (Entity WalletAddress)) +saveAddress dbFp w = runSqlite dbFp $ insertUniqueEntity w diff --git a/src/Zenith/Utils.hs b/src/Zenith/Utils.hs index f2b42a4..6ec841e 100644 --- a/src/Zenith/Utils.hs +++ b/src/Zenith/Utils.hs @@ -13,8 +13,9 @@ import qualified Data.Text.IO as TIO import System.Process (createProcess_, shell) import Text.Read (readMaybe) import Text.Regex.Posix -import ZcashHaskell.Orchard (isValidUnifiedAddress) +import ZcashHaskell.Orchard (encodeUnifiedAddress, isValidUnifiedAddress) import ZcashHaskell.Sapling (isValidShieldedAddress) +import ZcashHaskell.Types (UnifiedAddress(..)) import Zenith.Types ( AddressGroup(..) , AddressSource(..) @@ -30,6 +31,12 @@ displayZec s | s < 100000000 = show (fromIntegral s / 100000) ++ " mZEC " | otherwise = show (fromIntegral s / 100000000) ++ " ZEC " +-- | Helper function to display abbreviated Unified Address +showAddress :: UnifiedAddress -> T.Text +showAddress u = T.take 8 t <> "..." <> T.takeEnd 8 t + where + t = encodeUnifiedAddress u + -- | Helper function to extract addresses from AddressGroups getAddresses :: AddressGroup -> [ZcashAddress] getAddresses ag = agtransparent ag <> agsapling ag <> agunified ag diff --git a/zcash-haskell b/zcash-haskell index a52d223..c1507f3 160000 --- a/zcash-haskell +++ b/zcash-haskell @@ -1 +1 @@ -Subproject commit a52d2231f1a4f85a6504bfb9228a1475a0773088 +Subproject commit c1507f36e0146f0be76ee2a71cb2b3b4ebd9f3cf From e1262bf5f7daaf10f33c53fb9b4c39ade43fbe54 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Thu, 7 Mar 2024 08:01:29 -0600 Subject: [PATCH 2/8] Add error handling for account creation --- src/Zenith/CLI.hs | 49 +++++++++++++++++++++++----------------------- src/Zenith/Core.hs | 35 +++++++++++++++++++++++++++++++++ zcash-haskell | 2 +- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/Zenith/CLI.hs b/src/Zenith/CLI.hs index 87c31d9..1bc2c77 100644 --- a/src/Zenith/CLI.hs +++ b/src/Zenith/CLI.hs @@ -3,9 +3,10 @@ module Zenith.CLI where -import Control.Exception (throw) +import Control.Exception (throw, try) import Control.Monad (void) import Control.Monad.IO.Class (liftIO) +import Data.Maybe import qualified Data.Text as T import qualified Graphics.Vty as V import Lens.Micro ((&), (.~), (^.), set) @@ -53,7 +54,8 @@ import qualified Brick.Widgets.Dialog as D import qualified Brick.Widgets.List as L import qualified Data.Vector as Vec import Database.Persist -import ZcashHaskell.Keys (generateWalletSeedPhrase) +import ZcashHaskell.Keys (generateWalletSeedPhrase, getWalletSeed) +import ZcashHaskell.Orchard (genOrchardSpendingKey) import ZcashHaskell.Types import Zenith.Core import Zenith.DB @@ -81,7 +83,7 @@ data DialogType | Blank data State = State - { _network :: !String + { _network :: !ZcashNet , _wallets :: !(L.List Name (Entity ZcashWallet)) , _accounts :: !(L.List Name (Entity ZcashAccount)) , _addresses :: !(L.List Name (Entity WalletAddress)) @@ -108,7 +110,7 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] B.borderWithLabel (str ("Zenith - " <> - st ^. network <> + show (st ^. network) <> " - " <> T.unpack (maybe @@ -371,7 +373,7 @@ runZenithCLI host port dbFilePath = do void $ M.defaultMain theApp $ State - ((show . zgb_net) chainInfo) + (zgb_net chainInfo) (L.list WList (Vec.fromList walList) 1) (L.list AcList (Vec.fromList accList) 0) (L.list AList (Vec.fromList addrList) 1) @@ -396,7 +398,7 @@ addNewWallet :: T.Text -> State -> IO State addNewWallet n s = do sP <- generateWalletSeedPhrase let bH = s ^. startBlock - let netName = read $ s ^. network + let netName = s ^. network r <- saveWallet (s ^. dbPath) $ ZcashWallet n netName sP bH case r of Nothing -> do @@ -420,24 +422,23 @@ addNewAccount n s = do Just (_j, w1) -> return w1 Just (_k, w) -> return w aL' <- getMaxAccount (s ^. dbPath) (entityKey selWallet) - r <- - saveAccount (s ^. dbPath) $ - ZcashAccount - (aL' + 1) - (entityKey selWallet) - n - "fakeOrchKey" - "fakeSapKey" - "fakeTKey" - case r of - Nothing -> return $ s & msg .~ ("Account already exists: " ++ T.unpack n) - Just x -> do - aL <- getAccounts (s ^. dbPath) (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 + zA <- + try $ createZcashAccount n (aL' + 1) selWallet :: IO + (Either IOError ZcashAccount) + case zA of + Left e -> return $ s & msg .~ ("Error: " ++ show e) + Right zA' -> do + r <- saveAccount (s ^. dbPath) zA' + case r of + Nothing -> + return $ s & msg .~ ("Account already exists: " ++ T.unpack n) + Just x -> do + aL <- getAccounts (s ^. dbPath) (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 addNewAddress :: T.Text -> State -> IO State addNewAddress n s = do diff --git a/src/Zenith/Core.hs b/src/Zenith/Core.hs index d22f4ec..a47b76a 100644 --- a/src/Zenith/Core.hs +++ b/src/Zenith/Core.hs @@ -3,11 +3,17 @@ -- Core wallet functionality for Zenith module Zenith.Core where +import Control.Exception (throwIO) import Data.Aeson +import qualified Data.ByteString as BS import qualified Data.Text as T +import Database.Persist import Network.HTTP.Client +import ZcashHaskell.Keys +import ZcashHaskell.Orchard import ZcashHaskell.Types import ZcashHaskell.Utils +import Zenith.DB -- * Zebra Node interaction -- | Checks the status of the `zebrad` node @@ -36,3 +42,32 @@ connectZebra nodeHost nodePort m params = do res <- makeZebraCall nodeHost nodePort m params let body = responseBody res return $ result body + +-- * Spending Keys +-- | Create an Orchard Spending Key for the given wallet and account index +createOrchardSpendingKey :: ZcashWallet -> Int -> IO BS.ByteString +createOrchardSpendingKey zw i = do + let s = getWalletSeed $ zcashWalletSeedPhrase zw + case s of + Nothing -> throwIO $ userError "Unable to generate seed" + Just s' -> do + let coinType = + case zcashWalletNetwork zw of + MainNet -> MainNetCoin + TestNet -> TestNetCoin + RegTestNet -> RegTestNetCoin + let r = genOrchardSpendingKey s' coinType i + case r of + Nothing -> throwIO $ userError "Unable to generate Orchard spending key" + Just sk -> return sk + +-- * Accounts +-- | Create an account for the given wallet and account index +createZcashAccount :: + T.Text -- ^ The account's name + -> Int -- ^ The account's index + -> Entity ZcashWallet -- ^ The Zcash wallet that this account will be attached to + -> IO ZcashAccount +createZcashAccount n i zw = do + orSk <- createOrchardSpendingKey (entityVal zw) i + return $ ZcashAccount i (entityKey zw) n orSk "fakeSapKey" "fakeTkey" diff --git a/zcash-haskell b/zcash-haskell index c1507f3..e371fcd 160000 --- a/zcash-haskell +++ b/zcash-haskell @@ -1 +1 @@ -Subproject commit c1507f36e0146f0be76ee2a71cb2b3b4ebd9f3cf +Subproject commit e371fcdb724686bfb51157911c5d4c0fda433c53 From a366d3a87b11dca9046fd8a1aee565dcd327583b Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Thu, 7 Mar 2024 12:34:55 -0600 Subject: [PATCH 3/8] Change display of UAs per ZIP-316 --- src/Zenith/Utils.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Zenith/Utils.hs b/src/Zenith/Utils.hs index 6ec841e..86a58ed 100644 --- a/src/Zenith/Utils.hs +++ b/src/Zenith/Utils.hs @@ -33,7 +33,7 @@ displayZec s -- | Helper function to display abbreviated Unified Address showAddress :: UnifiedAddress -> T.Text -showAddress u = T.take 8 t <> "..." <> T.takeEnd 8 t +showAddress u = T.take 20 t <> "..." where t = encodeUnifiedAddress u From 856ade051e5e62c609a707874c7897231dd96c10 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Thu, 7 Mar 2024 14:20:06 -0600 Subject: [PATCH 4/8] Implement wallet and account switching --- src/Zenith/CLI.hs | 336 +++++++++++++++++++++++++++++++-------------- src/Zenith/Core.hs | 22 ++- 2 files changed, 254 insertions(+), 104 deletions(-) diff --git a/src/Zenith/CLI.hs b/src/Zenith/CLI.hs index 1bc2c77..fa4d503 100644 --- a/src/Zenith/CLI.hs +++ b/src/Zenith/CLI.hs @@ -44,7 +44,9 @@ import Brick.Widgets.Core , padBottom , padRight , str + , strWrap , txt + , txtWrap , vBox , vLimit , withAttr @@ -55,7 +57,7 @@ import qualified Brick.Widgets.List as L import qualified Data.Vector as Vec import Database.Persist import ZcashHaskell.Keys (generateWalletSeedPhrase, getWalletSeed) -import ZcashHaskell.Orchard (genOrchardSpendingKey) +import ZcashHaskell.Orchard (encodeUnifiedAddress, genOrchardSpendingKey) import ZcashHaskell.Types import Zenith.Core import Zenith.DB @@ -80,8 +82,15 @@ data DialogType = WName | AName | AdName + | WSelect + | ASelect | Blank +data DisplayType + = AddrDisplay + | MsgDisplay + | BlankDisplay + data State = State { _network :: !ZcashNet , _wallets :: !(L.List Name (Entity ZcashWallet)) @@ -96,12 +105,13 @@ data State = State , _focusRing :: !(F.FocusRing Name) , _startBlock :: !Int , _dbPath :: !T.Text + , _displayBox :: !DisplayType } makeLenses ''State drawUI :: State -> [Widget Name] -drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] +drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] where ui :: State -> Widget Name ui st = @@ -116,18 +126,17 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] (maybe "(None)" (\(_, w) -> zcashWalletName $ entityVal w) - (L.listSelectedElement (st ^. wallets))))) $ - (C.hCenter - (str - ("Account: " ++ - T.unpack - (maybe - "(None)" - (\(_, a) -> zcashAccountName $ entityVal a) - (L.listSelectedElement (st ^. accounts))))) <=> - listAddressBox "Addresses" (st ^. addresses) <+> - B.vBorder <+> C.center (listBox "Transactions" (st ^. transactions))) <=> - msgBox (st ^. msg) + (L.listSelectedElement (st ^. wallets))))) + (C.hCenter + (str + ("Account: " ++ + T.unpack + (maybe + "(None)" + (\(_, a) -> zcashAccountName $ entityVal a) + (L.listSelectedElement (st ^. accounts))))) <=> + listAddressBox "Addresses" (st ^. addresses) <+> + B.vBorder <+> C.center (listBox "Transactions" (st ^. transactions))) listBox :: Show e => String -> L.List Name e -> Widget Name listBox titleLabel l = C.vCenter $ @@ -138,6 +147,20 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] , str " " , C.hCenter $ str "Select " ] + selectListBox :: + Show e + => String + -> L.List Name e + -> (Bool -> e -> Widget Name) + -> Widget Name + selectListBox titleLabel l drawF = + vBox + [ C.hCenter + (B.borderWithLabel (str titleLabel) $ + hLimit 25 $ vLimit 15 $ L.renderList drawF True l) + , str " " + , C.hCenter $ str "Select " + ] listAddressBox :: String -> L.List Name (Entity WalletAddress) -> Widget Name listAddressBox titleLabel a = @@ -149,10 +172,6 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] , str " " , C.hCenter $ str "Use arrows to select" ] - msgBox :: String -> Widget Name - msgBox m = - vBox - [B.hBorderWithLabel (str "Messages"), hLimit 70 $ padRight Max $ str m] helpDialog :: State -> Widget Name helpDialog st = if st ^. helpBox @@ -162,11 +181,17 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] vBox ([str "Actions", B.hBorder] <> actionList)) else emptyWidget where - keyList = map (C.hCenter . str) ["?", "Esc", "c", "q"] + keyList = map (C.hCenter . str) ["?", "Esc", "w", "a", "v", "q"] actionList = map (hLimit 40 . str) - ["Open help", "Close dialog", "Create Wallet", "Quit"] + [ "Open help" + , "Close dialog" + , "Switch wallets" + , "Switch accounts" + , "View address" + , "Quit" + ] inputDialog :: State -> Widget Name inputDialog st = case st ^. dialogBox of @@ -182,6 +207,14 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] D.renderDialog (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) + ASelect -> + D.renderDialog + (D.dialog (Just (str "Select Account")) Nothing 50) + (selectListBox "Accounts" (st ^. accounts) listDrawAccount) Blank -> emptyWidget splashDialog :: State -> Widget Name splashDialog st = @@ -196,6 +229,28 @@ drawUI s = [splashDialog s, helpDialog s, inputDialog s, ui s] C.hCenter (withAttr titleAttr (str "Zcash Wallet v0.4.3.0")) <=> C.hCenter (withAttr blinkAttr $ str "Press any key...")) else emptyWidget + displayDialog :: State -> Widget Name + displayDialog st = + case st ^. displayBox of + AddrDisplay -> + case L.listSelectedElement $ st ^. addresses of + Just (_, a) -> + withBorderStyle unicodeBold $ + D.renderDialog + (D.dialog + (Just $ txt ("Address: " <> walletAddressName (entityVal a))) + Nothing + 60) + (padAll 1 $ + txtWrap $ + encodeUnifiedAddress $ walletAddressUAddress $ entityVal a) + Nothing -> emptyWidget + MsgDisplay -> + withBorderStyle unicodeBold $ + D.renderDialog + (D.dialog (Just $ txt "Message") Nothing 50) + (padAll 1 $ strWrap $ st ^. msg) + BlankDisplay -> emptyWidget mkInputForm :: DialogInput -> Form DialogInput e Name mkInputForm = @@ -262,66 +317,104 @@ appEvent (BT.VtyEvent e) = do BT.modify $ set helpBox False _ev -> return () else do - case s ^. dialogBox of - WName -> do - case e of - V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank - V.EvKey V.KEnter [] -> do - fs <- BT.zoom inputForm $ BT.gets formState - nw <- liftIO $ addNewWallet (fs ^. dialogInput) s - BT.put nw - aL <- use accounts - BT.modify $ - set dialogBox $ - if not (null $ L.listElements aL) - then Blank - else AName - ev -> BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) - AName -> do - case e of - V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank - V.EvKey V.KEnter [] -> do - fs <- BT.zoom inputForm $ BT.gets formState - na <- liftIO $ addNewAccount (fs ^. dialogInput) s - BT.put na - addrL <- use addresses - BT.modify $ - set dialogBox $ - if not (null $ L.listElements addrL) - then Blank - else AdName - ev -> BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) - AdName -> do - case e of - V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank - V.EvKey V.KEnter [] -> do - fs <- BT.zoom inputForm $ BT.gets formState - nAddr <- liftIO $ addNewAddress (fs ^. dialogInput) s - BT.put nAddr - BT.modify $ set dialogBox Blank - ev -> BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) - Blank -> do - case e of - V.EvKey (V.KChar '\t') [] -> focusRing %= F.focusNext - V.EvKey (V.KChar 'q') [] -> M.halt - V.EvKey (V.KChar '?') [] -> BT.modify $ set helpBox True - V.EvKey (V.KChar 'w') [] -> do - BT.modify $ - set inputForm $ - updateFormState (DialogInput "New Wallet") $ - s ^. inputForm - BT.modify $ set dialogBox WName - V.EvKey (V.KChar 'a') [] -> do - BT.modify $ - set inputForm $ - updateFormState (DialogInput "New Account") $ - s ^. inputForm - BT.modify $ set dialogBox AName - ev -> - case r of - Just AList -> BT.zoom addresses $ L.handleListEvent ev - Just TList -> BT.zoom transactions $ L.handleListEvent ev - _anyName -> return () + case s ^. displayBox of + AddrDisplay -> BT.modify $ set displayBox BlankDisplay + MsgDisplay -> BT.modify $ set displayBox BlankDisplay + BlankDisplay -> do + case s ^. dialogBox of + WName -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank + V.EvKey V.KEnter [] -> do + fs <- BT.zoom inputForm $ BT.gets formState + nw <- liftIO $ addNewWallet (fs ^. dialogInput) s + ns <- liftIO $ refreshWallet nw + BT.put ns + aL <- use accounts + BT.modify $ set displayBox MsgDisplay + BT.modify $ + set dialogBox $ + if not (null $ L.listElements aL) + then Blank + else AName + ev -> + BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) + AName -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank + V.EvKey V.KEnter [] -> do + fs <- BT.zoom inputForm $ BT.gets formState + na <- liftIO $ addNewAccount (fs ^. dialogInput) s + ns <- liftIO $ refreshAccount na + BT.put ns + addrL <- use addresses + BT.modify $ set displayBox MsgDisplay + BT.modify $ + set dialogBox $ + if not (null $ L.listElements addrL) + then Blank + else AdName + ev -> + BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) + AdName -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank + V.EvKey V.KEnter [] -> do + fs <- BT.zoom inputForm $ BT.gets formState + nAddr <- liftIO $ addNewAddress (fs ^. dialogInput) s + BT.put nAddr + BT.modify $ set displayBox MsgDisplay + BT.modify $ set dialogBox Blank + ev -> + BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) + WSelect -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank + V.EvKey V.KEnter [] -> do + ns <- liftIO $ refreshWallet s + BT.put ns + BT.modify $ set dialogBox Blank + V.EvKey (V.KChar 'c') [] -> do + BT.modify $ + set inputForm $ + updateFormState (DialogInput "New Wallet") $ + s ^. inputForm + BT.modify $ set dialogBox WName + ev -> BT.zoom wallets $ L.handleListEvent ev + ASelect -> do + case e of + V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank + V.EvKey V.KEnter [] -> do + ns <- liftIO $ refreshAccount s + BT.put ns + BT.modify $ set dialogBox Blank + V.EvKey (V.KChar 'c') [] -> do + BT.modify $ + set inputForm $ + updateFormState (DialogInput "New Account") $ + s ^. inputForm + BT.modify $ set dialogBox AName + ev -> BT.zoom accounts $ L.handleListEvent ev + Blank -> do + case e of + V.EvKey (V.KChar '\t') [] -> focusRing %= F.focusNext + V.EvKey (V.KChar 'q') [] -> M.halt + V.EvKey (V.KChar '?') [] -> BT.modify $ set helpBox True + V.EvKey (V.KChar 'n') [] -> + BT.modify $ set dialogBox AdName + V.EvKey (V.KChar 'v') [] -> + BT.modify $ set displayBox AddrDisplay + V.EvKey (V.KChar 'w') [] -> + BT.modify $ set dialogBox WSelect + V.EvKey (V.KChar 'a') [] -> + BT.modify $ set dialogBox ASelect + ev -> + case r of + Just AList -> + BT.zoom addresses $ L.handleListEvent ev + Just TList -> + BT.zoom transactions $ L.handleListEvent ev + _anyName -> return () where printMsg :: String -> BT.EventM Name State () printMsg s = BT.modify $ updateMsg s @@ -389,11 +482,34 @@ runZenithCLI host port dbFilePath = do (F.focusRing [AList, TList]) (zgb_blocks chainInfo) dbFilePath + MsgDisplay Nothing -> do print $ "No Zebra node available on port " <> show port <> ". Check your configuration" +refreshWallet :: State -> IO State +refreshWallet s = do + selWallet <- + do case L.listSelectedElement $ s ^. wallets of + Nothing -> do + let fWall = + L.listSelectedElement $ L.listMoveToBeginning $ s ^. wallets + case fWall of + Nothing -> throw $ userError "Failed to select wallet" + Just (_j, w1) -> return w1 + Just (_k, w) -> return w + aL <- getAccounts (s ^. dbPath) $ entityKey selWallet + addrL <- + if not (null aL) + then getAddresses (s ^. dbPath) $ entityKey $ head aL + else return [] + let aL' = L.listReplace (Vec.fromList aL) (Just 0) (s ^. accounts) + let addrL' = L.listReplace (Vec.fromList addrL) (Just 0) (s ^. addresses) + return $ + (s & accounts .~ aL') & addresses .~ addrL' & msg .~ "Switched to wallet: " ++ + T.unpack (zcashWalletName $ entityVal selWallet) + addNewWallet :: T.Text -> State -> IO State addNewWallet n s = do sP <- generateWalletSeedPhrase @@ -440,6 +556,23 @@ addNewAccount n s = do return $ (s & accounts .~ nL) & msg .~ "Created new account: " ++ T.unpack n +refreshAccount :: State -> IO State +refreshAccount s = do + selAccount <- + do case L.listSelectedElement $ s ^. accounts of + Nothing -> do + let fAcc = + L.listSelectedElement $ L.listMoveToBeginning $ s ^. accounts + case fAcc of + Nothing -> throw $ userError "Failed to select account" + Just (_j, w1) -> return w1 + Just (_k, w) -> return w + aL <- getAddresses (s ^. dbPath) $ entityKey selAccount + let aL' = L.listReplace (Vec.fromList aL) (Just 0) (s ^. addresses) + return $ + s & addresses .~ aL' & msg .~ "Switched to account: " ++ + T.unpack (zcashAccountName $ entityVal selAccount) + addNewAddress :: T.Text -> State -> IO State addNewAddress n s = do selAccount <- @@ -452,26 +585,23 @@ addNewAddress n s = do Just (_j, a1) -> return a1 Just (_k, a) -> return a maxAddr <- getMaxAddress (s ^. dbPath) (entityKey selAccount) - nAddr <- - saveAddress (s ^. dbPath) $ - WalletAddress - (maxAddr + 1) - (entityKey selAccount) - n - (UnifiedAddress - MainNet - "fakeBstring" - "fakeBString" - (Just $ TransparentAddress P2PKH MainNet "fakeBString")) - case nAddr of - Nothing -> return $ s & msg .~ ("Address already exists: " ++ T.unpack n) - Just x -> do - addrL <- getAddresses (s ^. dbPath) (entityKey selAccount) - let nL = - L.listMoveToElement x $ - L.listReplace (Vec.fromList addrL) (Just 0) (s ^. addresses) - return $ - (s & addresses .~ nL) & msg .~ "Created new address: " ++ - T.unpack n ++ - "(" ++ - T.unpack (showAddress $ walletAddressUAddress $ entityVal x) ++ ")" + uA <- + try $ createWalletAddress n (maxAddr + 1) (s ^. network) selAccount :: IO + (Either IOError WalletAddress) + case uA of + Left e -> return $ s & msg .~ ("Error: " ++ show e) + Right uA' -> do + nAddr <- saveAddress (s ^. dbPath) uA' + case nAddr of + Nothing -> + return $ s & msg .~ ("Address already exists: " ++ T.unpack n) + Just x -> do + addrL <- getAddresses (s ^. dbPath) (entityKey selAccount) + let nL = + L.listMoveToElement x $ + L.listReplace (Vec.fromList addrL) (Just 0) (s ^. addresses) + return $ + (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 a47b76a..da73809 100644 --- a/src/Zenith/Core.hs +++ b/src/Zenith/Core.hs @@ -33,7 +33,7 @@ checkBlockChain :: -> IO (Maybe ZebraGetBlockChainInfo) checkBlockChain nodeHost nodePort = do let f = makeZebraCall nodeHost nodePort - result <$> (responseBody <$> f "getblockchaininfo" []) + result . responseBody <$> f "getblockchaininfo" [] -- | Generic RPC call function connectZebra :: @@ -71,3 +71,23 @@ createZcashAccount :: createZcashAccount n i zw = do orSk <- createOrchardSpendingKey (entityVal zw) i return $ ZcashAccount i (entityKey zw) n orSk "fakeSapKey" "fakeTkey" + +-- * Addresses +-- | Create a unified address for the given account and index +createWalletAddress :: + T.Text -- ^ The address nickname + -> Int -- ^ The address' index + -> ZcashNet -- ^ The network for this address + -> Entity ZcashAccount -- ^ The Zcash account that the address will be attached to + -> IO WalletAddress +createWalletAddress n i zNet za = do + return $ + WalletAddress + i + (entityKey za) + n + (UnifiedAddress + zNet + "fakeBString" + "fakeBString" + (Just $ TransparentAddress P2PKH zNet "fakeBString")) From 2d119d24f1007397473583c2f3a2a60f6ff70e9d Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Thu, 14 Mar 2024 12:48:39 -0500 Subject: [PATCH 5/8] Upgrade to Zcash-Haskell 0.5 --- zcash-haskell | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zcash-haskell b/zcash-haskell index e371fcd..4963eea 160000 --- a/zcash-haskell +++ b/zcash-haskell @@ -1 +1 @@ -Subproject commit e371fcdb724686bfb51157911c5d4c0fda433c53 +Subproject commit 4963eea68bd1e3b38cbc14a64888d3f5aaef3f85 From bd32eb4f38a55efa9aec9fcf4a736d223c744e04 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Sun, 17 Mar 2024 07:17:52 -0500 Subject: [PATCH 6/8] Implement internal change addresses --- cabal.project | 2 +- src/Zenith/CLI.hs | 53 +++++++++++++------------ src/Zenith/Core.hs | 95 +++++++++++++++++++++++++++++++++++++++------ src/Zenith/DB.hs | 48 +++++++++++++++-------- src/Zenith/Types.hs | 55 ++++++++++++++++++++++++++ src/Zenith/Utils.hs | 9 ++--- test/Spec.hs | 74 +++++++++++++++++++++++++---------- zcash-haskell | 2 +- zenith.cabal | 13 +++---- 9 files changed, 262 insertions(+), 89 deletions(-) diff --git a/cabal.project b/cabal.project index cf9dbbc..217198a 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,7 @@ with-compiler: ghc-9.4.8 source-repository-package type: git location: https://git.vergara.tech/Vergara_Tech/haskell-hexstring.git - tag: fd1ddce73c0ad18a2a4509a299c6e93f8c6c383d + tag: 39d8da7b11a80269454c2f134a5c834e0f3cb9a7 source-repository-package type: git diff --git a/src/Zenith/CLI.hs b/src/Zenith/CLI.hs index fa4d503..5ce1a69 100644 --- a/src/Zenith/CLI.hs +++ b/src/Zenith/CLI.hs @@ -3,16 +3,6 @@ module Zenith.CLI where -import Control.Exception (throw, try) -import Control.Monad (void) -import Control.Monad.IO.Class (liftIO) -import Data.Maybe -import qualified Data.Text as T -import qualified Graphics.Vty as V -import Lens.Micro ((&), (.~), (^.), set) -import Lens.Micro.Mtl -import Lens.Micro.TH - import qualified Brick.AttrMap as A import qualified Brick.Focus as F import Brick.Forms @@ -42,11 +32,10 @@ import Brick.Widgets.Core , joinBorders , padAll , padBottom - , padRight , str , strWrap , txt - , txtWrap + , txtWrapWith , vBox , vLimit , withAttr @@ -54,13 +43,23 @@ import Brick.Widgets.Core ) import qualified Brick.Widgets.Dialog as D import qualified Brick.Widgets.List as L +import Control.Exception (throw, throwIO, try) +import Control.Monad (void) +import Control.Monad.IO.Class (liftIO) +import Data.Maybe +import qualified Data.Text as T import qualified Data.Vector as Vec import Database.Persist +import qualified Graphics.Vty as V +import Lens.Micro ((&), (.~), (^.), set) +import Lens.Micro.Mtl +import Lens.Micro.TH +import Text.Wrap (FillScope(..), FillStrategy(..), WrapSettings(..), wrapText) import ZcashHaskell.Keys (generateWalletSeedPhrase, getWalletSeed) -import ZcashHaskell.Orchard (encodeUnifiedAddress, genOrchardSpendingKey) import ZcashHaskell.Types import Zenith.Core import Zenith.DB +import Zenith.Types (PhraseDB(..), UnifiedAddressDB(..), ZcashNetDB(..)) import Zenith.Utils (showAddress) data Name @@ -242,8 +241,8 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] Nothing 60) (padAll 1 $ - txtWrap $ - encodeUnifiedAddress $ walletAddressUAddress $ entityVal a) + txtWrapWith (WrapSettings False True NoFill FillAfterFirst) $ + getUA $ walletAddressUAddress $ entityVal a) Nothing -> emptyWidget MsgDisplay -> withBorderStyle unicodeBold $ @@ -344,8 +343,11 @@ appEvent (BT.VtyEvent e) = do V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank V.EvKey V.KEnter [] -> do fs <- BT.zoom inputForm $ BT.gets formState - na <- liftIO $ addNewAccount (fs ^. dialogInput) s - ns <- liftIO $ refreshAccount na + ns <- + liftIO $ + refreshAccount =<< + addNewAddress "Change" Internal =<< + addNewAccount (fs ^. dialogInput) s BT.put ns addrL <- use addresses BT.modify $ set displayBox MsgDisplay @@ -361,7 +363,8 @@ appEvent (BT.VtyEvent e) = do V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank V.EvKey V.KEnter [] -> do fs <- BT.zoom inputForm $ BT.gets formState - nAddr <- liftIO $ addNewAddress (fs ^. dialogInput) s + nAddr <- + liftIO $ addNewAddress (fs ^. dialogInput) External s BT.put nAddr BT.modify $ set displayBox MsgDisplay BT.modify $ set dialogBox Blank @@ -451,7 +454,7 @@ runZenithCLI host port dbFilePath = do Just zebra -> do bc <- checkBlockChain host port case (bc :: Maybe ZebraGetBlockChainInfo) of - Nothing -> print "Unable to determine blockchain status" + Nothing -> throwIO $ userError "Unable to determine blockchain status" Just chainInfo -> do initDb dbFilePath walList <- getWallets dbFilePath $ zgb_net chainInfo @@ -515,7 +518,9 @@ addNewWallet n s = do sP <- generateWalletSeedPhrase let bH = s ^. startBlock let netName = s ^. network - r <- saveWallet (s ^. dbPath) $ ZcashWallet n netName sP bH + r <- + saveWallet (s ^. dbPath) $ + ZcashWallet n (ZcashNetDB netName) (PhraseDB sP) bH case r of Nothing -> do return $ s & msg .~ ("Wallet already exists: " ++ T.unpack n) @@ -573,8 +578,8 @@ refreshAccount s = do s & addresses .~ aL' & msg .~ "Switched to account: " ++ T.unpack (zcashAccountName $ entityVal selAccount) -addNewAddress :: T.Text -> State -> IO State -addNewAddress n s = do +addNewAddress :: T.Text -> Scope -> State -> IO State +addNewAddress n scope s = do selAccount <- do case L.listSelectedElement $ s ^. accounts of Nothing -> do @@ -584,9 +589,9 @@ addNewAddress n s = do Nothing -> throw $ userError "Failed to select account" Just (_j, a1) -> return a1 Just (_k, a) -> return a - maxAddr <- getMaxAddress (s ^. dbPath) (entityKey selAccount) + maxAddr <- getMaxAddress (s ^. dbPath) (entityKey selAccount) scope uA <- - try $ createWalletAddress n (maxAddr + 1) (s ^. network) selAccount :: IO + try $ createWalletAddress n (maxAddr + 1) (s ^. network) scope selAccount :: IO (Either IOError WalletAddress) case uA of Left e -> return $ s & msg .~ ("Error: " ++ show e) diff --git a/src/Zenith/Core.hs b/src/Zenith/Core.hs index da73809..4e1d2c6 100644 --- a/src/Zenith/Core.hs +++ b/src/Zenith/Core.hs @@ -5,15 +5,34 @@ module Zenith.Core where import Control.Exception (throwIO) import Data.Aeson -import qualified Data.ByteString as BS +import Data.HexString (hexString) import qualified Data.Text as T import Database.Persist import Network.HTTP.Client import ZcashHaskell.Keys import ZcashHaskell.Orchard + ( encodeUnifiedAddress + , genOrchardReceiver + , genOrchardSpendingKey + ) +import ZcashHaskell.Sapling + ( genSaplingInternalAddress + , genSaplingPaymentAddress + , genSaplingSpendingKey + ) +import ZcashHaskell.Transparent (genTransparentPrvKey, genTransparentReceiver) import ZcashHaskell.Types import ZcashHaskell.Utils import Zenith.DB +import Zenith.Types + ( OrchardSpendingKeyDB(..) + , PhraseDB(..) + , SaplingSpendingKeyDB(..) + , ScopeDB(..) + , TransparentSpendingKeyDB(..) + , UnifiedAddressDB(..) + , ZcashNetDB(..) + ) -- * Zebra Node interaction -- | Checks the status of the `zebrad` node @@ -45,14 +64,14 @@ connectZebra nodeHost nodePort m params = do -- * Spending Keys -- | Create an Orchard Spending Key for the given wallet and account index -createOrchardSpendingKey :: ZcashWallet -> Int -> IO BS.ByteString +createOrchardSpendingKey :: ZcashWallet -> Int -> IO OrchardSpendingKey createOrchardSpendingKey zw i = do - let s = getWalletSeed $ zcashWalletSeedPhrase zw + let s = getWalletSeed $ getPhrase $ zcashWalletSeedPhrase zw case s of Nothing -> throwIO $ userError "Unable to generate seed" Just s' -> do let coinType = - case zcashWalletNetwork zw of + case getNet $ zcashWalletNetwork zw of MainNet -> MainNetCoin TestNet -> TestNetCoin RegTestNet -> RegTestNetCoin @@ -61,6 +80,36 @@ createOrchardSpendingKey zw i = do Nothing -> throwIO $ userError "Unable to generate Orchard spending key" Just sk -> return sk +-- | Create a Sapling spending key for the given wallet and account index +createSaplingSpendingKey :: ZcashWallet -> Int -> IO SaplingSpendingKey +createSaplingSpendingKey zw i = do + let s = getWalletSeed $ getPhrase $ zcashWalletSeedPhrase zw + case s of + Nothing -> throwIO $ userError "Unable to generate seed" + Just s' -> do + let coinType = + case getNet $ zcashWalletNetwork zw of + MainNet -> MainNetCoin + TestNet -> TestNetCoin + RegTestNet -> RegTestNetCoin + let r = genSaplingSpendingKey s' coinType i + case r of + Nothing -> throwIO $ userError "Unable to generate Sapling spending key" + Just sk -> return sk + +createTransparentSpendingKey :: ZcashWallet -> Int -> IO TransparentSpendingKey +createTransparentSpendingKey zw i = do + let s = getWalletSeed $ getPhrase $ zcashWalletSeedPhrase zw + case s of + Nothing -> throwIO $ userError "Unable to generate seed" + Just s' -> do + let coinType = + case getNet $ zcashWalletNetwork zw of + MainNet -> MainNetCoin + TestNet -> TestNetCoin + RegTestNet -> RegTestNetCoin + genTransparentPrvKey s' coinType i + -- * Accounts -- | Create an account for the given wallet and account index createZcashAccount :: @@ -70,24 +119,46 @@ createZcashAccount :: -> IO ZcashAccount createZcashAccount n i zw = do orSk <- createOrchardSpendingKey (entityVal zw) i - return $ ZcashAccount i (entityKey zw) n orSk "fakeSapKey" "fakeTkey" + sapSk <- createSaplingSpendingKey (entityVal zw) i + tSk <- createTransparentSpendingKey (entityVal zw) i + return $ + ZcashAccount + i + (entityKey zw) + n + (OrchardSpendingKeyDB orSk) + (SaplingSpendingKeyDB sapSk) + (TransparentSpendingKeyDB tSk) -- * Addresses --- | Create a unified address for the given account and index +-- | Create an external unified address for the given account and index createWalletAddress :: T.Text -- ^ The address nickname -> Int -- ^ The address' index -> ZcashNet -- ^ The network for this address + -> Scope -- ^ External or Internal -> Entity ZcashAccount -- ^ The Zcash account that the address will be attached to -> IO WalletAddress -createWalletAddress n i zNet za = do +createWalletAddress n i zNet scope za = do + let oRec = + genOrchardReceiver i scope $ + getOrchSK $ zcashAccountOrchSpendKey $ entityVal za + let sRec = + case scope of + External -> + genSaplingPaymentAddress i $ + getSapSK $ zcashAccountSapSpendKey $ entityVal za + Internal -> + genSaplingInternalAddress $ + getSapSK $ zcashAccountSapSpendKey $ entityVal za + tRec <- + genTransparentReceiver i scope $ + getTranSK $ zcashAccountTPrivateKey $ entityVal za return $ WalletAddress i (entityKey za) n - (UnifiedAddress - zNet - "fakeBString" - "fakeBString" - (Just $ TransparentAddress P2PKH zNet "fakeBString")) + (UnifiedAddressDB $ + encodeUnifiedAddress $ UnifiedAddress zNet oRec sRec (Just tRec)) + (ScopeDB scope) diff --git a/src/Zenith/DB.hs b/src/Zenith/DB.hs index 9ead0e7..8345aef 100644 --- a/src/Zenith/DB.hs +++ b/src/Zenith/DB.hs @@ -23,19 +23,24 @@ import qualified Data.Text as T import Database.Persist import Database.Persist.Sqlite import Database.Persist.TH -import ZcashHaskell.Types (Phrase, UnifiedAddress(..), ZcashNet) - -derivePersistField "ZcashNet" - -derivePersistField "UnifiedAddress" +import ZcashHaskell.Types (Scope(..), ZcashNet) +import Zenith.Types + ( OrchardSpendingKeyDB(..) + , PhraseDB(..) + , SaplingSpendingKeyDB(..) + , ScopeDB(..) + , TransparentSpendingKeyDB + , UnifiedAddressDB(..) + , ZcashNetDB(..) + ) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| ZcashWallet name T.Text - network ZcashNet - seedPhrase Phrase + network ZcashNetDB + seedPhrase PhraseDB birthdayHeight Int UniqueWallet name network deriving Show Eq @@ -43,9 +48,9 @@ share index Int walletId ZcashWalletId name T.Text - orchSpendKey BS.ByteString - sapSpendKey BS.ByteString - tPrivateKey BS.ByteString + orchSpendKey OrchardSpendingKeyDB + sapSpendKey SaplingSpendingKeyDB + tPrivateKey TransparentSpendingKeyDB UniqueAccount index walletId UniqueAccName walletId name deriving Show Eq @@ -53,8 +58,9 @@ share index Int accId ZcashAccountId name T.Text - uAddress UnifiedAddress - UniqueAddress index accId + uAddress UnifiedAddressDB + scope ScopeDB + UniqueAddress index scope accId UniqueAddName accId name deriving Show Eq |] @@ -69,7 +75,8 @@ initDb dbName = do -- | Get existing wallets from database getWallets :: T.Text -> ZcashNet -> IO [Entity ZcashWallet] -getWallets dbFp n = runSqlite dbFp $ selectList [ZcashWalletNetwork ==. n] [] +getWallets dbFp n = + runSqlite dbFp $ selectList [ZcashWalletNetwork ==. ZcashNetDB n] [] -- | Save a new wallet to the database saveWallet :: @@ -110,17 +117,24 @@ getAddresses :: T.Text -- ^ The database path -> ZcashAccountId -- ^ The account ID to check -> IO [Entity WalletAddress] -getAddresses dbFp a = runSqlite dbFp $ selectList [WalletAddressAccId ==. a] [] +getAddresses dbFp a = + runSqlite dbFp $ + selectList + [WalletAddressAccId ==. a, WalletAddressScope ==. ScopeDB External] + [] -- | Returns the largest address index for the given account getMaxAddress :: T.Text -- ^ The database path - -> ZcashAccountId -- ^ The wallet ID to check + -> ZcashAccountId -- ^ The account ID to check + -> Scope -- ^ The scope of the address -> IO Int -getMaxAddress dbFp w = do +getMaxAddress dbFp aw s = do a <- runSqlite dbFp $ - selectFirst [WalletAddressAccId ==. w] [Desc WalletAddressIndex] + selectFirst + [WalletAddressAccId ==. aw, WalletAddressScope ==. ScopeDB s] + [Desc WalletAddressIndex] case a of Nothing -> return $ -1 Just x -> return $ walletAddressIndex $ entityVal x diff --git a/src/Zenith/Types.hs b/src/Zenith/Types.hs index 1ec4408..715e338 100644 --- a/src/Zenith/Types.hs +++ b/src/Zenith/Types.hs @@ -1,7 +1,11 @@ {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralisedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE StandaloneDeriving #-} module Zenith.Types where @@ -14,7 +18,58 @@ import Data.Maybe (fromMaybe) import qualified Data.Text as T import qualified Data.Text.Encoding as E import Data.Text.Encoding.Error (lenientDecode) +import Database.Persist.TH import GHC.Generics +import ZcashHaskell.Types + ( OrchardSpendingKey(..) + , Phrase(..) + , SaplingSpendingKey(..) + , Scope(..) + , TransparentSpendingKey + , ZcashNet(..) + ) + +newtype ZcashNetDB = ZcashNetDB + { getNet :: ZcashNet + } deriving newtype (Eq, Show, Read) + +derivePersistField "ZcashNetDB" + +newtype UnifiedAddressDB = UnifiedAddressDB + { getUA :: T.Text + } deriving newtype (Eq, Show, Read) + +derivePersistField "UnifiedAddressDB" + +newtype PhraseDB = PhraseDB + { getPhrase :: Phrase + } deriving newtype (Eq, Show, Read) + +derivePersistField "PhraseDB" + +newtype ScopeDB = ScopeDB + { getScope :: Scope + } deriving newtype (Eq, Show, Read) + +derivePersistField "ScopeDB" + +newtype OrchardSpendingKeyDB = OrchardSpendingKeyDB + { getOrchSK :: OrchardSpendingKey + } deriving newtype (Eq, Show, Read) + +derivePersistField "OrchardSpendingKeyDB" + +newtype SaplingSpendingKeyDB = SaplingSpendingKeyDB + { getSapSK :: SaplingSpendingKey + } deriving newtype (Eq, Show, Read) + +derivePersistField "SaplingSpendingKeyDB" + +newtype TransparentSpendingKeyDB = TransparentSpendingKeyDB + { getTranSK :: TransparentSpendingKey + } deriving newtype (Eq, Show, Read) + +derivePersistField "TransparentSpendingKeyDB" -- | A type to model Zcash RPC calls data RpcCall = RpcCall diff --git a/src/Zenith/Utils.hs b/src/Zenith/Utils.hs index 86a58ed..ed648a4 100644 --- a/src/Zenith/Utils.hs +++ b/src/Zenith/Utils.hs @@ -9,16 +9,13 @@ import Data.Functor (void) import Data.Maybe import qualified Data.Text as T import qualified Data.Text.Encoding as E -import qualified Data.Text.IO as TIO import System.Process (createProcess_, shell) -import Text.Read (readMaybe) import Text.Regex.Posix import ZcashHaskell.Orchard (encodeUnifiedAddress, isValidUnifiedAddress) import ZcashHaskell.Sapling (isValidShieldedAddress) -import ZcashHaskell.Types (UnifiedAddress(..)) import Zenith.Types ( AddressGroup(..) - , AddressSource(..) + , UnifiedAddressDB(..) , ZcashAddress(..) , ZcashPool(..) ) @@ -32,10 +29,10 @@ displayZec s | otherwise = show (fromIntegral s / 100000000) ++ " ZEC " -- | Helper function to display abbreviated Unified Address -showAddress :: UnifiedAddress -> T.Text +showAddress :: UnifiedAddressDB -> T.Text showAddress u = T.take 20 t <> "..." where - t = encodeUnifiedAddress u + t = getUA u -- | Helper function to extract addresses from AddressGroups getAddresses :: AddressGroup -> [ZcashAddress] diff --git a/test/Spec.hs b/test/Spec.hs index 03a2d20..bfc6f68 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -5,9 +5,17 @@ import Database.Persist import Database.Persist.Sqlite import System.Directory import Test.Hspec -import ZcashHaskell.Types (ZcashNet(..)) -import Zenith.Core (getAccounts) +import ZcashHaskell.Orchard (isValidUnifiedAddress) +import ZcashHaskell.Types + ( OrchardSpendingKey(..) + , Phrase(..) + , SaplingSpendingKey(..) + , Scope(..) + , ZcashNet(..) + ) +import Zenith.Core import Zenith.DB +import Zenith.Types main :: IO () main = do @@ -24,10 +32,12 @@ main = do runSqlite "test.db" $ do insert $ ZcashWallet - "one two three four five six seven eight nine ten eleven twelve" - 2000000 "Main Wallet" - MainNet + (ZcashNetDB MainNet) + (PhraseDB $ + Phrase + "one two three four five six seven eight nine ten eleven twelve") + 2000000 fromSqlKey s `shouldBe` 1 it "read wallet record" $ do s <- @@ -48,21 +58,43 @@ main = do delete recId get recId "None" `shouldBe` maybe "None" zcashWalletName s - describe "Account table" $ do - it "insert account" $ do + describe "Wallet function tests:" $ do + it "Save Wallet:" $ do + zw <- + saveWallet "test.db" $ + ZcashWallet + "Testing" + (ZcashNetDB MainNet) + (PhraseDB $ + Phrase + "cloth swing left trap random tornado have great onion element until make shy dad success art tuition canvas thunder apple decade elegant struggle invest") + 2200000 + zw `shouldNotBe` Nothing + it "Save Account:" $ do s <- runSqlite "test.db" $ do - insert $ - ZcashWallet - "one two three four five six seven eight nine ten eleven twelve" - 2000000 - "Main Wallet" - MainNet - t <- - runSqlite "test.db" $ do - insert $ ZcashAccount s 0 "132465798" "987654321" "739182462" - fromSqlKey t `shouldBe` 1 - it "read accounts for wallet" $ do - wList <- getWallets "test.db" MainNet - acc <- getAccounts "test.db" $ entityKey (head wList) - length acc `shouldBe` 1 + selectList [ZcashWalletName ==. "Testing"] [] + za <- + saveAccount "test.db" =<< + createZcashAccount "TestAccount" 0 (head s) + za `shouldNotBe` Nothing + it "Save address:" $ do + acList <- + runSqlite "test.db" $ + selectList [ZcashAccountName ==. "TestAccount"] [] + zAdd <- + saveAddress "test.db" =<< + createWalletAddress "Personal123" 0 MainNet External (head acList) + addList <- + runSqlite "test.db" $ + selectList + [ WalletAddressName ==. "Personal123" + , WalletAddressScope ==. ScopeDB External + ] + [] + getUA (walletAddressUAddress (entityVal $ head addList)) `shouldBe` + "u1trd8cvc6265ywwj4mmvuznsye5ghe2dhhn3zy8kcuyg4vx3svskw9r2dedp5hu6m740vylkqc34t4w9eqkl9fyu5uyzn3af72jg235440ke6tu5cf994eq85n97x69x9824hqejmwz3d8qqthtesrd6gerjupdymldhl9xccejjwfj0dhh9mt4rw4kytp325twlutsxd20rfqhzxu3m" + it "Address components are correct" $ do + let ua = + "utest1mvlny48qd4x94w8vz5u2lrxx0enuquajt72yekgq24p6pjaky3czk6m7x358h7g900ex6gzvdehaekl96qnakjzw8yaasp8y0u3j5jnlfd33trduznh6k3fcn5ek9qc857fgz8ehm37etx94sj58nrkc0k5hurxnuxpcpms3j8uy2t8kt2vy6vetvsfxxdhtjq0yqulqprvh7mf2u3x" + isValidUnifiedAddress ua `shouldNotBe` Nothing diff --git a/zcash-haskell b/zcash-haskell index 4963eea..f228eff 160000 --- a/zcash-haskell +++ b/zcash-haskell @@ -1 +1 @@ -Subproject commit 4963eea68bd1e3b38cbc14a64888d3f5aaef3f85 +Subproject commit f228eff367c776469455adc4d443102cc53e5538 diff --git a/zenith.cabal b/zenith.cabal index b14f4ea..cb291be 100644 --- a/zenith.cabal +++ b/zenith.cabal @@ -1,10 +1,10 @@ cabal-version: 3.0 name: zenith -version: 0.4.3.0 +version: 0.4.4.0 license: MIT license-file: LICENSE author: Rene Vergara -maintainer: pitmut@vergara.tech +maintainer: pitmutt@vergara.tech copyright: (c) 2022-2024 Vergara Technologies LLC build-type: Custom category: Blockchain @@ -13,8 +13,6 @@ extra-doc-files: CHANGELOG.md zenith.cfg -common warnings - ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -Wunused-imports custom-setup setup-depends: @@ -26,7 +24,6 @@ custom-setup , regex-compat library - import: warnings ghc-options: -Wall -Wunused-imports exposed-modules: Zenith.CLI @@ -56,6 +53,7 @@ library , persistent-sqlite , persistent-template , process + , hexstring , regex-base , regex-compat , regex-posix @@ -63,12 +61,13 @@ library , text , vector , vty + , word-wrap , zcash-haskell --pkgconfig-depends: rustzcash_wrapper default-language: Haskell2010 executable zenith - import: warnings + ghc-options: -threaded -rtsopts -with-rtsopts=-N main-is: Main.hs hs-source-dirs: app @@ -88,8 +87,8 @@ executable zenith default-language: Haskell2010 test-suite zenith-tests - import: warnings type: exitcode-stdio-1.0 + ghc-options: -threaded -rtsopts -with-rtsopts=-N main-is: Spec.hs hs-source-dirs: test From 466491a7d0f84633ab85640591ad336f5f0bab36 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Sun, 17 Mar 2024 14:38:26 -0500 Subject: [PATCH 7/8] Add command guides to screens --- src/Zenith/CLI.hs | 63 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/src/Zenith/CLI.hs b/src/Zenith/CLI.hs index 5ce1a69..8855d4e 100644 --- a/src/Zenith/CLI.hs +++ b/src/Zenith/CLI.hs @@ -28,6 +28,7 @@ import Brick.Widgets.Core , (<=>) , emptyWidget , fill + , hBox , hLimit , joinBorders , padAll @@ -35,6 +36,7 @@ import Brick.Widgets.Core , str , strWrap , txt + , txtWrap , txtWrapWith , vBox , vLimit @@ -48,6 +50,7 @@ import Control.Monad (void) import Control.Monad.IO.Class (liftIO) import Data.Maybe import qualified Data.Text as T +import qualified Data.Text.Encoding as E import qualified Data.Vector as Vec import Database.Persist import qualified Graphics.Vty as V @@ -88,6 +91,7 @@ data DialogType data DisplayType = AddrDisplay | MsgDisplay + | PhraseDisplay | BlankDisplay data State = State @@ -135,7 +139,14 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] (\(_, a) -> zcashAccountName $ entityVal a) (L.listSelectedElement (st ^. accounts))))) <=> listAddressBox "Addresses" (st ^. addresses) <+> - B.vBorder <+> C.center (listBox "Transactions" (st ^. transactions))) + B.vBorder <+> C.center (listBox "Transactions" (st ^. transactions))) <=> + C.hCenter + (hBox + [ capCommand "W" "allets" + , capCommand "A" "ccounts" + , capCommand "V" "iew address" + , capCommand "Q" "uit" + ]) listBox :: Show e => String -> L.List Name e -> Widget Name listBox titleLabel l = C.vCenter $ @@ -158,7 +169,6 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] (B.borderWithLabel (str titleLabel) $ hLimit 25 $ vLimit 15 $ L.renderList drawF True l) , str " " - , C.hCenter $ str "Select " ] listAddressBox :: String -> L.List Name (Entity WalletAddress) -> Widget Name @@ -209,11 +219,26 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] WSelect -> D.renderDialog (D.dialog (Just (str "Select Wallet")) Nothing 50) - (selectListBox "Wallets" (st ^. wallets) listDrawWallet) + (selectListBox "Wallets" (st ^. wallets) listDrawWallet <=> + C.hCenter + (hBox + [ capCommand "↑↓ " "move" + , capCommand "↲ " "select" + , capCommand "N" "ew" + , capCommand "S" "how phrase" + , xCommand + ])) ASelect -> D.renderDialog (D.dialog (Just (str "Select Account")) Nothing 50) - (selectListBox "Accounts" (st ^. accounts) listDrawAccount) + (selectListBox "Accounts" (st ^. accounts) listDrawAccount <=> + C.hCenter + (hBox + [ capCommand "↑↓ " "move" + , capCommand "↲ " "select" + , capCommand "N" "ew" + , xCommand + ])) Blank -> emptyWidget splashDialog :: State -> Widget Name splashDialog st = @@ -225,9 +250,13 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] titleAttr (str " _____ _ _ _ \n|__ /___ _ __ (_) |_| |__\n / // _ \\ '_ \\| | __| '_ \\\n / /| __/ | | | | |_| | | |\n/____\\___|_| |_|_|\\__|_| |_|") <=> - C.hCenter (withAttr titleAttr (str "Zcash Wallet v0.4.3.0")) <=> + C.hCenter (withAttr titleAttr (str "Zcash Wallet v0.4.4.0")) <=> C.hCenter (withAttr blinkAttr $ str "Press any key...")) else emptyWidget + capCommand :: String -> String -> Widget Name + capCommand k comm = hBox [withAttr titleAttr (str k), str comm, str " | "] + xCommand :: Widget Name + xCommand = hBox [str "E", withAttr titleAttr (str "x"), str "it"] displayDialog :: State -> Widget Name displayDialog st = case st ^. displayBox of @@ -244,6 +273,17 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] txtWrapWith (WrapSettings False True NoFill FillAfterFirst) $ getUA $ walletAddressUAddress $ entityVal a) Nothing -> emptyWidget + PhraseDisplay -> + case L.listSelectedElement $ st ^. wallets of + Just (_, w) -> + withBorderStyle unicodeBold $ + D.renderDialog + (D.dialog (Just $ txt "Seed Phrase") Nothing 50) + (padAll 1 $ + txtWrap $ + E.decodeUtf8Lenient $ + getBytes $ getPhrase $ zcashWalletSeedPhrase $ entityVal w) + Nothing -> emptyWidget MsgDisplay -> withBorderStyle unicodeBold $ D.renderDialog @@ -319,6 +359,7 @@ appEvent (BT.VtyEvent e) = do case s ^. displayBox of AddrDisplay -> BT.modify $ set displayBox BlankDisplay MsgDisplay -> BT.modify $ set displayBox BlankDisplay + PhraseDisplay -> BT.modify $ set displayBox BlankDisplay BlankDisplay -> do case s ^. dialogBox of WName -> do @@ -372,26 +413,30 @@ appEvent (BT.VtyEvent e) = do BT.zoom inputForm $ handleFormEvent (BT.VtyEvent ev) WSelect -> do case e of - V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank + V.EvKey (V.KChar 'x') [] -> + BT.modify $ set dialogBox Blank V.EvKey V.KEnter [] -> do ns <- liftIO $ refreshWallet s BT.put ns BT.modify $ set dialogBox Blank - V.EvKey (V.KChar 'c') [] -> do + V.EvKey (V.KChar 'n') [] -> do BT.modify $ set inputForm $ updateFormState (DialogInput "New Wallet") $ s ^. inputForm BT.modify $ set dialogBox WName + V.EvKey (V.KChar 's') [] -> + BT.modify $ set displayBox PhraseDisplay ev -> BT.zoom wallets $ L.handleListEvent ev ASelect -> do case e of - V.EvKey V.KEsc [] -> BT.modify $ set dialogBox Blank + V.EvKey (V.KChar 'x') [] -> + BT.modify $ set dialogBox Blank V.EvKey V.KEnter [] -> do ns <- liftIO $ refreshAccount s BT.put ns BT.modify $ set dialogBox Blank - V.EvKey (V.KChar 'c') [] -> do + V.EvKey (V.KChar 'n') [] -> do BT.modify $ set inputForm $ updateFormState (DialogInput "New Account") $ From 246fa05d11a10aabb0fc2f4c08b1bbf8460abe2f Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Sun, 17 Mar 2024 14:40:49 -0500 Subject: [PATCH 8/8] Bump version --- zenith.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zenith.cabal b/zenith.cabal index cb291be..081df74 100644 --- a/zenith.cabal +++ b/zenith.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: zenith -version: 0.4.4.0 +version: 0.4.4.1 license: MIT license-file: LICENSE author: Rene Vergara