diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b90b6e..597328a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.6.0] + +### Added + +- Display of account balance +- Functions to identify spends +- Functions to display transactions per address + +### Changed + +- Update `zcash-haskell` + +## [0.4.5.0] + +### Added + +- Functions to scan relevant transparent notes +- Functions to scan relevant Sapling notes +- Functions to scan relevant Orchard notes +- Function to query `zebrad` for commitment trees + +### Changed + +- Update `zcash-haskell` + ## [0.4.4.3] ### Added diff --git a/app/Main.hs b/app/Main.hs index d3c271b..eb13ce7 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -16,8 +16,10 @@ import System.Environment (getArgs) import System.Exit import System.IO import Text.Read (readMaybe) +import ZcashHaskell.Types import Zenith.CLI -import Zenith.Types (ZcashAddress(..), ZcashPool(..), ZcashTx(..)) +import Zenith.Core (clearSync, testSync) +import Zenith.Types (Config(..), ZcashAddress(..), ZcashPool(..), ZcashTx(..)) import Zenith.Utils import Zenith.Zcashd @@ -204,6 +206,7 @@ main = do nodePwd <- require config "nodePwd" zebraPort <- require config "zebraPort" zebraHost <- require config "zebraHost" + let myConfig = Config dbFilePath zebraHost zebraPort if not (null args) then do case head args of @@ -217,7 +220,9 @@ main = do " ______ _ _ _ \n |___ / (_) | | | \n / / ___ _ __ _| |_| |__ \n / / / _ \\ '_ \\| | __| '_ \\ \n / /_| __/ | | | | |_| | | |\n /_____\\___|_| |_|_|\\__|_| |_|\n Zcash Full Node CLI v0.4.0" } (root nodeUser nodePwd) - "cli" -> runZenithCLI zebraHost zebraPort dbFilePath + "cli" -> runZenithCLI myConfig + "sync" -> testSync myConfig + "rescan" -> clearSync myConfig _ -> printUsage else printUsage diff --git a/src/Zenith/CLI.hs b/src/Zenith/CLI.hs index c25eb69..7b18b9a 100644 --- a/src/Zenith/CLI.hs +++ b/src/Zenith/CLI.hs @@ -37,6 +37,7 @@ import Brick.Widgets.Core , padBottom , str , strWrap + , strWrapWith , txt , txtWrap , txtWrapWith @@ -53,6 +54,7 @@ import Control.Monad.IO.Class (liftIO) import Data.Maybe import qualified Data.Text as T import qualified Data.Text.Encoding as E +import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import qualified Data.Vector as Vec import Database.Persist import qualified Graphics.Vty as V @@ -61,13 +63,18 @@ import Lens.Micro.Mtl import Lens.Micro.TH import Text.Wrap (FillScope(..), FillStrategy(..), WrapSettings(..), wrapText) import ZcashHaskell.Keys (generateWalletSeedPhrase, getWalletSeed) -import ZcashHaskell.Orchard (isValidUnifiedAddress) -import ZcashHaskell.Transparent (encodeTransparent) +import ZcashHaskell.Orchard (getSaplingFromUA, isValidUnifiedAddress) +import ZcashHaskell.Transparent (encodeTransparentReceiver) import ZcashHaskell.Types import Zenith.Core import Zenith.DB -import Zenith.Types (PhraseDB(..), UnifiedAddressDB(..), ZcashNetDB(..)) -import Zenith.Utils (showAddress) +import Zenith.Types + ( Config(..) + , PhraseDB(..) + , UnifiedAddressDB(..) + , ZcashNetDB(..) + ) +import Zenith.Utils (displayTaz, displayZec, showAddress) data Name = WList @@ -96,6 +103,7 @@ data DisplayType = AddrDisplay | MsgDisplay | PhraseDisplay + | TxDisplay | BlankDisplay data State = State @@ -103,7 +111,7 @@ data State = State , _wallets :: !(L.List Name (Entity ZcashWallet)) , _accounts :: !(L.List Name (Entity ZcashAccount)) , _addresses :: !(L.List Name (Entity WalletAddress)) - , _transactions :: !(L.List Name String) + , _transactions :: !(L.List Name (Entity UserTx)) , _msg :: !String , _helpBox :: !Bool , _dialogBox :: !DialogType @@ -113,6 +121,8 @@ data State = State , _startBlock :: !Int , _dbPath :: !T.Text , _displayBox :: !DisplayType + , _syncBlock :: !Int + , _balance :: !Integer } makeLenses ''State @@ -142,8 +152,16 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] "(None)" (\(_, a) -> zcashAccountName $ entityVal a) (L.listSelectedElement (st ^. accounts))))) <=> + C.hCenter + (str + ("Balance: " ++ + if st ^. network == MainNet + then displayZec (st ^. balance) + else displayTaz (st ^. balance))) <=> listAddressBox "Addresses" (st ^. addresses) <+> - B.vBorder <+> C.center (listBox "Transactions" (st ^. transactions))) <=> + B.vBorder <+> + (C.hCenter (str ("Last block seen: " ++ show (st ^. syncBlock))) <=> + listTxBox "Transactions" (st ^. transactions))) <=> C.hCenter (hBox [ capCommand "W" "allets" @@ -185,6 +203,16 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] , str " " , C.hCenter $ str "Use arrows to select" ] + listTxBox :: String -> L.List Name (Entity UserTx) -> Widget Name + listTxBox titleLabel tx = + C.vCenter $ + vBox + [ C.hCenter + (B.borderWithLabel (str titleLabel) $ + hLimit 40 $ vLimit 15 $ L.renderList listDrawTx True tx) + , str " " + , C.hCenter $ str "Use arrows to select" + ] helpDialog :: State -> Widget Name helpDialog st = if st ^. helpBox @@ -254,7 +282,7 @@ 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.4.0")) <=> + C.hCenter (withAttr titleAttr (str "Zcash Wallet v0.4.6.0")) <=> C.hCenter (withAttr blinkAttr $ str "Press any key...")) else emptyWidget capCommand :: String -> String -> Widget Name @@ -280,13 +308,15 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] getUA $ walletAddressUAddress $ entityVal a) <=> B.borderWithLabel (str "Legacy Shielded") - (txtWrapWith - (WrapSettings False True NoFill FillAfterFirst) - "Pending") <=> + (txtWrapWith (WrapSettings False True NoFill FillAfterFirst) $ + fromMaybe "None" $ + (getSaplingFromUA . + E.encodeUtf8 . getUA . walletAddressUAddress) + (entityVal a)) <=> B.borderWithLabel (str "Transparent") (txtWrapWith (WrapSettings False True NoFill FillAfterFirst) $ - maybe "Pending" (encodeTransparent (st ^. network)) $ + maybe "None" (encodeTransparentReceiver (st ^. network)) $ t_rec =<< (isValidUnifiedAddress . E.encodeUtf8 . getUA . walletAddressUAddress) @@ -308,6 +338,35 @@ drawUI s = [splashDialog s, helpDialog s, displayDialog s, inputDialog s, ui s] D.renderDialog (D.dialog (Just $ txt "Message") Nothing 50) (padAll 1 $ strWrap $ st ^. msg) + TxDisplay -> + case L.listSelectedElement $ st ^. transactions of + Nothing -> emptyWidget + Just (_, tx) -> + withBorderStyle unicodeBold $ + D.renderDialog + (D.dialog (Just $ txt "Transaction") Nothing 50) + (padAll + 1 + (str + ("Date: " ++ + show + (posixSecondsToUTCTime + (fromIntegral (userTxTime $ entityVal tx)))) <=> + (str "Tx ID: " <+> + strWrapWith + (WrapSettings False True NoFill FillAfterFirst) + (show (userTxHex $ entityVal tx))) <=> + str + ("Amount: " ++ + if st ^. network == MainNet + then displayZec + (fromIntegral $ userTxAmount $ entityVal tx) + else displayTaz + (fromIntegral $ userTxAmount $ entityVal tx)) <=> + (txt "Memo: " <+> + txtWrapWith + (WrapSettings False True NoFill FillAfterFirst) + (userTxMemo (entityVal tx))))) BlankDisplay -> emptyWidget mkInputForm :: DialogInput -> Form DialogInput e Name @@ -353,6 +412,23 @@ listDrawAddress sel w = walletAddressName (entityVal w) <> ": " <> showAddress (walletAddressUAddress (entityVal w)) +listDrawTx :: Bool -> Entity UserTx -> Widget Name +listDrawTx sel tx = + selStr $ + T.pack + (show $ posixSecondsToUTCTime (fromIntegral (userTxTime $ entityVal tx))) <> + " " <> fmtAmt + where + amt = fromIntegral (userTxAmount $ entityVal tx) / 100000000 + fmtAmt = + if amt > 0 + then "↘" <> T.pack (show amt) <> " " + else " " <> T.pack (show amt) <> "↗" + selStr s = + if sel + then withAttr customAttr (txt $ "> " <> s) + else txt $ " " <> s + customAttr :: A.AttrName customAttr = L.listSelectedAttr <> A.attrName "custom" @@ -379,6 +455,7 @@ appEvent (BT.VtyEvent e) = do AddrDisplay -> BT.modify $ set displayBox BlankDisplay MsgDisplay -> BT.modify $ set displayBox BlankDisplay PhraseDisplay -> BT.modify $ set displayBox BlankDisplay + TxDisplay -> BT.modify $ set displayBox BlankDisplay BlankDisplay -> do case s ^. dialogBox of WName -> do @@ -465,6 +542,9 @@ appEvent (BT.VtyEvent e) = do Blank -> do case e of V.EvKey (V.KChar '\t') [] -> focusRing %= F.focusNext + V.EvKey V.KEnter [] -> do + ns <- liftIO $ refreshTxs s + BT.put ns V.EvKey (V.KChar 'q') [] -> M.halt V.EvKey (V.KChar '?') [] -> BT.modify $ set helpBox True V.EvKey (V.KChar 'n') [] -> @@ -473,6 +553,8 @@ appEvent (BT.VtyEvent e) = do BT.modify $ set displayBox AddrDisplay V.EvKey (V.KChar 'w') [] -> BT.modify $ set dialogBox WSelect + V.EvKey (V.KChar 't') [] -> + BT.modify $ set displayBox TxDisplay V.EvKey (V.KChar 'a') [] -> BT.modify $ set dialogBox ASelect ev -> @@ -511,8 +593,11 @@ theApp = , M.appAttrMap = const theMap } -runZenithCLI :: T.Text -> Int -> T.Text -> IO () -runZenithCLI host port dbFilePath = do +runZenithCLI :: Config -> IO () +runZenithCLI config = do + let host = c_zebraHost config + let port = c_zebraPort config + let dbFilePath = c_dbPath config w <- try $ checkZebra host port :: IO (Either IOError ZebraGetInfo) case w of Right zebra -> do @@ -532,6 +617,18 @@ runZenithCLI host port dbFilePath = do if not (null accList) then getAddresses dbFilePath $ entityKey $ head accList else return [] + txList <- + if not (null addrList) + then getUserTx dbFilePath $ entityKey $ head addrList + else return [] + let block = + if not (null walList) + then zcashWalletLastSync $ entityVal $ head walList + else 0 + bal <- + if not (null accList) + then getBalance dbFilePath $ entityKey $ head accList + else return 0 void $ M.defaultMain theApp $ State @@ -539,7 +636,7 @@ runZenithCLI host port dbFilePath = do (L.list WList (Vec.fromList walList) 1) (L.list AcList (Vec.fromList accList) 0) (L.list AList (Vec.fromList addrList) 1) - (L.list TList (Vec.fromList ["tx1", "tx2", "tx3"]) 1) + (L.list TList (Vec.fromList txList) 1) ("Start up Ok! Connected to Zebra " ++ (T.unpack . zgi_build) zebra ++ " on port " ++ show port ++ ".") False @@ -552,6 +649,8 @@ runZenithCLI host port dbFilePath = do (zgb_blocks chainInfo) dbFilePath MsgDisplay + block + bal Left e -> do print $ "No Zebra node available on port " <> @@ -569,14 +668,29 @@ refreshWallet s = do Just (_j, w1) -> return w1 Just (_k, w) -> return w aL <- getAccounts (s ^. dbPath) $ entityKey selWallet + let bl = zcashWalletLastSync $ entityVal selWallet addrL <- if not (null aL) then getAddresses (s ^. dbPath) $ entityKey $ head aL else return [] + bal <- + if not (null aL) + then getBalance (s ^. dbPath) $ entityKey $ head aL + else return 0 + txL <- + if not (null addrL) + then getUserTx (s ^. dbPath) $ entityKey $ head addrL + else return [] let aL' = L.listReplace (Vec.fromList aL) (Just 0) (s ^. accounts) let addrL' = L.listReplace (Vec.fromList addrL) (Just 0) (s ^. addresses) + let txL' = L.listReplace (Vec.fromList txL) (Just 0) (s ^. transactions) return $ - (s & accounts .~ aL') & addresses .~ addrL' & msg .~ "Switched to wallet: " ++ + (s & accounts .~ aL') & syncBlock .~ bl & balance .~ bal & addresses .~ + addrL' & + transactions .~ + txL' & + msg .~ + "Switched to wallet: " ++ T.unpack (zcashWalletName $ entityVal selWallet) addNewWallet :: T.Text -> State -> IO State @@ -586,7 +700,7 @@ addNewWallet n s = do let netName = s ^. network r <- saveWallet (s ^. dbPath) $ - ZcashWallet n (ZcashNetDB netName) (PhraseDB sP) bH + ZcashWallet n (ZcashNetDB netName) (PhraseDB sP) bH 0 case r of Nothing -> do return $ s & msg .~ ("Wallet already exists: " ++ T.unpack n) @@ -639,10 +753,42 @@ refreshAccount s = do Just (_j, w1) -> return w1 Just (_k, w) -> return w aL <- getAddresses (s ^. dbPath) $ entityKey selAccount + bal <- getBalance (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) + selAddress <- + do case L.listSelectedElement aL' of + Nothing -> do + let fAdd = L.listSelectedElement $ L.listMoveToBeginning aL' + return fAdd + Just a2 -> return $ Just a2 + case selAddress of + Nothing -> + return $ + s & balance .~ bal & addresses .~ aL' & msg .~ "Switched to account: " ++ + T.unpack (zcashAccountName $ entityVal selAccount) + Just (_i, a) -> do + tList <- getUserTx (s ^. dbPath) $ entityKey a + let tL' = L.listReplace (Vec.fromList tList) (Just 0) (s ^. transactions) + return $ + s & balance .~ bal & addresses .~ aL' & transactions .~ tL' & msg .~ + "Switched to account: " ++ + T.unpack (zcashAccountName $ entityVal selAccount) + +refreshTxs :: State -> IO State +refreshTxs s = do + selAddress <- + do case L.listSelectedElement $ s ^. addresses of + Nothing -> do + let fAdd = + L.listSelectedElement $ L.listMoveToBeginning $ s ^. addresses + return fAdd + Just a2 -> return $ Just a2 + case selAddress of + Nothing -> return s + Just (_i, a) -> do + tList <- getUserTx (s ^. dbPath) $ entityKey a + let tL' = L.listReplace (Vec.fromList tList) (Just 0) (s ^. transactions) + return $ s & transactions .~ tL' addNewAddress :: T.Text -> Scope -> State -> IO State addNewAddress n scope s = do diff --git a/src/Zenith/Core.hs b/src/Zenith/Core.hs index 3d9fac6..3d3ecbe 100644 --- a/src/Zenith/Core.hs +++ b/src/Zenith/Core.hs @@ -3,35 +3,49 @@ -- | Core wallet functionality for Zenith module Zenith.Core where -import Control.Exception (throwIO) +import Control.Exception (throwIO, try) import Data.Aeson import Data.HexString (hexString) +import Data.Maybe (fromJust) import qualified Data.Text as T +import qualified Data.Text.Encoding as E import Database.Persist +import Database.Persist.Sqlite import Network.HTTP.Client import ZcashHaskell.Keys import ZcashHaskell.Orchard - ( encodeUnifiedAddress + ( decryptOrchardActionSK + , encodeUnifiedAddress , genOrchardReceiver , genOrchardSpendingKey + , getOrchardNotePosition + , getOrchardWitness + , updateOrchardCommitmentTree ) import ZcashHaskell.Sapling - ( genSaplingInternalAddress + ( decodeSaplingOutputEsk + , genSaplingInternalAddress , genSaplingPaymentAddress , genSaplingSpendingKey + , getSaplingNotePosition + , getSaplingWitness + , updateSaplingCommitmentTree ) import ZcashHaskell.Transparent (genTransparentPrvKey, genTransparentReceiver) import ZcashHaskell.Types import ZcashHaskell.Utils import Zenith.DB import Zenith.Types - ( OrchardSpendingKeyDB(..) + ( Config(..) + , HexStringDB(..) + , OrchardSpendingKeyDB(..) , PhraseDB(..) , SaplingSpendingKeyDB(..) , ScopeDB(..) , TransparentSpendingKeyDB(..) , UnifiedAddressDB(..) , ZcashNetDB(..) + , ZebraTreeInfo(..) ) -- * Zebra Node interaction @@ -57,6 +71,23 @@ checkBlockChain nodeHost nodePort = do Left e -> throwIO $ userError e Right bci -> return bci +-- | Get commitment trees from Zebra +getCommitmentTrees :: + T.Text -- ^ Host where `zebrad` is avaiable + -> Int -- ^ Port where `zebrad` is available + -> Int -- ^ Block height + -> IO ZebraTreeInfo +getCommitmentTrees nodeHost nodePort block = do + r <- + makeZebraCall + nodeHost + nodePort + "z_gettreestate" + [Data.Aeson.String $ T.pack $ show block] + case r of + Left e -> throwIO $ userError e + Right zti -> return zti + -- * Spending Keys -- | Create an Orchard Spending Key for the given wallet and account index createOrchardSpendingKey :: ZcashWallet -> Int -> IO OrchardSpendingKey @@ -159,9 +190,233 @@ createWalletAddress n i zNet scope za = do (ScopeDB scope) -- * Wallet +-- | Find the Sapling notes that match the given spending key +findSaplingOutputs :: + Config -- ^ the configuration parameters + -> Int -- ^ the starting block + -> ZcashNetDB -- ^ The network + -> Entity ZcashAccount -- ^ The account to use + -> IO () +findSaplingOutputs config b znet za = do + let dbPath = c_dbPath config + let zebraHost = c_zebraHost config + let zebraPort = c_zebraPort config + let zn = getNet znet + tList <- getShieldedOutputs dbPath b + trees <- getCommitmentTrees zebraHost zebraPort (b - 1) + let sT = SaplingCommitmentTree $ ztiSapling trees + decryptNotes sT zn tList + sapNotes <- getWalletSapNotes dbPath (entityKey za) + findSapSpends dbPath (entityKey za) sapNotes + where + sk :: SaplingSpendingKeyDB + sk = zcashAccountSapSpendKey $ entityVal za + decryptNotes :: + SaplingCommitmentTree + -> ZcashNet + -> [(Entity ZcashTransaction, Entity ShieldOutput)] + -> IO () + decryptNotes _ _ [] = return () + decryptNotes st n ((zt, o):txs) = do + let updatedTree = + updateSaplingCommitmentTree + st + (getHex $ shieldOutputCmu $ entityVal o) + case updatedTree of + Nothing -> throwIO $ userError "Failed to update commitment tree" + Just uT -> do + let noteWitness = getSaplingWitness uT + let notePos = getSaplingNotePosition <$> noteWitness + case notePos of + Nothing -> throwIO $ userError "Failed to obtain note position" + Just nP -> do + case decodeShOut External n nP o of + Nothing -> do + case decodeShOut Internal n nP o of + Nothing -> do + decryptNotes uT n txs + Just dn1 -> do + print dn1 + wId <- + saveWalletTransaction + (c_dbPath config) + (entityKey za) + zt + saveWalletSapNote + (c_dbPath config) + wId + nP + (fromJust noteWitness) + True + (entityKey za) + dn1 + decryptNotes uT n txs + Just dn0 -> do + print dn0 + wId <- + saveWalletTransaction (c_dbPath config) (entityKey za) zt + saveWalletSapNote + (c_dbPath config) + wId + nP + (fromJust noteWitness) + False + (entityKey za) + dn0 + decryptNotes uT n txs + decodeShOut :: + Scope + -> ZcashNet + -> Integer + -> Entity ShieldOutput + -> Maybe DecodedNote + decodeShOut scope n pos s = do + decodeSaplingOutputEsk + (getSapSK sk) + (ShieldedOutput + (getHex $ shieldOutputCv $ entityVal s) + (getHex $ shieldOutputCmu $ entityVal s) + (getHex $ shieldOutputEphKey $ entityVal s) + (getHex $ shieldOutputEncCipher $ entityVal s) + (getHex $ shieldOutputOutCipher $ entityVal s) + (getHex $ shieldOutputProof $ entityVal s)) + n + scope + pos + +-- | Get Orchard actions +findOrchardActions :: + Config -- ^ the configuration parameters + -> Int -- ^ the starting block + -> ZcashNetDB -- ^ The network + -> Entity ZcashAccount -- ^ The account to use + -> IO () +findOrchardActions config b znet za = do + let dbPath = c_dbPath config + let zebraHost = c_zebraHost config + let zebraPort = c_zebraPort config + let zn = getNet znet + tList <- getOrchardActions dbPath b + trees <- getCommitmentTrees zebraHost zebraPort (b - 1) + let sT = OrchardCommitmentTree $ ztiOrchard trees + decryptNotes sT zn tList + orchNotes <- getWalletOrchNotes dbPath (entityKey za) + findOrchSpends dbPath (entityKey za) orchNotes + where + decryptNotes :: + OrchardCommitmentTree + -> ZcashNet + -> [(Entity ZcashTransaction, Entity OrchAction)] + -> IO () + decryptNotes _ _ [] = return () + decryptNotes ot n ((zt, o):txs) = do + let updatedTree = + updateOrchardCommitmentTree + ot + (getHex $ orchActionCmx $ entityVal o) + case updatedTree of + Nothing -> throwIO $ userError "Failed to update commitment tree" + Just uT -> do + let noteWitness = getOrchardWitness uT + let notePos = getOrchardNotePosition <$> noteWitness + case notePos of + Nothing -> throwIO $ userError "Failed to obtain note position" + Just nP -> + case decodeOrchAction External nP o of + Nothing -> + case decodeOrchAction Internal nP o of + Nothing -> decryptNotes uT n txs + Just dn1 -> do + print dn1 + wId <- + saveWalletTransaction + (c_dbPath config) + (entityKey za) + zt + saveWalletOrchNote + (c_dbPath config) + wId + nP + (fromJust noteWitness) + True + (entityKey za) + dn1 + decryptNotes uT n txs + Just dn -> do + print dn + wId <- + saveWalletTransaction (c_dbPath config) (entityKey za) zt + saveWalletOrchNote + (c_dbPath config) + wId + nP + (fromJust noteWitness) + False + (entityKey za) + dn + decryptNotes uT n txs + sk :: OrchardSpendingKeyDB + sk = zcashAccountOrchSpendKey $ entityVal za + decodeOrchAction :: + Scope -> Integer -> Entity OrchAction -> Maybe DecodedNote + decodeOrchAction scope pos o = + decryptOrchardActionSK (getOrchSK sk) scope $ + OrchardAction + (getHex $ orchActionNf $ entityVal o) + (getHex $ orchActionRk $ entityVal o) + (getHex $ orchActionCmx $ entityVal o) + (getHex $ orchActionEphKey $ entityVal o) + (getHex $ orchActionEncCipher $ entityVal o) + (getHex $ orchActionOutCipher $ entityVal o) + (getHex $ orchActionCv $ entityVal o) + (getHex $ orchActionAuth $ entityVal o) + -- | Sync the wallet with the data store syncWallet :: - T.Text -- ^ The database path + Config -- ^ configuration parameters -> Entity ZcashWallet - -> IO () -syncWallet walletDb w = undefined + -> IO String +syncWallet config w = do + let walletDb = c_dbPath config + accs <- getAccounts walletDb $ entityKey w + addrs <- concat <$> mapM (getAddresses walletDb . entityKey) accs + intAddrs <- concat <$> mapM (getInternalAddresses walletDb . entityKey) accs + chainTip <- getMaxBlock walletDb + let lastBlock = zcashWalletLastSync $ entityVal w + let startBlock = + if lastBlock > 0 + then lastBlock + else zcashWalletBirthdayHeight $ entityVal w + mapM_ (findTransparentNotes walletDb startBlock) addrs + mapM_ (findTransparentNotes walletDb startBlock) intAddrs + mapM_ (findTransparentSpends walletDb . entityKey) accs + sapNotes <- + mapM + (findSaplingOutputs config startBlock (zcashWalletNetwork $ entityVal w)) + accs + orchNotes <- + mapM + (findOrchardActions config startBlock (zcashWalletNetwork $ entityVal w)) + accs + updateWalletSync walletDb chainTip (entityKey w) + mapM_ (getWalletTransactions walletDb) addrs + return "Testing" + +testSync :: Config -> IO () +testSync config = do + let dbPath = c_dbPath config + _ <- initDb dbPath + w <- getWallets dbPath TestNet + r <- mapM (syncWallet config) w + print r + +clearSync :: Config -> IO () +clearSync config = do + let dbPath = c_dbPath config + _ <- initDb dbPath + _ <- clearWalletTransactions dbPath + w <- getWallets dbPath TestNet + mapM_ (updateWalletSync dbPath 0 . entityKey) w + w' <- getWallets dbPath TestNet + r <- mapM (syncWallet config) w' + print r diff --git a/src/Zenith/DB.hs b/src/Zenith/DB.hs index 496a353..a23ae67 100644 --- a/src/Zenith/DB.hs +++ b/src/Zenith/DB.hs @@ -14,32 +14,57 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-} +{-# LANGUAGE TypeApplications #-} module Zenith.DB where -import Control.Monad (when) -import Control.Monad.IO.Class (liftIO) +import Control.Exception (throwIO) +import Control.Monad (forM_, when) +import Control.Monad.IO.Class (MonadIO, liftIO) import qualified Data.ByteString as BS import Data.HexString -import Data.Maybe (fromJust, isJust) +import Data.List (group, sort) +import Data.Maybe (catMaybes, fromJust, isJust) import qualified Data.Text as T -import Database.Persist -import Database.Persist.Sqlite +import qualified Data.Text.Encoding as TE +import Data.Word +import Database.Esqueleto.Experimental +import qualified Database.Persist as P +import qualified Database.Persist.Sqlite as PS import Database.Persist.TH -import Haskoin.Transaction.Common (TxOut(..)) +import Haskoin.Transaction.Common + ( OutPoint(..) + , TxIn(..) + , TxOut(..) + , txHashToHex + ) +import ZcashHaskell.Orchard (isValidUnifiedAddress) +import ZcashHaskell.Sapling (decodeSaplingOutputEsk) import ZcashHaskell.Types - ( OrchardAction(..) + ( DecodedNote(..) + , OrchardAction(..) , OrchardBundle(..) + , OrchardSpendingKey(..) + , OrchardWitness(..) , SaplingBundle(..) + , SaplingCommitmentTree(..) + , SaplingSpendingKey(..) + , SaplingWitness(..) , Scope(..) , ShieldedOutput(..) , ShieldedSpend(..) + , ToBytes(..) , Transaction(..) + , TransparentAddress(..) , TransparentBundle(..) + , TransparentReceiver(..) + , UnifiedAddress(..) , ZcashNet + , decodeHexText ) import Zenith.Types - ( HexStringDB(..) + ( Config(..) + , HexStringDB(..) , OrchardSpendingKeyDB(..) , PhraseDB(..) , SaplingSpendingKeyDB(..) @@ -57,6 +82,7 @@ share network ZcashNetDB seedPhrase PhraseDB birthdayHeight Int + lastSync Int default=0 UniqueWallet name network deriving Show Eq ZcashAccount @@ -80,45 +106,97 @@ share deriving Show Eq WalletTransaction txId HexStringDB + accId ZcashAccountId block Int conf Int time Int + UniqueWTx txId accId + deriving Show Eq + UserTx + hex HexStringDB + address WalletAddressId + time Int + amount Int + memo T.Text + UniqueUTx hex address deriving Show Eq WalletTrNote - tx WalletTransactionId - addrId WalletAddressId - value Int - rawId TransparentNoteId + tx WalletTransactionId OnDeleteCascade OnUpdateCascade + accId ZcashAccountId + value Word64 spent Bool + script BS.ByteString + change Bool + position Word64 + UniqueTNote tx script + deriving Show Eq + WalletTrSpend + tx WalletTransactionId OnDeleteCascade OnUpdateCascade + note WalletTrNoteId + accId ZcashAccountId + value Word64 deriving Show Eq WalletSapNote - tx WalletTransactionId - addrId WalletAddressId - value Int + tx WalletTransactionId OnDeleteCascade OnUpdateCascade + accId ZcashAccountId + value Word64 recipient BS.ByteString memo T.Text - rawId ShieldOutputId spent Bool + nullifier HexStringDB + position Word64 + witness HexStringDB + change Bool + UniqueSapNote tx nullifier + deriving Show Eq + WalletSapSpend + tx WalletTransactionId OnDeleteCascade OnUpdateCascade + note WalletSapNoteId + accId ZcashAccountId + value Word64 deriving Show Eq WalletOrchNote - tx WalletTransactionId - addrId WalletAddressId - value Int + tx WalletTransactionId OnDeleteCascade OnUpdateCascade + accId ZcashAccountId + value Word64 recipient BS.ByteString memo T.Text - rawId OrchActionId spent Bool + nullifier HexStringDB + position Word64 + witness HexStringDB + change Bool + UniqueOrchNote tx nullifier + deriving Show Eq + WalletOrchSpend + tx WalletTransactionId OnDeleteCascade OnUpdateCascade + note WalletOrchNoteId + accId ZcashAccountId + value Word64 deriving Show Eq ZcashTransaction block Int txId HexStringDB conf Int time Int + UniqueTx block txId deriving Show Eq TransparentNote tx ZcashTransactionId - value Int + value Word64 script BS.ByteString + position Int + UniqueTNPos tx position + deriving Show Eq + TransparentSpend + tx ZcashTransactionId + outPointHash HexStringDB + outPointIndex Word64 + script BS.ByteString + seq Word64 + position Int + UniqueTSPos tx position + deriving Show Eq OrchAction tx ZcashTransactionId nf HexStringDB @@ -129,6 +207,8 @@ share outCipher HexStringDB cv HexStringDB auth HexStringDB + position Int + UniqueOAPos tx position deriving Show Eq ShieldOutput tx ZcashTransactionId @@ -138,6 +218,8 @@ share encCipher HexStringDB outCipher HexStringDB proof HexStringDB + position Int + UniqueSOPos tx position deriving Show Eq ShieldSpend tx ZcashTransactionId @@ -147,6 +229,8 @@ share rk HexStringDB proof HexStringDB authSig HexStringDB + position Int + UniqueSSPos tx position deriving Show Eq |] @@ -156,26 +240,50 @@ initDb :: T.Text -- ^ The database path to check -> IO () initDb dbName = do - runSqlite dbName $ do runMigration migrateAll + PS.runSqlite dbName $ do runMigration migrateAll + +-- | Upgrade the database +upgradeDb :: + T.Text -- ^ database path + -> IO () +upgradeDb dbName = do + PS.runSqlite dbName $ do runMigrationUnsafe migrateAll -- | Get existing wallets from database getWallets :: T.Text -> ZcashNet -> IO [Entity ZcashWallet] getWallets dbFp n = - runSqlite dbFp $ selectList [ZcashWalletNetwork ==. ZcashNetDB n] [] + PS.runSqlite dbFp $ + select $ do + wallets <- from $ table @ZcashWallet + where_ (wallets ^. ZcashWalletNetwork ==. val (ZcashNetDB n)) + pure wallets -- | 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 +saveWallet dbFp w = PS.runSqlite dbFp $ insertUniqueEntity w + +-- | Update the last sync block for the wallet +updateWalletSync :: T.Text -> Int -> ZcashWalletId -> IO () +updateWalletSync dbPath b i = do + PS.runSqlite dbPath $ do + update $ \w -> do + set w [ZcashWalletLastSync =. val b] + where_ $ w ^. ZcashWalletId ==. val i -- | 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] [] +getAccounts dbFp w = + PS.runSqlite dbFp $ + select $ do + accs <- from $ table @ZcashAccount + where_ (accs ^. ZcashAccountWalletId ==. val w) + pure accs -- | Returns the largest account index for the given wallet getMaxAccount :: @@ -184,8 +292,12 @@ getMaxAccount :: -> IO Int getMaxAccount dbFp w = do a <- - runSqlite dbFp $ - selectFirst [ZcashAccountWalletId ==. w] [Desc ZcashAccountIndex] + PS.runSqlite dbFp $ + selectOne $ do + accs <- from $ table @ZcashAccount + where_ (accs ^. ZcashAccountWalletId ==. val w) + orderBy [desc $ accs ^. ZcashAccountIndex] + pure accs case a of Nothing -> return $ -1 Just x -> return $ zcashAccountIndex $ entityVal x @@ -195,7 +307,7 @@ 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 +saveAccount dbFp a = PS.runSqlite dbFp $ insertUniqueEntity a -- | Returns the largest block in storage getMaxBlock :: @@ -203,34 +315,51 @@ getMaxBlock :: -> IO Int getMaxBlock dbPath = do b <- - runSqlite dbPath $ - selectFirst [ZcashTransactionBlock >. 0] [Desc ZcashTransactionBlock] + PS.runSqlite dbPath $ + selectOne $ do + txs <- from $ table @ZcashTransaction + where_ (txs ^. ZcashTransactionBlock >. val 0) + orderBy [desc $ txs ^. ZcashTransactionBlock] + pure txs case b of Nothing -> return $ -1 Just x -> return $ zcashTransactionBlock $ entityVal x --- | Returns the largest block in the wallet -getMaxWalletBlock :: - T.Text -- ^ The database path - -> IO Int -getMaxWalletBlock dbPath = do - b <- - runSqlite dbPath $ - selectFirst [WalletTransactionBlock >. 0] [Desc WalletTransactionBlock] - case b of - Nothing -> return $ -1 - Just x -> return $ walletTransactionBlock $ entityVal x - -- | 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, WalletAddressScope ==. ScopeDB External] - [] + PS.runSqlite dbFp $ + select $ do + addrs <- from $ table @WalletAddress + where_ (addrs ^. WalletAddressAccId ==. val a) + where_ (addrs ^. WalletAddressScope ==. val (ScopeDB External)) + pure addrs + +-- | Returns a list of change addresses associated with the given account +getInternalAddresses :: + T.Text -- ^ The database path + -> ZcashAccountId -- ^ The account ID to check + -> IO [Entity WalletAddress] +getInternalAddresses dbFp a = + PS.runSqlite dbFp $ + select $ do + addrs <- from $ table @WalletAddress + where_ (addrs ^. WalletAddressAccId ==. val a) + where_ (addrs ^. WalletAddressScope ==. val (ScopeDB Internal)) + pure addrs + +-- | Returns a list of addressess associated with the given wallet +getWalletAddresses :: + T.Text -- ^ The database path + -> ZcashWalletId -- ^ the wallet to search + -> IO [Entity WalletAddress] +getWalletAddresses dbFp w = do + accs <- getAccounts dbFp w + addrs <- mapM (getAddresses dbFp . entityKey) accs + return $ concat addrs -- | Returns the largest address index for the given account getMaxAddress :: @@ -240,10 +369,13 @@ getMaxAddress :: -> IO Int getMaxAddress dbFp aw s = do a <- - runSqlite dbFp $ - selectFirst - [WalletAddressAccId ==. aw, WalletAddressScope ==. ScopeDB s] - [Desc WalletAddressIndex] + PS.runSqlite dbFp $ + selectOne $ do + addrs <- from $ table @WalletAddress + where_ $ addrs ^. WalletAddressAccId ==. val aw + where_ $ addrs ^. WalletAddressScope ==. val (ScopeDB s) + orderBy [desc $ addrs ^. WalletAddressIndex] + pure addrs case a of Nothing -> return $ -1 Just x -> return $ walletAddressIndex $ entityVal x @@ -253,7 +385,7 @@ 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 +saveAddress dbFp w = PS.runSqlite dbFp $ insertUniqueEntity w -- | Save a transaction to the data model saveTransaction :: @@ -262,30 +394,50 @@ saveTransaction :: -> Transaction -- ^ The transaction to save -> IO (Key ZcashTransaction) saveTransaction dbFp t wt = - runSqlite dbFp $ do + PS.runSqlite dbFp $ do + let ix = [0 ..] w <- insert $ ZcashTransaction (tx_height wt) (HexStringDB $ tx_id wt) (tx_conf wt) t - when (isJust $ tx_transpBundle wt) $ - insertMany_ $ - map (storeTxOut w) $ (tb_vout . fromJust . tx_transpBundle) wt + when (isJust $ tx_transpBundle wt) $ do + _ <- + insertMany_ $ + zipWith (curry (storeTxOut w)) ix $ + (tb_vout . fromJust . tx_transpBundle) wt + _ <- + insertMany_ $ + zipWith (curry (storeTxIn w)) ix $ + (tb_vin . fromJust . tx_transpBundle) wt + return () when (isJust $ tx_saplingBundle wt) $ do _ <- insertMany_ $ - map (storeSapSpend w) $ (sbSpends . fromJust . tx_saplingBundle) wt + zipWith (curry (storeSapSpend w)) ix $ + (sbSpends . fromJust . tx_saplingBundle) wt _ <- insertMany_ $ - map (storeSapOutput w) $ (sbOutputs . fromJust . tx_saplingBundle) wt + zipWith (curry (storeSapOutput w)) ix $ + (sbOutputs . fromJust . tx_saplingBundle) wt return () when (isJust $ tx_orchardBundle wt) $ insertMany_ $ - map (storeOrchAction w) $ (obActions . fromJust . tx_orchardBundle) wt + zipWith (curry (storeOrchAction w)) ix $ + (obActions . fromJust . tx_orchardBundle) wt return w where - storeTxOut :: ZcashTransactionId -> TxOut -> TransparentNote - storeTxOut wid (TxOut v s) = TransparentNote wid (fromIntegral v) s - storeSapSpend :: ZcashTransactionId -> ShieldedSpend -> ShieldSpend - storeSapSpend wid sp = + storeTxOut :: ZcashTransactionId -> (Int, TxOut) -> TransparentNote + storeTxOut wid (i, TxOut v s) = TransparentNote wid (fromIntegral v) s i + storeTxIn :: ZcashTransactionId -> (Int, TxIn) -> TransparentSpend + storeTxIn wid (i, TxIn (OutPoint h k) s sq) = + TransparentSpend + wid + (HexStringDB . fromText $ txHashToHex h) + (fromIntegral k) + s + (fromIntegral sq) + i + storeSapSpend :: ZcashTransactionId -> (Int, ShieldedSpend) -> ShieldSpend + storeSapSpend wid (i, sp) = ShieldSpend wid (HexStringDB $ sp_cv sp) @@ -294,8 +446,10 @@ saveTransaction dbFp t wt = (HexStringDB $ sp_rk sp) (HexStringDB $ sp_proof sp) (HexStringDB $ sp_auth sp) - storeSapOutput :: ZcashTransactionId -> ShieldedOutput -> ShieldOutput - storeSapOutput wid so = + i + storeSapOutput :: + ZcashTransactionId -> (Int, ShieldedOutput) -> ShieldOutput + storeSapOutput wid (i, so) = ShieldOutput wid (HexStringDB $ s_cv so) @@ -304,8 +458,9 @@ saveTransaction dbFp t wt = (HexStringDB $ s_encCipherText so) (HexStringDB $ s_outCipherText so) (HexStringDB $ s_proof so) - storeOrchAction :: ZcashTransactionId -> OrchardAction -> OrchAction - storeOrchAction wid oa = + i + storeOrchAction :: ZcashTransactionId -> (Int, OrchardAction) -> OrchAction + storeOrchAction wid (i, oa) = OrchAction wid (HexStringDB $ nf oa) @@ -316,3 +471,639 @@ saveTransaction dbFp t wt = (HexStringDB $ out_ciphertext oa) (HexStringDB $ cv oa) (HexStringDB $ auth oa) + i + +-- | Get the transactions from a particular block forward +getZcashTransactions :: + T.Text -- ^ The database path + -> Int -- ^ Block + -> IO [Entity ZcashTransaction] +getZcashTransactions dbFp b = + PS.runSqlite dbFp $ + select $ do + txs <- from $ table @ZcashTransaction + where_ $ txs ^. ZcashTransactionBlock >. val b + orderBy [asc $ txs ^. ZcashTransactionBlock] + return txs + +-- * Wallet +-- | Get the block of the last transaction known to the wallet +getMaxWalletBlock :: + T.Text -- ^ The database path + -> IO Int +getMaxWalletBlock dbPath = do + b <- + PS.runSqlite dbPath $ + selectOne $ do + txs <- from $ table @WalletTransaction + where_ $ txs ^. WalletTransactionBlock >. val 0 + orderBy [desc $ txs ^. WalletTransactionBlock] + return txs + case b of + Nothing -> return $ -1 + Just x -> return $ walletTransactionBlock $ entityVal x + +-- | Save a @WalletTransaction@ +saveWalletTransaction :: + T.Text + -> ZcashAccountId + -> Entity ZcashTransaction + -> IO WalletTransactionId +saveWalletTransaction dbPath za zt = do + let zT' = entityVal zt + PS.runSqlite dbPath $ do + t <- + upsert + (WalletTransaction + (zcashTransactionTxId zT') + za + (zcashTransactionBlock zT') + (zcashTransactionConf zT') + (zcashTransactionTime zT')) + [] + return $ entityKey t + +-- | Save a @WalletSapNote@ +saveWalletSapNote :: + T.Text -- ^ The database path + -> WalletTransactionId -- ^ The index for the transaction that contains the note + -> Integer -- ^ note position + -> SaplingWitness -- ^ the Sapling incremental witness + -> Bool -- ^ change flag + -> ZcashAccountId + -> DecodedNote -- The decoded Sapling note + -> IO () +saveWalletSapNote dbPath wId pos wit ch za dn = do + PS.runSqlite dbPath $ do + _ <- + upsert + (WalletSapNote + wId + za + (fromIntegral $ a_value dn) + (a_recipient dn) + (T.filter (/= '\NUL') $ TE.decodeUtf8Lenient $ a_memo dn) + False + (HexStringDB $ a_nullifier dn) + (fromIntegral pos) + (HexStringDB $ sapWit wit) + ch) + [] + return () + +-- | Save a @WalletOrchNote@ +saveWalletOrchNote :: + T.Text + -> WalletTransactionId + -> Integer + -> OrchardWitness + -> Bool + -> ZcashAccountId + -> DecodedNote + -> IO () +saveWalletOrchNote dbPath wId pos wit ch za dn = do + PS.runSqlite dbPath $ do + _ <- + upsert + (WalletOrchNote + wId + za + (fromIntegral $ a_value dn) + (a_recipient dn) + (T.filter (/= '\NUL') $ TE.decodeUtf8Lenient $ a_memo dn) + False + (HexStringDB $ a_nullifier dn) + (fromIntegral pos) + (HexStringDB $ orchWit wit) + ch) + [] + return () + +-- | Find the Transparent Notes that match the given transparent receiver +findTransparentNotes :: + T.Text -- ^ The database path + -> Int -- ^ Starting block + -> Entity WalletAddress + -> IO () +findTransparentNotes dbPath b t = do + let tReceiver = t_rec =<< readUnifiedAddressDB (entityVal t) + case tReceiver of + Just tR -> do + let s = + BS.concat + [ BS.pack [0x76, 0xA9, 0x14] + , (toBytes . tr_bytes) tR + , BS.pack [0x88, 0xAC] + ] + tN <- + PS.runSqlite dbPath $ + select $ do + (txs :& tNotes) <- + from $ table @ZcashTransaction `innerJoin` table @TransparentNote `on` + (\(txs :& tNotes) -> + txs ^. ZcashTransactionId ==. tNotes ^. TransparentNoteTx) + where_ (txs ^. ZcashTransactionBlock >. val b) + where_ (tNotes ^. TransparentNoteScript ==. val s) + pure (txs, tNotes) + mapM_ + (saveWalletTrNote + dbPath + (getScope $ walletAddressScope $ entityVal t) + (walletAddressAccId $ entityVal t)) + tN + Nothing -> return () + +-- | Add the transparent notes to the wallet +saveWalletTrNote :: + T.Text -- ^ the database path + -> Scope + -> ZcashAccountId + -> (Entity ZcashTransaction, Entity TransparentNote) + -> IO () +saveWalletTrNote dbPath ch za (zt, tn) = do + let zT' = entityVal zt + PS.runSqlite dbPath $ do + t <- + upsert + (WalletTransaction + (zcashTransactionTxId zT') + za + (zcashTransactionBlock zT') + (zcashTransactionConf zT') + (zcashTransactionTime zT')) + [] + insert_ $ + WalletTrNote + (entityKey t) + za + (transparentNoteValue $ entityVal tn) + False + (transparentNoteScript $ entityVal tn) + (ch == Internal) + (fromIntegral $ transparentNotePosition $ entityVal tn) + +-- | Save a Sapling note to the wallet database +saveSapNote :: T.Text -> WalletSapNote -> IO () +saveSapNote dbPath wsn = PS.runSqlite dbPath $ do insert_ wsn + +-- | Get the shielded outputs from the given blockheight +getShieldedOutputs :: + T.Text -- ^ database path + -> Int -- ^ block + -> IO [(Entity ZcashTransaction, Entity ShieldOutput)] +getShieldedOutputs dbPath b = + PS.runSqlite dbPath $ do + select $ do + (txs :& sOutputs) <- + from $ table @ZcashTransaction `innerJoin` table @ShieldOutput `on` + (\(txs :& sOutputs) -> + txs ^. ZcashTransactionId ==. sOutputs ^. ShieldOutputTx) + where_ (txs ^. ZcashTransactionBlock >=. val b) + orderBy + [ asc $ txs ^. ZcashTransactionId + , asc $ sOutputs ^. ShieldOutputPosition + ] + pure (txs, sOutputs) + +-- | Get the Orchard actions from the given blockheight forward +getOrchardActions :: + T.Text -- ^ database path + -> Int -- ^ block + -> IO [(Entity ZcashTransaction, Entity OrchAction)] +getOrchardActions dbPath b = + PS.runSqlite dbPath $ do + select $ do + (txs :& oActions) <- + from $ table @ZcashTransaction `innerJoin` table @OrchAction `on` + (\(txs :& oActions) -> + txs ^. ZcashTransactionId ==. oActions ^. OrchActionTx) + where_ (txs ^. ZcashTransactionBlock >=. val b) + orderBy + [asc $ txs ^. ZcashTransactionId, asc $ oActions ^. OrchActionPosition] + pure (txs, oActions) + +-- | Get the transactions belonging to the given address +getWalletTransactions :: + T.Text -- ^ database path + -> Entity WalletAddress + -> IO () +getWalletTransactions dbPath w = do + let w' = entityVal w + chgAddr <- getInternalAddresses dbPath $ walletAddressAccId $ entityVal w + let ctReceiver = t_rec =<< readUnifiedAddressDB (entityVal $ head chgAddr) + let csReceiver = s_rec =<< readUnifiedAddressDB (entityVal $ head chgAddr) + let coReceiver = o_rec =<< readUnifiedAddressDB (entityVal $ head chgAddr) + let tReceiver = t_rec =<< readUnifiedAddressDB w' + let sReceiver = s_rec =<< readUnifiedAddressDB w' + let oReceiver = o_rec =<< readUnifiedAddressDB w' + trNotes <- + case tReceiver of + Nothing -> return [] + Just tR -> do + let s = + BS.concat + [ BS.pack [0x76, 0xA9, 0x14] + , (toBytes . tr_bytes) tR + , BS.pack [0x88, 0xAC] + ] + PS.runSqlite dbPath $ do + select $ do + tnotes <- from $ table @WalletTrNote + where_ (tnotes ^. WalletTrNoteScript ==. val s) + pure tnotes + trChgNotes <- + case ctReceiver of + Nothing -> return [] + Just tR -> do + let s1 = + BS.concat + [ BS.pack [0x76, 0xA9, 0x14] + , (toBytes . tr_bytes) tR + , BS.pack [0x88, 0xAC] + ] + PS.runSqlite dbPath $ do + select $ do + tnotes <- from $ table @WalletTrNote + where_ (tnotes ^. WalletTrNoteScript ==. val s1) + pure tnotes + trSpends <- + PS.runSqlite dbPath $ do + select $ do + trSpends <- from $ table @WalletTrSpend + where_ + (trSpends ^. WalletTrSpendNote `in_` + valList (map entityKey (trNotes <> trChgNotes))) + pure trSpends + sapNotes <- + case sReceiver of + Nothing -> return [] + Just sR -> do + PS.runSqlite dbPath $ do + select $ do + snotes <- from $ table @WalletSapNote + where_ (snotes ^. WalletSapNoteRecipient ==. val (getBytes sR)) + pure snotes + sapChgNotes <- + case csReceiver of + Nothing -> return [] + Just sR -> do + PS.runSqlite dbPath $ do + select $ do + snotes <- from $ table @WalletSapNote + where_ (snotes ^. WalletSapNoteRecipient ==. val (getBytes sR)) + pure snotes + sapSpends <- mapM (getSapSpends . entityKey) (sapNotes <> sapChgNotes) + orchNotes <- + case oReceiver of + Nothing -> return [] + Just oR -> do + PS.runSqlite dbPath $ do + select $ do + onotes <- from $ table @WalletOrchNote + where_ (onotes ^. WalletOrchNoteRecipient ==. val (getBytes oR)) + pure onotes + orchChgNotes <- + case coReceiver of + Nothing -> return [] + Just oR -> do + PS.runSqlite dbPath $ do + select $ do + onotes <- from $ table @WalletOrchNote + where_ (onotes ^. WalletOrchNoteRecipient ==. val (getBytes oR)) + pure onotes + orchSpends <- mapM (getOrchSpends . entityKey) (orchNotes <> orchChgNotes) + mapM_ addTr trNotes + mapM_ addTr trChgNotes + mapM_ addSap sapNotes + mapM_ addSap sapChgNotes + mapM_ addOrch orchNotes + mapM_ addOrch orchChgNotes + mapM_ subTSpend trSpends + mapM_ subSSpend $ catMaybes sapSpends + mapM_ subOSpend $ catMaybes orchSpends + where + getSapSpends :: WalletSapNoteId -> IO (Maybe (Entity WalletSapSpend)) + getSapSpends n = do + PS.runSqlite dbPath $ do + selectOne $ do + sapSpends <- from $ table @WalletSapSpend + where_ (sapSpends ^. WalletSapSpendNote ==. val n) + pure sapSpends + getOrchSpends :: WalletOrchNoteId -> IO (Maybe (Entity WalletOrchSpend)) + getOrchSpends n = do + PS.runSqlite dbPath $ do + selectOne $ do + orchSpends <- from $ table @WalletOrchSpend + where_ (orchSpends ^. WalletOrchSpendNote ==. val n) + pure orchSpends + addTr :: Entity WalletTrNote -> IO () + addTr n = + upsertUserTx + (walletTrNoteTx $ entityVal n) + (entityKey w) + (fromIntegral $ walletTrNoteValue $ entityVal n) + "" + addSap :: Entity WalletSapNote -> IO () + addSap n = + upsertUserTx + (walletSapNoteTx $ entityVal n) + (entityKey w) + (fromIntegral $ walletSapNoteValue $ entityVal n) + (walletSapNoteMemo $ entityVal n) + addOrch :: Entity WalletOrchNote -> IO () + addOrch n = + upsertUserTx + (walletOrchNoteTx $ entityVal n) + (entityKey w) + (fromIntegral $ walletOrchNoteValue $ entityVal n) + (walletOrchNoteMemo $ entityVal n) + subTSpend :: Entity WalletTrSpend -> IO () + subTSpend n = + upsertUserTx + (walletTrSpendTx $ entityVal n) + (entityKey w) + (-(fromIntegral $ walletTrSpendValue $ entityVal n)) + "" + subSSpend :: Entity WalletSapSpend -> IO () + subSSpend n = + upsertUserTx + (walletSapSpendTx $ entityVal n) + (entityKey w) + (-(fromIntegral $ walletSapSpendValue $ entityVal n)) + "" + subOSpend :: Entity WalletOrchSpend -> IO () + subOSpend n = + upsertUserTx + (walletOrchSpendTx $ entityVal n) + (entityKey w) + (-(fromIntegral $ walletOrchSpendValue $ entityVal n)) + "" + upsertUserTx :: + WalletTransactionId -> WalletAddressId -> Int -> T.Text -> IO () + upsertUserTx tId wId amt memo = do + tr <- + PS.runSqlite dbPath $ do + select $ do + tx <- from $ table @WalletTransaction + where_ (tx ^. WalletTransactionId ==. val tId) + pure tx + existingUtx <- + PS.runSqlite dbPath $ do + selectOne $ do + ut <- from $ table @UserTx + where_ + (ut ^. UserTxHex ==. + val (walletTransactionTxId $ entityVal $ head tr)) + where_ (ut ^. UserTxAddress ==. val wId) + pure ut + case existingUtx of + Nothing -> do + _ <- + PS.runSqlite dbPath $ do + upsert + (UserTx + (walletTransactionTxId $ entityVal $ head tr) + wId + (walletTransactionTime $ entityVal $ head tr) + amt + memo) + [] + return () + Just uTx -> do + _ <- + PS.runSqlite dbPath $ do + update $ \t -> do + set + t + [ UserTxAmount +=. val amt + , UserTxMemo =. + val (memo <> " " <> userTxMemo (entityVal uTx)) + ] + where_ (t ^. UserTxId ==. val (entityKey uTx)) + return () + +getUserTx :: T.Text -> WalletAddressId -> IO [Entity UserTx] +getUserTx dbPath aId = do + PS.runSqlite dbPath $ do + select $ do + uTxs <- from $ table @UserTx + where_ (uTxs ^. UserTxAddress ==. val aId) + return uTxs + +-- | Get wallet transparent notes by account +getWalletTrNotes :: T.Text -> ZcashAccountId -> IO [Entity WalletTrNote] +getWalletTrNotes dbPath za = do + PS.runSqlite dbPath $ do + select $ do + n <- from $ table @WalletTrNote + where_ (n ^. WalletTrNoteAccId ==. val za) + pure n + +-- | find Transparent spends +findTransparentSpends :: T.Text -> ZcashAccountId -> IO () +findTransparentSpends dbPath za = do + notes <- getWalletTrNotes dbPath za + mapM_ findOneTrSpend notes + where + findOneTrSpend :: Entity WalletTrNote -> IO () + findOneTrSpend n = do + mReverseTxId <- + PS.runSqlite dbPath $ do + selectOne $ do + wtx <- from $ table @WalletTransaction + where_ + (wtx ^. WalletTransactionId ==. val (walletTrNoteTx $ entityVal n)) + pure $ wtx ^. WalletTransactionTxId + case mReverseTxId of + Nothing -> throwIO $ userError "failed to get tx ID" + Just (Value reverseTxId) -> do + s <- + PS.runSqlite dbPath $ do + select $ do + (tx :& trSpends) <- + from $ + table @ZcashTransaction `innerJoin` table @TransparentSpend `on` + (\(tx :& trSpends) -> + tx ^. ZcashTransactionId ==. trSpends ^. TransparentSpendTx) + where_ + (trSpends ^. TransparentSpendOutPointHash ==. val reverseTxId) + where_ + (trSpends ^. TransparentSpendOutPointIndex ==. + val (walletTrNotePosition $ entityVal n)) + pure (tx, trSpends) + if null s + then return () + else do + PS.runSqlite dbPath $ do + _ <- + update $ \w -> do + set w [WalletTrNoteSpent =. val True] + where_ $ w ^. WalletTrNoteId ==. val (entityKey n) + t' <- upsertWalTx (entityVal $ fst $ head s) za + insert_ $ + WalletTrSpend + (entityKey t') + (entityKey n) + za + (walletTrNoteValue $ entityVal n) + +getWalletSapNotes :: T.Text -> ZcashAccountId -> IO [Entity WalletSapNote] +getWalletSapNotes dbPath za = do + PS.runSqlite dbPath $ do + select $ do + n <- from $ table @WalletSapNote + where_ (n ^. WalletSapNoteAccId ==. val za) + pure n + +-- | Sapling DAG-aware spend tracking +findSapSpends :: T.Text -> ZcashAccountId -> [Entity WalletSapNote] -> IO () +findSapSpends _ _ [] = return () +findSapSpends dbPath za (n:notes) = do + s <- + PS.runSqlite dbPath $ do + select $ do + (tx :& sapSpends) <- + from $ table @ZcashTransaction `innerJoin` table @ShieldSpend `on` + (\(tx :& sapSpends) -> + tx ^. ZcashTransactionId ==. sapSpends ^. ShieldSpendTx) + where_ + (sapSpends ^. ShieldSpendNullifier ==. + val (walletSapNoteNullifier (entityVal n))) + pure (tx, sapSpends) + if null s + then findSapSpends dbPath za notes + else do + PS.runSqlite dbPath $ do + _ <- + update $ \w -> do + set w [WalletSapNoteSpent =. val True] + where_ $ w ^. WalletSapNoteId ==. val (entityKey n) + t' <- upsertWalTx (entityVal $ fst $ head s) za + insert_ $ + WalletSapSpend + (entityKey t') + (entityKey n) + za + (walletSapNoteValue $ entityVal n) + findSapSpends dbPath za notes + +getWalletOrchNotes :: T.Text -> ZcashAccountId -> IO [Entity WalletOrchNote] +getWalletOrchNotes dbPath za = do + PS.runSqlite dbPath $ do + select $ do + n <- from $ table @WalletOrchNote + where_ (n ^. WalletOrchNoteAccId ==. val za) + pure n + +findOrchSpends :: T.Text -> ZcashAccountId -> [Entity WalletOrchNote] -> IO () +findOrchSpends _ _ [] = return () +findOrchSpends dbPath za (n:notes) = do + s <- + PS.runSqlite dbPath $ do + select $ do + (tx :& orchSpends) <- + from $ table @ZcashTransaction `innerJoin` table @OrchAction `on` + (\(tx :& orchSpends) -> + tx ^. ZcashTransactionId ==. orchSpends ^. OrchActionTx) + where_ + (orchSpends ^. OrchActionNf ==. + val (walletOrchNoteNullifier (entityVal n))) + pure (tx, orchSpends) + if null s + then findOrchSpends dbPath za notes + else do + PS.runSqlite dbPath $ do + _ <- + update $ \w -> do + set w [WalletOrchNoteSpent =. val True] + where_ $ w ^. WalletOrchNoteId ==. val (entityKey n) + t' <- upsertWalTx (entityVal $ fst $ head s) za + insert_ $ + WalletOrchSpend + (entityKey t') + (entityKey n) + za + (walletOrchNoteValue $ entityVal n) + findOrchSpends dbPath za notes + +upsertWalTx :: + MonadIO m + => ZcashTransaction + -> ZcashAccountId + -> SqlPersistT m (Entity WalletTransaction) +upsertWalTx zt za = + upsert + (WalletTransaction + (zcashTransactionTxId zt) + za + (zcashTransactionBlock zt) + (zcashTransactionConf zt) + (zcashTransactionTime zt)) + [] + +getBalance :: T.Text -> ZcashAccountId -> IO Integer +getBalance dbPath za = do + trNotes <- + PS.runSqlite dbPath $ do + select $ do + n <- from $ table @WalletTrNote + where_ (n ^. WalletTrNoteAccId ==. val za) + where_ (n ^. WalletTrNoteSpent ==. val False) + pure n + let tAmts = map (walletTrNoteValue . entityVal) trNotes + let tBal = sum tAmts + sapNotes <- + PS.runSqlite dbPath $ do + select $ do + n1 <- from $ table @WalletSapNote + where_ (n1 ^. WalletSapNoteAccId ==. val za) + where_ (n1 ^. WalletSapNoteSpent ==. val False) + pure n1 + let sAmts = map (walletSapNoteValue . entityVal) sapNotes + let sBal = sum sAmts + orchNotes <- + PS.runSqlite dbPath $ do + select $ do + n2 <- from $ table @WalletOrchNote + where_ (n2 ^. WalletOrchNoteAccId ==. val za) + where_ (n2 ^. WalletOrchNoteSpent ==. val False) + pure n2 + let oAmts = map (walletOrchNoteValue . entityVal) orchNotes + let oBal = sum oAmts + return . fromIntegral $ tBal + sBal + oBal + +clearWalletTransactions :: T.Text -> IO () +clearWalletTransactions dbPath = do + PS.runSqlite dbPath $ do + delete $ do + _ <- from $ table @WalletOrchSpend + return () + delete $ do + _ <- from $ table @WalletOrchNote + return () + delete $ do + _ <- from $ table @WalletSapSpend + return () + delete $ do + _ <- from $ table @WalletSapNote + return () + delete $ do + _ <- from $ table @WalletTrNote + return () + delete $ do + _ <- from $ table @WalletTrSpend + return () + delete $ do + _ <- from $ table @WalletTransaction + return () + delete $ do + _ <- from $ table @UserTx + return () + +-- | Helper function to extract a Unified Address from the database +readUnifiedAddressDB :: WalletAddress -> Maybe UnifiedAddress +readUnifiedAddressDB = + isValidUnifiedAddress . TE.encodeUtf8 . getUA . walletAddressUAddress + +rmdups :: Ord a => [a] -> [a] +rmdups = map head . group . sort diff --git a/src/Zenith/Scanner.hs b/src/Zenith/Scanner.hs index 283387c..8d49a74 100644 --- a/src/Zenith/Scanner.hs +++ b/src/Zenith/Scanner.hs @@ -44,7 +44,10 @@ scanZebra b host port dbFilePath = do if sb > zgb_blocks bStatus || sb < 1 then throwIO $ userError "Invalid starting block for scan" else do - let bList = [sb .. (zgb_blocks bStatus)] + print $ + "Scanning from " ++ + show (sb + 1) ++ " to " ++ show (zgb_blocks bStatus) + let bList = [(sb + 1) .. (zgb_blocks bStatus)] displayConsoleRegions $ do pg <- newProgressBar def {pgTotal = fromIntegral $ length bList} txList <- diff --git a/src/Zenith/Types.hs b/src/Zenith/Types.hs index 33a946b..32c44ea 100644 --- a/src/Zenith/Types.hs +++ b/src/Zenith/Types.hs @@ -10,7 +10,6 @@ module Zenith.Types where import Data.Aeson -import Data.Aeson.Types (prependFailure, typeMismatch) import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as C @@ -30,6 +29,7 @@ import ZcashHaskell.Types , ZcashNet(..) ) +-- * UI -- * Database field type wrappers newtype HexStringDB = HexStringDB { getHex :: HexString @@ -80,6 +80,36 @@ newtype TransparentSpendingKeyDB = TransparentSpendingKeyDB derivePersistField "TransparentSpendingKeyDB" -- * RPC +-- | Type for Configuration parameters +data Config = Config + { c_dbPath :: !T.Text + , c_zebraHost :: !T.Text + , c_zebraPort :: !Int + } deriving (Eq, Prelude.Show) + +-- ** `zebrad` +-- | Type for modeling the tree state response +data ZebraTreeInfo = ZebraTreeInfo + { ztiHeight :: !Int + , ztiTime :: !Int + , ztiSapling :: !HexString + , ztiOrchard :: !HexString + } deriving (Eq, Show, Read) + +instance FromJSON ZebraTreeInfo where + parseJSON = + withObject "ZebraTreeInfo" $ \obj -> do + h <- obj .: "height" + t <- obj .: "time" + s <- obj .: "sapling" + o <- obj .: "orchard" + sc <- s .: "commitments" + oc <- o .: "commitments" + sf <- sc .: "finalState" + ocf <- oc .: "finalState" + pure $ ZebraTreeInfo h t sf ocf + +-- ** `zcashd` -- | Type for modelling the different address sources for `zcashd` 5.0.0 data AddressSource = LegacyRandom diff --git a/src/Zenith/Utils.hs b/src/Zenith/Utils.hs index 0f325ff..0f013e8 100644 --- a/src/Zenith/Utils.hs +++ b/src/Zenith/Utils.hs @@ -31,6 +31,14 @@ displayZec s | 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 + | s < 100 = show s ++ " tazs " + | s < 100000 = show (fromIntegral s / 100) ++ " μTAZ " + | s < 100000000 = show (fromIntegral s / 100000) ++ " mTAZ " + | otherwise = show (fromIntegral s / 100000000) ++ " TAZ " + -- | Helper function to display abbreviated Unified Address showAddress :: UnifiedAddressDB -> T.Text showAddress u = T.take 20 t <> "..." diff --git a/test/Spec.hs b/test/Spec.hs index bfc6f68..af1f21f 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1,16 +1,27 @@ {-# LANGUAGE OverloadedStrings #-} import Control.Monad (when) +import Data.HexString import Database.Persist import Database.Persist.Sqlite import System.Directory +import Test.HUnit import Test.Hspec import ZcashHaskell.Orchard (isValidUnifiedAddress) +import ZcashHaskell.Sapling + ( decodeSaplingOutputEsk + , getSaplingNotePosition + , getSaplingWitness + , updateSaplingCommitmentTree + ) import ZcashHaskell.Types - ( OrchardSpendingKey(..) + ( DecodedNote(..) + , OrchardSpendingKey(..) , Phrase(..) + , SaplingCommitmentTree(..) , SaplingSpendingKey(..) , Scope(..) + , ShieldedOutput(..) , ZcashNet(..) ) import Zenith.Core @@ -38,6 +49,7 @@ main = do Phrase "one two three four five six seven eight nine ten eleven twelve") 2000000 + 0 fromSqlKey s `shouldBe` 1 it "read wallet record" $ do s <- @@ -69,6 +81,7 @@ main = do 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 + 0 zw `shouldNotBe` Nothing it "Save Account:" $ do s <- @@ -98,3 +111,52 @@ main = do let ua = "utest1mvlny48qd4x94w8vz5u2lrxx0enuquajt72yekgq24p6pjaky3czk6m7x358h7g900ex6gzvdehaekl96qnakjzw8yaasp8y0u3j5jnlfd33trduznh6k3fcn5ek9qc857fgz8ehm37etx94sj58nrkc0k5hurxnuxpcpms3j8uy2t8kt2vy6vetvsfxxdhtjq0yqulqprvh7mf2u3x" isValidUnifiedAddress ua `shouldNotBe` Nothing + describe "Function tests" $ do + describe "Sapling Decoding" $ do + let sk = + SaplingSpendingKey + "\ETX}\195.\SUB\NUL\NUL\NUL\128\NUL\203\"\229IL\CANJ*\209\EM\145\228m\172\&4\SYNNl\DC3\161\147\SO\157\238H\192\147eQ\143L\201\216\163\180\147\145\156Zs+\146>8\176`ta\161\223\SO\140\177\b;\161\SO\236\151W\148<\STX\171|\DC2\172U\195(I\140\146\214\182\137\211\228\159\128~bV\STXy{m'\224\175\221\219\180!\ENQ_\161\132\240?\255\236\"6\133\181\170t\181\139\143\207\170\211\ENQ\167a\184\163\243\246\140\158t\155\133\138X\a\241\200\140\EMT\GS~\175\249&z\250\214\231\239mi\223\206\STX\t\EM<{V~J\253FB" + let tree = + SaplingCommitmentTree $ + hexString + "01818f2bd58b1e392334d0565181cc7843ae09e3533b2a50a8f1131af657340a5c001001161f962245812ba5e1804fd0a336bc78fa4ee4441a8e0f1525ca5da1b285d35101120f45afa700b8c1854aa8b9c8fe8ed92118ef790584bfcb926078812a10c83a00000000012f4f72c03f8c937a94919a01a07f21165cc8394295291cb888ca91ed003810390107114fe4bb4cd08b47f6ae47477c182d5da9fe5c189061808c1091e9bf3b4524000001447d6b9100cddd5f80c8cf4ddee2b87eba053bd987465aec2293bd0514e68b0d015f6c95e75f4601a0a31670a7deb970fc8988c611685161d2e1629d0a1a0ebd07015f8b9205e0514fa235d75c150b87e23866b882b39786852d1ab42aab11d31a4a0117ddeb3a5f8d2f6b2d0a07f28f01ab25e03a05a9319275bb86d72fcaef6fc01501f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39" + let nextTree = + SaplingCommitmentTree $ + hexString + "01bd8a3f3cfc964332a2ada8c09a0da9dfc24174befb938abb086b9be5ca049e4900100000019f0d7efb00169bb2202152d3266059d208ab17d14642c3339f9075e997160657000000012f4f72c03f8c937a94919a01a07f21165cc8394295291cb888ca91ed003810390107114fe4bb4cd08b47f6ae47477c182d5da9fe5c189061808c1091e9bf3b4524000001447d6b9100cddd5f80c8cf4ddee2b87eba053bd987465aec2293bd0514e68b0d015f6c95e75f4601a0a31670a7deb970fc8988c611685161d2e1629d0a1a0ebd07015f8b9205e0514fa235d75c150b87e23866b882b39786852d1ab42aab11d31a4a0117ddeb3a5f8d2f6b2d0a07f28f01ab25e03a05a9319275bb86d72fcaef6fc01501f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39" + it "Sapling is decoded correctly" $ do + so <- + runSqlite "zenith.db" $ + selectList [ShieldOutputTx ==. toSqlKey 38318] [] + let cmus = map (getHex . shieldOutputCmu . entityVal) so + let pos = + getSaplingNotePosition <$> + (getSaplingWitness =<< + updateSaplingCommitmentTree tree (head cmus)) + let pos1 = getSaplingNotePosition <$> getSaplingWitness tree + let pos2 = getSaplingNotePosition <$> getSaplingWitness nextTree + case pos of + Nothing -> assertFailure "couldn't get note position" + Just p -> do + print p + print pos1 + print pos2 + let dn = + decodeSaplingOutputEsk + sk + (ShieldedOutput + (getHex $ shieldOutputCv $ entityVal $ head so) + (getHex $ shieldOutputCmu $ entityVal $ head so) + (getHex $ shieldOutputEphKey $ entityVal $ head so) + (getHex $ shieldOutputEncCipher $ entityVal $ head so) + (getHex $ shieldOutputOutCipher $ entityVal $ head so) + (getHex $ shieldOutputProof $ entityVal $ head so)) + TestNet + External + p + case dn of + Nothing -> assertFailure "couldn't decode Sap output" + Just d -> + a_nullifier d `shouldBe` + hexString + "6c5d1413c63a9a88db71c3f41dc12cd60197ee742fc75b217215e7144db48bd3" diff --git a/zcash-haskell b/zcash-haskell index 938ccb4..00400c4 160000 --- a/zcash-haskell +++ b/zcash-haskell @@ -1 +1 @@ -Subproject commit 938ccb4b9730fd8615513eb27bdbffacd62e29cc +Subproject commit 00400c433dd8a584ef19af58fcab7fdd108d4110 diff --git a/zenith.cabal b/zenith.cabal index 6691f3f..1b0ea04 100644 --- a/zenith.cabal +++ b/zenith.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: zenith -version: 0.4.4.3 +version: 0.4.6.0 license: MIT license-file: LICENSE author: Rene Vergara @@ -39,10 +39,12 @@ library Clipboard , aeson , array + , ascii-progress , base >=4.12 && <5 , base64-bytestring , brick , bytestring + , esqueleto , ghc , haskoin-core , hexstring @@ -62,10 +64,10 @@ library , regex-posix , scientific , text + , time , vector , vty , word-wrap - , ascii-progress , zcash-haskell --pkgconfig-depends: rustzcash_wrapper default-language: Haskell2010 @@ -119,6 +121,8 @@ test-suite zenith-tests , persistent , persistent-sqlite , hspec + , hexstring + , HUnit , directory , zcash-haskell , zenith