{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeApplications #-} module Zenith.DB where import Control.Monad (when) import Control.Monad.IO.Class (MonadIO) import Data.Bifunctor import qualified Data.ByteString as BS import Data.HexString import Data.Maybe (fromJust, isJust) import qualified Data.Text as T 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 ( OutPoint(..) , TxIn(..) , TxOut(..) , txHashToHex ) import ZcashHaskell.Orchard (isValidUnifiedAddress) import ZcashHaskell.Sapling (decodeSaplingOutputEsk) import ZcashHaskell.Types ( DecodedNote(..) , OrchardAction(..) , OrchardBundle(..) , OrchardSpendingKey(..) , OrchardWitness(..) , SaplingBundle(..) , SaplingCommitmentTree(..) , SaplingSpendingKey(..) , SaplingWitness(..) , Scope(..) , ShieldedOutput(..) , ShieldedSpend(..) , ToBytes(..) , Transaction(..) , TransparentAddress(..) , TransparentBundle(..) , TransparentReceiver(..) , UnifiedAddress(..) , ZcashNet , decodeHexText ) import Zenith.Types ( Config(..) , HexStringDB(..) , OrchardSpendingKeyDB(..) , PhraseDB(..) , SaplingSpendingKeyDB(..) , ScopeDB(..) , TransparentSpendingKeyDB , UnifiedAddressDB(..) , UserTx(..) , ZcashNetDB(..) ) share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| ZcashWallet name T.Text network ZcashNetDB seedPhrase PhraseDB birthdayHeight Int UniqueWallet name network deriving Show Eq ZcashAccount index Int walletId ZcashWalletId name T.Text orchSpendKey OrchardSpendingKeyDB sapSpendKey SaplingSpendingKeyDB tPrivateKey TransparentSpendingKeyDB UniqueAccount index walletId UniqueAccName walletId name deriving Show Eq WalletAddress index Int accId ZcashAccountId name T.Text uAddress UnifiedAddressDB scope ScopeDB UniqueAddress index scope accId UniqueAddName accId name deriving Show Eq WalletTransaction txId HexStringDB block Int conf Int time Int UniqueWTx txId deriving Show Eq WalletTrNote tx WalletTransactionId value Word64 spent Bool script BS.ByteString change Bool UniqueTNote tx script deriving Show Eq WalletTrSpend tx WalletTransactionId note WalletTrNoteId value Word64 deriving Show Eq WalletSapNote tx WalletTransactionId value Word64 recipient BS.ByteString memo T.Text spent Bool nullifier HexStringDB position Word64 witness HexStringDB change Bool UniqueSapNote tx nullifier deriving Show Eq WalletSapSpend tx WalletTransactionId note WalletSapNoteId value Word64 deriving Show Eq WalletOrchNote tx WalletTransactionId value Word64 recipient BS.ByteString memo T.Text spent Bool nullifier HexStringDB position Word64 witness HexStringDB change Bool UniqueOrchNote tx nullifier deriving Show Eq WalletOrchSpend tx WalletTransactionId note WalletOrchNoteId value Word64 deriving Show Eq ZcashTransaction block Int txId HexStringDB conf Int time Int UniqueTx block txId deriving Show Eq TransparentNote tx ZcashTransactionId value Word64 script BS.ByteString position Int UniqueTNPos tx position deriving Show Eq TransparentSpend tx ZcashTransactionId outPointHash HexStringDB outPointIndex Int script BS.ByteString seq Int position Int UniqueTSPos tx position deriving Show Eq OrchAction tx ZcashTransactionId nf HexStringDB rk HexStringDB cmx HexStringDB ephKey HexStringDB encCipher HexStringDB outCipher HexStringDB cv HexStringDB auth HexStringDB position Int UniqueOAPos tx position deriving Show Eq ShieldOutput tx ZcashTransactionId cv HexStringDB cmu HexStringDB ephKey HexStringDB encCipher HexStringDB outCipher HexStringDB proof HexStringDB position Int UniqueSOPos tx position deriving Show Eq ShieldSpend tx ZcashTransactionId cv HexStringDB anchor HexStringDB nullifier HexStringDB rk HexStringDB proof HexStringDB authSig HexStringDB position Int UniqueSSPos tx position deriving Show Eq |] -- * Database functions -- | Initializes the database initDb :: T.Text -- ^ The database path to check -> IO () initDb dbName = do PS.runSqlite dbName $ do runMigration migrateAll -- | Get existing wallets from database getWallets :: T.Text -> ZcashNet -> IO [Entity ZcashWallet] getWallets dbFp 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 = PS.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 = 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 :: T.Text -- ^ The database path -> ZcashWalletId -- ^ The wallet ID to check -> IO Int getMaxAccount dbFp w = do a <- 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 -- | 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 = PS.runSqlite dbFp $ insertUniqueEntity a -- | Returns the largest block in storage getMaxBlock :: T.Text -- ^ The database path -> IO Int getMaxBlock dbPath = do b <- 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 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 = 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 :: T.Text -- ^ The database path -> ZcashAccountId -- ^ The account ID to check -> Scope -- ^ The scope of the address -> IO Int getMaxAddress dbFp aw s = do a <- 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 -- | 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 = PS.runSqlite dbFp $ insertUniqueEntity w -- | Save a transaction to the data model saveTransaction :: T.Text -- ^ the database path -> Int -- ^ block time -> Transaction -- ^ The transaction to save -> IO (Key ZcashTransaction) saveTransaction dbFp t wt = 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) $ 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_ $ zipWith (curry (storeSapSpend w)) ix $ (sbSpends . fromJust . tx_saplingBundle) wt _ <- insertMany_ $ zipWith (curry (storeSapOutput w)) ix $ (sbOutputs . fromJust . tx_saplingBundle) wt return () when (isJust $ tx_orchardBundle wt) $ insertMany_ $ zipWith (curry (storeOrchAction w)) ix $ (obActions . fromJust . tx_orchardBundle) wt return w where 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) (HexStringDB $ sp_anchor sp) (HexStringDB $ sp_nullifier sp) (HexStringDB $ sp_rk sp) (HexStringDB $ sp_proof sp) (HexStringDB $ sp_auth sp) i storeSapOutput :: ZcashTransactionId -> (Int, ShieldedOutput) -> ShieldOutput storeSapOutput wid (i, so) = ShieldOutput wid (HexStringDB $ s_cv so) (HexStringDB $ s_cmu so) (HexStringDB $ s_ephKey so) (HexStringDB $ s_encCipherText so) (HexStringDB $ s_outCipherText so) (HexStringDB $ s_proof so) i storeOrchAction :: ZcashTransactionId -> (Int, OrchardAction) -> OrchAction storeOrchAction wid (i, oa) = OrchAction wid (HexStringDB $ nf oa) (HexStringDB $ rk oa) (HexStringDB $ cmx oa) (HexStringDB $ eph_key oa) (HexStringDB $ enc_ciphertext oa) (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 -> Entity ZcashTransaction -> IO WalletTransactionId saveWalletTransaction dbPath zt = do let zT' = entityVal zt PS.runSqlite dbPath $ do t <- upsert (WalletTransaction (zcashTransactionTxId zT') (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 -> DecodedNote -- The decoded Sapling note -> IO () saveWalletSapNote dbPath wId pos wit ch dn = do PS.runSqlite dbPath $ do _ <- upsert (WalletSapNote wId (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 -> DecodedNote -> IO () saveWalletOrchNote dbPath wId pos wit ch dn = do PS.runSqlite dbPath $ do _ <- upsert (WalletOrchNote wId (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 -> WalletAddress -> IO [(Entity ZcashTransaction, Entity TransparentNote)] findTransparentNotes dbPath b t = do let tReceiver = t_rec =<< readUnifiedAddressDB t case tReceiver of Just tR -> do let s = BS.concat [ BS.pack [0x76, 0xA9, 0x14] , (toBytes . tr_bytes) tR , BS.pack [0x88, 0xAC] ] 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) Nothing -> return [] -- | Add the transparent notes to the wallet saveWalletTrNote :: T.Text -- ^ the database path -> Scope -> (Entity ZcashTransaction, Entity TransparentNote) -> IO () saveWalletTrNote dbPath ch (zt, tn) = do let zT' = entityVal zt PS.runSqlite dbPath $ do t <- upsert (WalletTransaction (zcashTransactionTxId zT') (zcashTransactionBlock zT') (zcashTransactionConf zT') (zcashTransactionTime zT')) [] insert_ $ WalletTrNote (entityKey t) (transparentNoteValue $ entityVal tn) False (transparentNoteScript $ entityVal tn) (ch == Internal) -- | 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 forward 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 -> WalletAddress -> IO [WalletTransactionId] getWalletTransactions dbPath w = do 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 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 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 let addrTx = map (walletTrNoteTx . entityVal) trNotes <> map (walletSapNoteTx . entityVal) sapNotes <> map (walletOrchNoteTx . entityVal) orchNotes return addrTx getUserTx :: T.Text -> [WalletTransactionId] -> IO [UserTx] getUserTx dbPath addrTx = do mapM convertUserTx addrTx where convertUserTx :: WalletTransactionId -> IO UserTx convertUserTx tId = do tr <- PS.runSqlite dbPath $ do select $ do tx <- from $ table @WalletTransaction where_ (tx ^. WalletTransactionId ==. val tId) pure tx trNotes <- PS.runSqlite dbPath $ do select $ do trNotes <- from $ table @WalletTrNote where_ (trNotes ^. WalletTrNoteTx ==. val tId) pure trNotes trSpends <- PS.runSqlite dbPath $ do select $ do trSpends <- from $ table @WalletTrSpend where_ (trSpends ^. WalletTrSpendTx ==. val tId) pure trSpends sapNotes <- PS.runSqlite dbPath $ do select $ do sapNotes <- from $ table @WalletSapNote where_ (sapNotes ^. WalletSapNoteTx ==. val tId) pure sapNotes sapSpends <- PS.runSqlite dbPath $ do select $ do sapSpends <- from $ table @WalletSapSpend where_ (sapSpends ^. WalletSapSpendTx ==. val tId) pure sapSpends orchNotes <- PS.runSqlite dbPath $ do select $ do orchNotes <- from $ table @WalletOrchNote where_ (orchNotes ^. WalletOrchNoteTx ==. val tId) pure orchNotes orchSpends <- PS.runSqlite dbPath $ do select $ do orchSpends <- from $ table @WalletOrchSpend where_ (orchSpends ^. WalletOrchSpendTx ==. val tId) pure orchSpends return $ UserTx (getHex $ walletTransactionTxId $ entityVal $ head tr) (fromIntegral $ walletTransactionTime $ entityVal $ head tr) (sum (map (fromIntegral . walletTrNoteValue . entityVal) trNotes) + sum (map (fromIntegral . walletSapNoteValue . entityVal) sapNotes) + sum (map (fromIntegral . walletOrchNoteValue . entityVal) orchNotes) - sum (map (fromIntegral . walletTrSpendValue . entityVal) trSpends) - sum (map (fromIntegral . walletSapSpendValue . entityVal) sapSpends) - sum (map (fromIntegral . walletOrchSpendValue . entityVal) orchSpends)) (T.concat (map (walletSapNoteMemo . entityVal) sapNotes) <> T.concat (map (walletOrchNoteMemo . entityVal) orchNotes)) -- | Sapling DAG-aware spend tracking findSapSpends :: T.Text -> SaplingSpendingKey -> [Entity WalletSapNote] -> IO () findSapSpends _ _ [] = return () findSapSpends dbPath sk (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 sk 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 insert_ $ WalletSapSpend (entityKey t') (entityKey n) (walletSapNoteValue $ entityVal n) findSapSpends dbPath sk notes findOrchSpends :: T.Text -> OrchardSpendingKey -> [Entity WalletOrchNote] -> IO () findOrchSpends _ _ [] = return () findOrchSpends dbPath sk (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 sk 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 insert_ $ WalletOrchSpend (entityKey t') (entityKey n) (walletOrchNoteValue $ entityVal n) findOrchSpends dbPath sk notes upsertWalTx :: MonadIO m => ZcashTransaction -> SqlPersistT m (Entity WalletTransaction) upsertWalTx zt = upsert (WalletTransaction (zcashTransactionTxId zt) (zcashTransactionBlock zt) (zcashTransactionConf zt) (zcashTransactionTime zt)) [] -- | Helper function to extract a Unified Address from the database readUnifiedAddressDB :: WalletAddress -> Maybe UnifiedAddress readUnifiedAddressDB = isValidUnifiedAddress . TE.encodeUtf8 . getUA . walletAddressUAddress