Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store TxHistory outside of the wallet state #150

Merged
merged 2 commits into from
Apr 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions src/Cardano/Wallet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,20 @@
-- intermediary between the three.


module Cardano.Wallet where
module Cardano.Wallet
(
-- * Types
WalletLayer (..)
, NewWallet(..)
, ReadWalletError(..)
, CreateWalletError(..)

-- * Construction
, mkWalletLayer

-- * Helpers
, unsafeRunExceptT
) where

import Prelude

Expand All @@ -37,17 +50,24 @@ import Cardano.Wallet.Primitive.Types
( Block (..), WalletId (..), WalletMetadata (..), WalletName (..) )
import Control.Exception
( Exception )
import Control.Monad
( (>=>) )
import Control.Monad.Fail
( MonadFail )
import Control.Monad.IO.Class
( liftIO )
import Control.Monad.Trans.Except
( ExceptT, throwE )
( ExceptT, runExceptT, throwE )
import Data.List
( foldl' )
import GHC.Generics
( Generic )


-- | Types
{-------------------------------------------------------------------------------
Types
-------------------------------------------------------------------------------}

data WalletLayer s = WalletLayer
{ createWallet
:: NewWallet
Expand Down Expand Up @@ -83,6 +103,9 @@ newtype CreateWalletError
= ErrCreateWalletIdAlreadyExists WalletId
deriving (Eq, Show)

{-------------------------------------------------------------------------------
Construction
-------------------------------------------------------------------------------}

-- | Create a new instance of the wallet layer.
mkWalletLayer
Expand Down Expand Up @@ -123,10 +146,27 @@ mkWalletLayer db network = WalletLayer
where
applyBlocks :: WalletId -> [Block] -> IO ()
applyBlocks wid blocks = do
cp' <- readCheckpoint db (PrimaryKey wid) >>= \case
(txs, cp') <- readCheckpoint db (PrimaryKey wid) >>= \case
Nothing ->
fail $ "couldn't find worker wallet: " <> show wid
Just cp -> do
let nonEmpty = not . null . transactions
return $ foldl' (flip applyBlock) cp (filter nonEmpty blocks)
let applyOne (txs, cp') b = (txs <> txs', cp'') where
(txs', cp'') = applyBlock b cp'
return $ foldl' applyOne (mempty, cp) (filter nonEmpty blocks)
putCheckpoint db (PrimaryKey wid) cp'
unsafeRunExceptT $ putTxHistory db (PrimaryKey wid) txs -- Safe after ^

{-------------------------------------------------------------------------------
Helpers
-------------------------------------------------------------------------------}

-- | Run an ExcepT and throws the error if any. This makes sense only if called
-- after checking for an invariant or, after ensuring that preconditions for
-- meeting the underlying error have been discarded.
unsafeRunExceptT :: (MonadFail m, Show e) => ExceptT e m a -> m a
unsafeRunExceptT = runExceptT >=> \case
Left e ->
fail $ "unexpected error: " <> show e
Right a ->
return a
39 changes: 36 additions & 3 deletions src/Cardano/Wallet/DB.hs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{-# LANGUAGE DataKinds #-}

-- |
-- Copyright: © 2018-2019 IOHK
-- License: MIT
Expand All @@ -8,35 +10,66 @@
module Cardano.Wallet.DB
( DBLayer(..)
, PrimaryKey(..)
, ErrPutTxHistory(..)
) where

import Prelude

import Cardano.Wallet.Primitive.Model
( Wallet )
import Cardano.Wallet.Primitive.Types
( WalletId )
( Hash, Tx, TxMeta, WalletId )
import Control.Monad.Trans.Except
( ExceptT )
import Data.Map.Strict
( Map )


-- | A Database interface for storing various things in a DB. In practice,
-- we'll need some extra contraints on the wallet state that allows us to
-- serialize and unserialize it (e.g. @forall s. (Serialize s) => ...@)
data DBLayer m s = DBLayer
-- Wallet checkpoints, checkpoints are handled as a bounded FIFO, where we
-- eventually store @k@ values (e.g. k=2160) at the same time.
{ putCheckpoint
:: PrimaryKey WalletId
-> Wallet s
-> m ()
-- ^ Replace the current checkpoint for a given wallet. We do not handle
-- rollbacks yet, and therefore only stores the latest available
-- checkpoint.

, readCheckpoint
:: PrimaryKey WalletId
-> m (Maybe (Wallet s))
-- ^ Fetch the most recent checkpoint of a given wallet. Return 'Nothing'
-- if there's no such wallet.

, readWallets
:: m [PrimaryKey WalletId]
-- ^ Get the list of all known wallets in the DB, possibly empty.

, putTxHistory
:: PrimaryKey WalletId
-> Map (Hash "Tx") (Tx, TxMeta)
-> ExceptT ErrPutTxHistory m ()
-- ^ Augments the transaction history for a known wallet.
--
-- If an entry for a particular transaction already exists it is not
-- altered nor merged (just ignored).
--
-- If the wallet doesn't exist, this operation returns an error.

, readTxHistory
:: PrimaryKey WalletId
-> m (Map (Hash "Tx") (Tx, TxMeta))
-- ^ Fetch the current transaction history of a known wallet. Returns an
-- empty map if the wallet isn't found.
}

-- | Error while trying to insert transaction history in the DB.
newtype ErrPutTxHistory
= ErrNoSuchWallet WalletId
deriving (Show, Eq)

-- | A primary key which can take many forms depending on the value. This may
-- become a type family as we move forward, but for now, it illustrate that
-- some queries are ran against some sort of store;
Expand Down
43 changes: 36 additions & 7 deletions src/Cardano/Wallet/DB/MVar.hs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}

-- |
Expand All @@ -15,25 +17,52 @@ module Cardano.Wallet.DB.MVar
import Prelude

import Cardano.Wallet.DB
( DBLayer (..) )
( DBLayer (..), ErrPutTxHistory (..), PrimaryKey (..) )
import Cardano.Wallet.Primitive.Model
( Wallet )
import Cardano.Wallet.Primitive.Types
( Hash, Tx, TxMeta, WalletId )
import Control.Concurrent.MVar
( modifyMVar_, newMVar, readMVar )
( modifyMVar, modifyMVar_, newMVar, readMVar )
import Control.DeepSeq
( deepseq )
import Control.Monad.Trans.Except
( ExceptT (..) )
import Data.Map.Strict
( Map )

import qualified Data.Map.Strict as Map


-- | Instantiate a new in-memory "database" layer that simply stores data in
-- a local MVar. Data vanishes if the software is shut down.
newDBLayer :: forall s. IO (DBLayer IO s)
newDBLayer = do
wallets <- newMVar mempty
db <- newMVar (mempty
:: Map (PrimaryKey WalletId) (Wallet s, Map (Hash "Tx") (Tx, TxMeta)))
return $ DBLayer
{ putCheckpoint = \key cp ->
cp `deepseq` (modifyMVar_ wallets (return . Map.insert key cp))
let
alter = \case
Nothing -> Just (cp, mempty)
Just (_, history) -> Just (cp, history)
in
cp `deepseq` modifyMVar_ db (return . (Map.alter alter key))

, readCheckpoint = \key ->
Map.lookup key <$> readMVar wallets
fmap fst . Map.lookup key <$> readMVar db
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so basically we are storing the pair (wallet state, txs) and keep it separated. And we have separate db calls for the first one and transactions part. And txs history is not polluting and affecting wallet state -> straightforward property to test


, readWallets =
Map.keys <$> readMVar wallets
Map.keys <$> readMVar db

, putTxHistory = \key@(PrimaryKey wid) txs' -> ExceptT $ do
let alter = \case
Nothing -> Left (ErrNoSuchWallet wid)
Just (cp, txs) -> Right (Just (cp, txs <> txs'))
let handle m = \case
Left err -> return (m, Left err)
Right m' -> return (m', Right ())
txs' `deepseq` modifyMVar db (\m -> handle m $ Map.alterF alter key m)

, readTxHistory = \key ->
maybe mempty snd . Map.lookup key <$> readMVar db
}
54 changes: 39 additions & 15 deletions src/Cardano/Wallet/Primitive/Model.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import Cardano.Wallet.Primitive.Types
( Block (..)
, Direction (..)
, Dom (..)
, Hash (..)
, IsOurs (..)
, SlotId (..)
, Tx (..)
Expand All @@ -77,6 +78,8 @@ import Data.Generics.Internal.VL.Lens
( (^.) )
import Data.Generics.Labels
()
import Data.Map.Strict
( Map )
import Data.Maybe
( catMaybes )
import Data.Quantity
Expand All @@ -89,6 +92,9 @@ import Numeric.Natural
import qualified Data.Map as Map
import qualified Data.Set as Set

{-------------------------------------------------------------------------------
Type
-------------------------------------------------------------------------------}

-- | An opaque wallet type, see @initWallet@ and @applyBlock@ to construct and
-- update wallets.
Expand All @@ -103,7 +109,7 @@ data Wallet s where
Wallet :: (IsOurs s, NFData s, Show s)
=> UTxO -- Unspent tx outputs belonging to this wallet
-> Set Tx -- Pending transactions
-> Set (Tx, TxMeta) -- Transaction history
-> Set (Hash "Tx") -- Transaction history
-> SlotId -- Latest applied block (current tip)
-> s -- Address discovery state
-> Wallet s
Expand All @@ -118,27 +124,44 @@ instance NFData (Wallet s) where
deepseq (rnf sl) $
deepseq (rnf s) ()

{-------------------------------------------------------------------------------
Construction & Modification
-------------------------------------------------------------------------------}

-- | Create an empty wallet from an initial state
initWallet
:: (IsOurs s, NFData s, Show s)
=> s
-> Wallet s
initWallet = Wallet mempty mempty mempty (SlotId 0 0)

-- | Apply Block is the only way to make the wallet evolve.
-- | Apply Block is the only way to make the wallet evolve. It returns a new
-- updated wallet state, as well as the set of all our transaction discovered
-- while applying the block.
applyBlock
:: Block
-> Wallet s
-> Wallet s
applyBlock !b (Wallet !utxo !pending !txs _ s) =
-> (Map (Hash "Tx") (Tx, TxMeta), Wallet s)
applyBlock !b (Wallet !utxo !pending !history _ s) =
let
-- Prefilter Block
((txs', utxo'), s') = prefilterBlock b utxo s
-- Prefilter Block / Update UTxO
((txs, utxo'), s') = prefilterBlock b utxo s
-- Update Pending
newIns = txIns (Set.map fst txs')
newIns = txIns (Set.map fst txs)
pending' = pending `pendingExcluding` newIns
-- Update Tx history
txs' = Map.fromList $ Set.toList $ Set.map
(\(tx, meta) -> (txId tx, (tx, meta)))
txs
history' = history <> Map.keysSet txs'
in
Wallet utxo' pending' (txs <> txs') (b ^. #header . #slotId) s'
( txs'
, Wallet utxo' pending' history' (b ^. #header . #slotId) s'
)

{-------------------------------------------------------------------------------
Accessors
-------------------------------------------------------------------------------}

-- | Get the wallet current tip
currentTip :: Wallet s -> SlotId
Expand All @@ -149,8 +172,8 @@ getState :: Wallet s -> s
getState (Wallet _ _ _ _ s) = s

-- | Get the transaction metadata for transactions associated with the wallet.
getTxHistory :: Wallet s -> Set (Tx, TxMeta)
getTxHistory (Wallet _ _ txs _ _) = txs
getTxHistory :: Wallet s -> Set (Hash "Tx")
getTxHistory (Wallet _ _ history _ _) = history

-- | Available balance = 'balance' . 'availableUTxO'
availableBalance :: Wallet s -> Natural
Expand All @@ -162,12 +185,12 @@ totalBalance :: Wallet s -> Natural
totalBalance =
balance . totalUTxO

-- | Available UTxO = UTxO that aren't part of pending txs
-- | Available UTxO = @pending ⋪ utxo@
availableUTxO :: Wallet s -> UTxO
availableUTxO (Wallet utxo pending _ _ _) =
utxo `excluding` txIns pending

-- | Total UTxO = 'availableUTxO' <> "pending UTxO"
-- | Total UTxO = 'availableUTxO' @<>@ 'changeUTxO'
totalUTxO :: Wallet s -> UTxO
totalUTxO wallet@(Wallet _ pending _ _ s) =
availableUTxO wallet <> changeUTxO pending s
Expand Down Expand Up @@ -256,9 +279,10 @@ changeUTxO pending = evalState $ do
let ins = txIns pending
return $ fold ourUtxo `restrictedBy` ins

-- | Construct a UTxO corresponding to a given transaction. It is important for
-- the transaction outputs to be ordered correctly, since they become available
-- inputs for the subsequent blocks.
-- | Construct our _next_ UTxO (possible empty) from a transaction by selecting
-- outputs that are ours. It is important for the transaction outputs to be
-- ordered correctly, since they become available inputs for the subsequent
-- blocks.
utxoOurs :: IsOurs s => Tx -> s -> (UTxO, s)
utxoOurs tx = runState $ toUtxo <$> forM (zip [0..] (outputs tx)) filterOut
where
Expand Down
16 changes: 1 addition & 15 deletions test/integration/Cardano/WalletSpec.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}

Expand All @@ -12,7 +11,7 @@ import Prelude
import Cardano.Launcher
( Command (..), StdStream (..), launch )
import Cardano.Wallet
( NewWallet (..), WalletLayer (..), mkWalletLayer )
( NewWallet (..), WalletLayer (..), mkWalletLayer, unsafeRunExceptT )
import Cardano.Wallet.Primitive.AddressDerivation
( Passphrase (..) )
import Cardano.Wallet.Primitive.Mnemonic
Expand All @@ -25,12 +24,6 @@ import Control.Concurrent
( threadDelay )
import Control.Concurrent.Async
( async, cancel )
import Control.Monad
( (>=>) )
import Control.Monad.Fail
( MonadFail )
import Control.Monad.Trans.Except
( ExceptT, runExceptT )
import Test.Hspec
( Spec, after, before, it, shouldSatisfy )

Expand Down Expand Up @@ -73,10 +66,3 @@ spec = do
(handle,) <$> (mkWalletLayer
<$> MVar.newDBLayer
<*> HttpBridge.newNetworkLayer "testnet" port)

unsafeRunExceptT :: (MonadFail m, Show e) => ExceptT e m a -> m a
unsafeRunExceptT = runExceptT >=> \case
Left e ->
fail $ "unable to perform expect IO action: " <> show e
Right a ->
return a
Loading