diff --git a/lib/customer-deposit-wallet/customer-deposit-wallet.cabal b/lib/customer-deposit-wallet/customer-deposit-wallet.cabal index 3f27b8d8df0..0f6c89d729f 100644 --- a/lib/customer-deposit-wallet/customer-deposit-wallet.cabal +++ b/lib/customer-deposit-wallet/customer-deposit-wallet.cabal @@ -49,8 +49,8 @@ flag release manual: True library - import: language, opts-lib, no-delta-table-on-windows - hs-source-dirs: src + import: language, opts-lib, no-delta-table-on-windows + hs-source-dirs: src build-depends: , async , base @@ -68,11 +68,14 @@ library , delta-types , io-classes , microlens + , monoidal-containers , mtl + , mwc-random , OddWord + , random , text + , time - reexported-modules: Cardano.Wallet.Address.BIP32 exposed-modules: Cardano.Wallet.Deposit.IO Cardano.Wallet.Deposit.IO.DB @@ -80,7 +83,10 @@ library Cardano.Wallet.Deposit.IO.Network.Type Cardano.Wallet.Deposit.IO.Resource Cardano.Wallet.Deposit.IO.Resource.Event + Cardano.Wallet.Deposit.Map Cardano.Wallet.Deposit.Pure + Cardano.Wallet.Deposit.Pure.API.TxHistory + Cardano.Wallet.Deposit.Pure.API.TxHistory.Mock Cardano.Wallet.Deposit.Pure.Balance Cardano.Wallet.Deposit.Pure.Submissions Cardano.Wallet.Deposit.Pure.UTxO @@ -153,6 +159,7 @@ library rest , contra-tracer , crypto-primitives , customer-deposit-wallet + , customer-deposit-wallet-pure , delta-store , directory , filepath diff --git a/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST.hs b/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST.hs index 52312587546..b9c3b428149 100644 --- a/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST.hs +++ b/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST.hs @@ -36,7 +36,7 @@ module Cardano.Wallet.Deposit.REST , getWalletTip , availableBalance , getCustomerHistory - , getCustomerHistories + , getValueTransfers -- ** Writing to the blockchain , createPayment @@ -47,6 +47,7 @@ module Cardano.Wallet.Deposit.REST , deleteWallet , deleteTheDepositWalletOnDisk , customerAddress + , getValueTransfersWithTxIds ) where import Prelude @@ -110,6 +111,9 @@ import Data.ByteArray.Encoding import Data.List ( isPrefixOf ) +import Data.Map.Strict + ( Map + ) import Data.Store ( Store (..) , newStore @@ -129,7 +133,6 @@ import qualified Cardano.Wallet.Deposit.Read as Read import qualified Cardano.Wallet.Deposit.Write as Write import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL -import qualified Data.Map as Map {----------------------------------------------------------------------------- Types @@ -290,10 +293,10 @@ loadWallet bootEnv dir = do let action :: (WalletIO.WalletInstance -> IO b) -> IO (Either ErrDatabase b) action f = findTheDepositWalletOnDisk dir $ \case Right wallet -> - Right <$> - WalletIO.withWalletLoad + Right + <$> WalletIO.withWalletLoad (WalletIO.WalletEnv bootEnv wallet) - f + f Left e -> pure $ Left $ ErrLoadingDatabase e resource <- ask lift @@ -323,9 +326,9 @@ initXPubWallet tr bootEnv dir xpub users = do (WalletIO.WalletEnv bootEnv wallet) xpub users - $ \i -> do - ls <- WalletIO.listCustomers i - last ls `seq` f i + $ \i -> do + ls <- WalletIO.listCustomers i + last ls `seq` f i Nothing -> pure $ Left @@ -357,12 +360,12 @@ walletPublicIdentity = onWalletInstance WalletIO.walletPublicIdentity {----------------------------------------------------------------------------- Operations ------------------------------------------------------------------------------} + -- | List all tracked customers addresses. listCustomers :: WalletResourceM [(Customer, Address)] listCustomers = onWalletInstance WalletIO.listCustomers -- | Retrieve the address for a customer if it's tracked by the wallet. - customerAddress :: Customer -> WalletResourceM (Maybe Address) customerAddress = onWalletInstance . WalletIO.customerAddress @@ -378,13 +381,21 @@ availableBalance = onWalletInstance WalletIO.availableBalance getCustomerHistory :: Customer - -> WalletResourceM [Wallet.TxSummary] + -> WalletResourceM (Map Read.TxId Wallet.TxSummary) getCustomerHistory = onWalletInstance . WalletIO.getCustomerHistory -getCustomerHistories - :: (Read.ChainPoint, Read.ChainPoint) - -> WalletResourceM (Map.Map Customer Wallet.ValueTransfer) -getCustomerHistories = onWalletInstance . WalletIO.getCustomerHistories +getValueTransfers + :: WalletResourceM (Map Read.Slot (Map Address Wallet.ValueTransfer)) +getValueTransfers = onWalletInstance WalletIO.getValueTransfers + +getValueTransfersWithTxIds + :: WalletResourceM + ( Map + Read.Slot + (Map Address (Map Read.TxId Wallet.ValueTransfer)) + ) +getValueTransfersWithTxIds = + onWalletInstance WalletIO.getValueTransfersWithTxIds {----------------------------------------------------------------------------- Operations diff --git a/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST/Start.hs b/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST/Start.hs index abf752bcd70..e1fe2505a39 100644 --- a/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST/Start.hs +++ b/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST/Start.hs @@ -23,8 +23,14 @@ import Control.Monad.IO.Class ) import Control.Tracer ( Tracer + , stdoutTracer , traceWith ) +import Data.Functor.Contravariant + ( (>$<) + ) + +import qualified Cardano.Wallet.Deposit.Read as Read lg :: (MonadIO m, Show a) => Tracer IO String -> String -> a -> m () lg tr p x = liftIO $ traceWith tr $ p <> ": " <> show x @@ -46,10 +52,10 @@ loadDepositWalletFromDisk tr dir env resource = do Left e -> error $ show e Right _ -> pure () -fakeBootEnv :: WalletBootEnv m +fakeBootEnv :: MonadIO m => WalletBootEnv m fakeBootEnv = ( WalletBootEnv - (error "Not defined") - (error "Not defined") - (error "Not defined") + (show >$< stdoutTracer) + Read.mockGenesisDataMainnet + (error "network env not defined") ) diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO.hs index 6afc6930255..b1561adf75f 100644 --- a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO.hs +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO.hs @@ -1,36 +1,38 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE RecordWildCards #-} + module Cardano.Wallet.Deposit.IO - ( - -- * Types + ( -- * Types WalletEnv (..) , WalletBootEnv (..) , WalletPublicIdentity (..) , WalletInstance - -- * Operations - -- ** Initialization + -- * Operations + + -- ** Initialization , withWalletInit , Word31 , withWalletLoad - -- ** Mapping between customers and addresses + -- ** Mapping between customers and addresses , listCustomers , customerAddress - -- ** Reading from the blockchain + -- ** Reading from the blockchain , getWalletTip , availableBalance , getCustomerHistory - , getCustomerHistories + , getValueTransfers - -- ** Writing to the blockchain + -- ** Writing to the blockchain , createPayment , getBIP32PathsForOwnedInputs , signTxBody , WalletStore , walletPublicIdentity + , getValueTransfersWithTxIds ) where import Prelude @@ -43,12 +45,15 @@ import Cardano.Wallet.Address.BIP32 ) import Cardano.Wallet.Deposit.Pure ( Customer + , TxSummary + , ValueTransfer , WalletPublicIdentity (..) , WalletState , Word31 ) import Cardano.Wallet.Deposit.Read ( Address + , Slot ) import Cardano.Wallet.Network.Checkpoints.Policy ( defaultPolicy @@ -63,6 +68,9 @@ import Data.Bifunctor import Data.List.NonEmpty ( NonEmpty ) +import Data.Map.Strict + ( Map + ) import qualified Cardano.Wallet.Deposit.IO.Network.Type as Network import qualified Cardano.Wallet.Deposit.Pure as Wallet @@ -74,7 +82,6 @@ import qualified Data.Delta as Delta ( Replace (..) ) import qualified Data.Delta.Update as Delta -import qualified Data.Map.Strict as Map import qualified Data.Store as Store {----------------------------------------------------------------------------- @@ -84,25 +91,25 @@ import qualified Data.Store as Store -- | The environment needed to initialize a wallet, before a database is -- connected. data WalletBootEnv m = WalletBootEnv - { logger :: Tracer m WalletLog - -- ^ Logger for the wallet. - , genesisData :: Read.GenesisData - -- ^ Genesis data for the wallet. - , networkEnv :: Network.NetworkEnv m (Read.EraValue Read.Block) - -- ^ Network environment for the wallet. + { logger :: Tracer m WalletLog + -- ^ Logger for the wallet. + , genesisData :: Read.GenesisData + -- ^ Genesis data for the wallet. + , networkEnv :: Network.NetworkEnv m (Read.EraValue Read.Block) + -- ^ Network environment for the wallet. } -- | The wallet store type. type WalletStore = Store.UpdateStore IO Wallet.DeltaWalletState -- | The full environment needed to run a wallet. -data WalletEnv m = - WalletEnv - { bootEnv :: WalletBootEnv m - -- ^ The boot environment. - , store :: WalletStore - -- ^ The store for the wallet. - } +data WalletEnv m + = WalletEnv + { bootEnv :: WalletBootEnv m + -- ^ The boot environment. + , store :: WalletStore + -- ^ The store for the wallet. + } data WalletInstance = WalletInstance { env :: WalletEnv IO @@ -112,6 +119,7 @@ data WalletInstance = WalletInstance {----------------------------------------------------------------------------- Helpers ------------------------------------------------------------------------------} + -- | Convenience to apply an 'Update' to the 'WalletState' via the 'DBLayer'. onWalletState :: WalletInstance @@ -119,7 +127,8 @@ onWalletState -> IO r onWalletState WalletInstance{walletState} = Delta.onDBVar walletState - -- FIXME: Propagation of exceptions from Pure to IO. + +-- FIXME: Propagation of exceptions from Pure to IO. -- | Convenience to read the 'WalletState'. -- @@ -202,10 +211,12 @@ customerAddress c w = Wallet.customerAddress c <$> readWalletState w walletPublicIdentity :: WalletInstance -> IO WalletPublicIdentity walletPublicIdentity w = do state <- readWalletState w - pure $ WalletPublicIdentity - { pubXpub = Wallet.walletXPub state - , pubNextUser = Wallet.trackedCustomers state - } + pure + $ WalletPublicIdentity + { pubXpub = Wallet.walletXPub state + , pubNextUser = Wallet.trackedCustomers state + } + {----------------------------------------------------------------------------- Operations Reading from the blockchain @@ -218,16 +229,20 @@ availableBalance :: WalletInstance -> IO Read.Value availableBalance w = Wallet.availableBalance <$> readWalletState w -getCustomerHistory :: Customer -> WalletInstance -> IO [Wallet.TxSummary] +getCustomerHistory :: Customer -> WalletInstance -> IO (Map Read.TxId TxSummary) getCustomerHistory c w = Wallet.getCustomerHistory c <$> readWalletState w -getCustomerHistories - :: (Read.ChainPoint, Read.ChainPoint) - -> WalletInstance - -> IO (Map.Map Customer Wallet.ValueTransfer) -getCustomerHistories a w = - Wallet.getCustomerHistories a <$> readWalletState w +getValueTransfers + :: WalletInstance + -> IO (Map Slot (Map Address ValueTransfer)) +getValueTransfers w = Wallet.getValueTransfers <$> readWalletState w + +getValueTransfersWithTxIds + :: WalletInstance + -> IO (Map Slot (Map Address (Map Read.TxId ValueTransfer))) +getValueTransfersWithTxIds w = + Wallet.getValueTransfersWithTxIds <$> readWalletState w rollForward :: WalletInstance -> NonEmpty (Read.EraValue Read.Block) -> tip -> IO () @@ -265,3 +280,4 @@ signTxBody txbody w = Wallet.signTxBody txbody <$> readWalletState w ------------------------------------------------------------------------------} data WalletLog = WalletLogDummy + deriving Show diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO/Network/Mock.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO/Network/Mock.hs index a2a38da9445..1aa70b09ccc 100644 --- a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO/Network/Mock.hs +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO/Network/Mock.hs @@ -1,12 +1,17 @@ {-# LANGUAGE DuplicateRecordFields #-} -{-| -Copyright: © 2024 Cardano Foundation -License: Apache-2.0 -Mock implementation of a 'NetworkEnv'. --} +-- | +-- Copyright: © 2024 Cardano Foundation +-- License: Apache-2.0 +-- +-- Mock implementation of a 'NetworkEnv'. module Cardano.Wallet.Deposit.IO.Network.Mock ( newNetworkEnvMock + , unsafeUTCTimeOfSlot + , unsafeSlotsToUTCTimes + , unsafeSlotOfUTCTime + , originTime + , shelleyTime ) where import Prelude @@ -17,6 +22,11 @@ import Cardano.Wallet.Deposit.IO.Network.Type import Cardano.Wallet.Network ( ChainFollower (..) ) +import Cardano.Wallet.Read + ( Slot + , SlotNo (..) + , WithOrigin (..) + ) import Control.Concurrent.Class.MonadSTM ( MonadSTM , atomically @@ -29,6 +39,9 @@ import Control.Concurrent.Class.MonadSTM import Control.Monad ( forever ) +import Control.Monad.Class.MonadTime + ( UTCTime + ) import Control.Monad.Class.MonadTimer ( MonadDelay , threadDelay @@ -39,9 +52,24 @@ import Data.Foldable import Data.List.NonEmpty ( NonEmpty ((:|)) ) +import Data.Map.Strict + ( Map + ) +import Data.Maybe + ( maybeToList + ) +import Data.Set + ( Set + ) +import Data.Time.Clock.POSIX + ( posixSecondsToUTCTime + , utcTimeToPOSIXSeconds + ) import qualified Cardano.Wallet.Deposit.Read as Read import qualified Cardano.Wallet.Deposit.Write as Write +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set {----------------------------------------------------------------------------- Mock implementation of 'NetworkEnv' @@ -57,11 +85,11 @@ newNetworkEnvMock = do let registerAndUpdate follower = do _ <- rollBackward follower Read.GenesisPoint (chain, tip) <- atomically $ do - modifyTVar mfollowers (follower:) + modifyTVar mfollowers (follower :) (,) <$> readTVar mchain <*> readTVar mtip case reverse chain of [] -> pure () - (b:bs) -> rollForward follower (b :| bs) tip + (b : bs) -> rollForward follower (b :| bs) tip let forgeBlock tx = atomically $ do tipOld <- readTVar mtip @@ -69,7 +97,7 @@ newNetworkEnvMock = do blockNew = Read.mockNextBlock tipOld [txRead] tipNew = Read.getChainPoint blockNew writeTVar mtip tipNew - modifyTVar mchain (blockNew:) + modifyTVar mchain (blockNew :) pure (blockNew, tipNew) let broadcast block tip = do @@ -77,14 +105,56 @@ newNetworkEnvMock = do for_ followers $ \follower -> rollForward follower (block :| []) tip - pure NetworkEnv - { chainSync = \_ follower -> do - registerAndUpdate follower - forever $ threadDelay 1000000 - , postTx = \tx -> do - (block, tip) <- forgeBlock tx - broadcast block tip - -- brief delay to account for asynchronous chain followers - threadDelay 100 - pure $ Right () - } + pure + NetworkEnv + { chainSync = \_ follower -> do + registerAndUpdate follower + forever $ threadDelay 1000000 + , postTx = \tx -> do + (block, tip) <- forgeBlock tx + broadcast block tip + -- brief delay to account for asynchronous chain followers + threadDelay 100 + pure $ Right () + , slotsToUTCTimes = pure . unsafeSlotsToUTCTimes + , utcTimeToSlot = pure . Just . unsafeSlotOfUTCTime + } + +unsafeSlotsToUTCTimes :: Set Slot -> Map Slot (WithOrigin UTCTime) +unsafeSlotsToUTCTimes slots = + Map.fromList $ do + slot <- Set.toList slots + time <- maybeToList $ unsafeUTCTimeOfSlot slot + pure (slot, time) + +unsafeUTCTimeOfSlot :: Slot -> Maybe (WithOrigin UTCTime) +unsafeUTCTimeOfSlot Origin = Just Origin +unsafeUTCTimeOfSlot (At (SlotNo n)) = + Just . At + $ posixSecondsToUTCTime + $ fromIntegral pt + where + pts = fromIntegral n - byronSlots + pt = + if pts >= 0 + then shelleyTime + pts + else shelleyTime + pts * 20 + +unsafeSlotOfUTCTime :: UTCTime -> Read.Slot +unsafeSlotOfUTCTime t + | origin = Origin + | byron = At $ SlotNo $ fromIntegral $ (pt - originTime) `div` 20 + | otherwise = At $ SlotNo $ fromIntegral $ pt - shelleyTime + byronSlots + where + pt = floor $ utcTimeToPOSIXSeconds t + origin = pt < originTime + byron = pt < shelleyTime + +byronSlots :: Integer +byronSlots = 4924800 + +shelleyTime :: Integer +shelleyTime = 1596491091 + +originTime :: Integer +originTime = shelleyTime - byronSlots * 20 diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO/Network/Type.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO/Network/Type.hs index 0152156023b..7e21ed7299d 100644 --- a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO/Network/Type.hs +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO/Network/Type.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE NamedFieldPuns #-} + module Cardano.Wallet.Deposit.IO.Network.Type ( NetworkEnv (..) , mapBlock @@ -8,16 +9,29 @@ module Cardano.Wallet.Deposit.IO.Network.Type import Prelude +import Cardano.Wallet.Deposit.Read + ( Slot + , WithOrigin + ) import Cardano.Wallet.Network ( ChainFollower (..) , mapChainFollower ) +import Control.Monad.Class.MonadTime + ( UTCTime + ) import Control.Tracer ( Tracer ) import Data.List.NonEmpty ( NonEmpty ) +import Data.Map.Strict + ( Map + ) +import Data.Set + ( Set + ) import Data.Text ( Text ) @@ -40,12 +54,18 @@ data NetworkEnv m block = NetworkEnv :: Tracer m ChainFollowLog -> ChainFollower m Read.ChainPoint Read.ChainPoint (NonEmpty block) -> m Void - -- ^ Run the chain-sync mini-protocol (forever). - + -- ^ Run the chain-sync mini-protocol (forever). , postTx - :: Write.Tx -> m (Either ErrPostTx ()) - -- ^ Post a transaction to the Cardano network. - + :: Write.Tx + -> m (Either ErrPostTx ()) + -- ^ Post a transaction to the Cardano network. + , slotsToUTCTimes + :: Set Slot + -> m (Map Slot (WithOrigin UTCTime)) + -- ^ Try to convert a set of slots to their UTCTimes counterparts + , utcTimeToSlot + :: UTCTime + -> m (Maybe Slot) } mapBlock @@ -53,11 +73,14 @@ mapBlock => (block1 -> block2) -> NetworkEnv m block1 -> NetworkEnv m block2 -mapBlock f NetworkEnv{chainSync,postTx} = NetworkEnv - { chainSync = \tr follower -> - chainSync tr (mapChainFollower id id id (fmap f) follower) - , postTx = postTx - } +mapBlock f NetworkEnv{chainSync, postTx, slotsToUTCTimes, utcTimeToSlot} = + NetworkEnv + { chainSync = \tr follower -> + chainSync tr (mapChainFollower id id id (fmap f) follower) + , postTx = postTx + , slotsToUTCTimes = slotsToUTCTimes + , utcTimeToSlot = utcTimeToSlot + } {------------------------------------------------------------------------------- Errors @@ -76,7 +99,8 @@ data ErrPostTx -- | Higher level log of a chain follower. -- -- Includes computed statistics about synchronization progress. data ChainFollowLog - -- = MsgChainSync (ChainSyncLog BlockHeader ChainPoint) - -- | MsgFollowStats (FollowStats Rearview) - = MsgStartFollowing + = -- = MsgChainSync (ChainSyncLog BlockHeader ChainPoint) + + -- | MsgFollowStats (FollowStats Rearview) + MsgStartFollowing deriving (Eq, Show, Generic) diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Map.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Map.hs new file mode 100644 index 00000000000..383acbfebe1 --- /dev/null +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Map.hs @@ -0,0 +1,244 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Cardano.Wallet.Deposit.Map + ( Map (..) + , K + , W + , type (^^^) + , Lookup + , Key (..) + , singletonMap + , openMap + , singletonPatched + , unPatch + , withMap + , forMap + , withPatched + , lookup + , lookupPatched + , forPatched + , openPatched + , At (..) + , forgetPatch + ) where + +import Prelude hiding + ( lookup + ) + +import Data.Kind + ( Type + ) +import Data.Map.Monoidal.Strict + ( MonoidalMap (..) + ) + +import qualified Data.Map.Monoidal.Strict as MonoidalMap + +-- | Infix form of MonoidalMap type +type (^^^) = MonoidalMap + +infixr 5 ^^^ + +-- | A phantom type for tuples of mappings from 'k' tupled with a spurious monoid +-- 'w'. This is used to keep track of the patches applied to the map. +data W (w :: Type) (k :: Type) + +-- | A phantom type for mappings from 'k' +data K (k :: Type) + +-- | A nested monoidal map. Every nesting can also be patched with a monoid 'w'. +data Map :: [Type] -> Type -> Type where + Value :: v -> Map '[] v + Map :: k ^^^ Map ks v -> Map (K k ': ks) v + Patched :: w -> k ^^^ Map ks v -> Map (W w k ': ks) v + +deriving instance Show v => Show (Map '[] v) + +deriving instance + ( Show k + , Show v + , Show (Map ks v) + ) + => Show (Map (K k ': ks) v) + +deriving instance + ( Show w + , Show k + , Show (Map ks v) + ) + => Show (Map (W w k ': ks) v) + +deriving instance Eq v => Eq (Map '[] v) + +deriving instance + ( Eq k + , Eq v + , Eq (k ^^^ Map ks v) + ) + => Eq (Map (K k ': ks) v) + +deriving instance + ( Eq w + , Eq k + , Eq (Map ks v) + ) + => Eq (Map (W w k ': ks) v) + +deriving instance Functor (Map '[]) + +deriving instance Functor (Map ks) => Functor (Map (k ': ks)) + +instance Monoid v => Monoid (Map '[] v) where + mempty = Value mempty + +instance + ( Monoid (Map ks v) + , Ord k + ) + => Monoid (Map (K k : ks) v) + where + mempty = Map mempty + +instance + ( Monoid (Map ks v) + , Ord k + , Monoid w + ) + => Monoid (Map (W w k : ks) v) + where + mempty = Patched mempty mempty + +instance Semigroup v => Semigroup (Map '[] v) where + Value a <> Value b = Value (a <> b) + +instance + ( Monoid (Map ks v) + , Ord k + ) + => Semigroup (Map (K k : ks) v) + where + Map a <> Map b = Map (a <> b) + +instance + ( Ord x + , Semigroup (Map xs v) + , Semigroup w + ) + => Semigroup (Map (W w x : xs) v) + where + Patched w a <> Patched w' b = Patched (w <> w') (a <> b) + +instance Foldable (Map '[]) where + foldMap f (Value v) = f v + +instance (Foldable (Map xs), Ord x) => Foldable (Map (K x : xs)) where + foldMap f (Map m) = foldMap (foldMap f) m + +instance (Foldable (Map xs), Ord x) => Foldable (Map (W w x : xs)) where + foldMap f (Patched _ m) = foldMap (foldMap f) m + +-- | Push the patch down to the leaves of the map. +unPatch :: Functor (Map xs) => Map (W w x : xs) v -> Map (K x : xs) (w, v) +unPatch (Patched w m) = Map $ fmap (fmap (w,)) m + +-- | Forget the patch applied to the map. +forgetPatch :: Map (W w x : xs) v -> Map (K x : xs) v +forgetPatch (Patched _ m) = Map m + +-- | Open the map to access the underlying monoidal map. +openMap :: Map (K x : xs) v -> x ^^^ Map xs v +openMap (Map m) = m + +-- | Open the patched map to access the underlying monoidal map and the patch. +openPatched :: Map (W w x : xs) v -> (w, x ^^^ Map xs v) +openPatched (Patched w m) = (w, m) + +-- | Create a map with a single key-value pair. +singletonMap :: x -> Map xs v -> Map (K x : xs) v +singletonMap x m = Map $ MonoidalMap.singleton x m + +-- | Create a patched map with a single key-value pair. +singletonPatched :: w -> x -> Map xs v -> Map (W w x : xs) v +singletonPatched w x m = Patched w $ MonoidalMap.singleton x m + +-- | Destroy the underlying monoidal map. +withMap + :: Map (K k ': ks) a + -> (forall x. MonoidalMap k x -> b) + -> b +withMap (Map m) f = f m + +-- | Apply a function to the underlying monoidal map. +forMap + :: Map (K k ': ks) a + -> (forall x. MonoidalMap k x -> MonoidalMap k x) + -> Map (K k ': ks) a +forMap (Map m) f = Map (f m) + +-- | Destroy the underlying monoidal map and the patch. +withPatched + :: Map (W w k ': ks) a + -> (forall x. w -> MonoidalMap k x -> b) + -> b +withPatched (Patched w m) f = f w m + +-- | Apply a function to the underlying monoidal map and the patch. +forPatched + :: Map (W w k ': ks) a + -> (forall x. w -> MonoidalMap k x -> MonoidalMap k x) + -> Map (W w k ': ks) a +forPatched (Patched w m) f = Patched w (f w m) + +-- | Lookup a value in first layer of the map. +lookup :: (Ord k) => k -> Map (K k : ks) a -> Maybe (Map ks a) +lookup k (Map m) = MonoidalMap.lookup k m + +-- | Lookup a value in first layer of the map and return the patch as well. +lookupPatched :: (Ord k) => k -> Map (W w k : ks) a -> Maybe (w, Map ks a) +lookupPatched k (Patched w m) = (w,) <$> MonoidalMap.lookup k m + +-- | A key to access a value at some depth in the map. It's also a witness of +-- ks being a prefix of rs. +data Key ks rs where + KeyK :: k -> Key ks rs -> Key (K k ': ks) (K k ': rs) + KeyW :: k -> Key ks rs -> Key (W w k ': ks) (W w k ': rs) + LastK :: Key '[] rs + +-- | Compute the type of a lookup at some depth in the map. The type is lossless +-- so it carries the patched semigroups as well. +type family Lookup xs ys a :: Type where + Lookup '[] ys a = Map ys a + Lookup (K x ': xs) (K x : ys) a = Lookup xs ys a + Lookup (W w x ': xs) (W w x : ys) a = (w, Lookup xs ys a) + +-- | A class to access a value at some depth in the map. +class At ks rs where + at + :: Key ks rs + -- ^ The key list to access the value. + -> Map rs a + -- ^ The map to access the value from. + -> Maybe (Lookup ks rs a) + -- ^ The value at the given key list. + +instance At '[] rs where + at LastK = Just + +instance (Ord k, At ks rs) => At (K k ': ks) (K k ': rs) where + at (KeyK k ks) m = lookup k m >>= at ks + +instance (Ord k, At ks rs) => At (W w k ': ks) (W w k ': rs) where + at (KeyW k ks) m = lookupPatched k m >>= \(w, m') -> (w,) <$> at ks m' diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure.hs index f1ed84409a4..412bedec69e 100644 --- a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure.hs +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure.hs @@ -1,13 +1,14 @@ {-# LANGUAGE DuplicateRecordFields #-} + module Cardano.Wallet.Deposit.Pure - ( - -- * Types + ( -- * Types WalletState , DeltaWalletState , WalletPublicIdentity (..) - -- * Operations - -- ** Mapping between customers and addresses + -- * Operations + + -- ** Mapping between customers and addresses , Customer , listCustomers , deriveAddress @@ -19,7 +20,7 @@ module Cardano.Wallet.Deposit.Pure , trackedCustomers , walletXPub - -- ** Reading from the blockchain + -- ** Reading from the blockchain , fromXPubAndGenesis , Word31 , getWalletTip @@ -27,24 +28,23 @@ module Cardano.Wallet.Deposit.Pure , rollForwardMany , rollForwardOne , rollBackward - , TxSummary (..) , ValueTransfer (..) , getCustomerHistory - , getCustomerHistories + , getValueTransfers - -- ** Writing to the blockchain + -- ** Writing to the blockchain , createPayment , BIP32Path (..) , DerivationType (..) , getBIP32PathsForOwnedInputs , signTxBody - , addTxSubmission , listTxsInSubmission - -- * Internal, for testing + -- * Internal, for testing , availableUTxO + , getValueTransfersWithTxIds ) where import Prelude @@ -78,8 +78,15 @@ import Data.List.NonEmpty import Data.Map.Strict ( Map ) +import Data.Maps.PairMap + ( PairMap (..) + ) +import Data.Maps.Timeline + ( Timeline (eventsByTime) + ) import Data.Maybe ( mapMaybe + , maybeToList ) import Data.Set ( Set @@ -92,11 +99,13 @@ import qualified Cardano.Wallet.Deposit.Pure.Address as Address import qualified Cardano.Wallet.Deposit.Pure.Balance as Balance import qualified Cardano.Wallet.Deposit.Pure.RollbackWindow as Rollback import qualified Cardano.Wallet.Deposit.Pure.Submissions as Sbm +import qualified Cardano.Wallet.Deposit.Pure.TxHistory as TxHistory import qualified Cardano.Wallet.Deposit.Pure.UTxO as UTxO import qualified Cardano.Wallet.Deposit.Pure.UTxO.UTxOHistory as UTxOHistory import qualified Cardano.Wallet.Deposit.Read as Read import qualified Cardano.Wallet.Deposit.Write as Write import qualified Data.Delta as Delta +import qualified Data.Map.Strict as Map import qualified Data.Set as Set {----------------------------------------------------------------------------- @@ -108,7 +117,7 @@ data WalletState = WalletState { walletTip :: Read.ChainPoint , addresses :: !Address.AddressState , utxoHistory :: !UTxOHistory.UTxOHistory - -- , txHistory :: [Read.Tx] + , txHistory :: !TxHistory.TxHistory , submissions :: Sbm.TxSubmissions , rootXSignKey :: Maybe XPrv -- , info :: !WalletInfo @@ -120,7 +129,7 @@ data WalletPublicIdentity = WalletPublicIdentity { pubXpub :: XPub , pubNextUser :: Word31 } - deriving Show + deriving (Show) {----------------------------------------------------------------------------- Operations @@ -178,6 +187,7 @@ fromXPubAndGenesis xpub knownCustomerCount genesisData = , addresses = Address.fromXPubAndCount network xpub knownCustomerCount , utxoHistory = UTxOHistory.empty initialUTxO + , txHistory = TxHistory.empty , submissions = Sbm.empty , rootXSignKey = Nothing } @@ -206,11 +216,14 @@ rollForwardOne (Read.EraValue block) w = rollForwardUTxO :: Read.IsEra era - => (Address -> Bool) -> Read.Block era -> UTxOHistory -> UTxOHistory + => (Address -> Bool) + -> Read.Block era + -> UTxOHistory + -> UTxOHistory rollForwardUTxO isOurs block u = UTxOHistory.rollForward slot deltaUTxO u where - (deltaUTxO,_) = Balance.applyBlock isOurs block (UTxOHistory.getUTxO u) + (deltaUTxO, _) = Balance.applyBlock isOurs block (UTxOHistory.getUTxO u) slot = Read.getEraSlotNo $ Read.getEraBHeader block rollBackward @@ -238,9 +251,9 @@ rollBackward targetPoint w = -- any other point than the target point (or genesis). actualPoint = if (targetSlot `Rollback.member` UTxOHistory.getRollbackWindow h) - -- FIXME: Add test for rollback window of `submissions` - then targetPoint - else Read.GenesisPoint + -- FIXME: Add test for rollback window of `submissions` + then targetPoint + else Read.GenesisPoint availableBalance :: WalletState -> Read.Value availableBalance = UTxO.balance . availableUTxO @@ -252,16 +265,29 @@ availableUTxO w = pending = listTxsInSubmission w utxo = UTxOHistory.getUTxO $ utxoHistory w -getCustomerHistory :: Customer -> WalletState -> [TxSummary] -getCustomerHistory = undefined +getCustomerHistory :: Customer -> WalletState -> Map Read.TxId TxSummary +getCustomerHistory c state = + case customerAddress c state of + Nothing -> mempty + Just addr -> TxHistory.getAddressHistory addr (txHistory state) -- TODO: Return an error if any of the `ChainPoint` are no longer -- part of the consensus chain? -getCustomerHistories - :: (Read.ChainPoint, Read.ChainPoint) - -> WalletState - -> Map Customer ValueTransfer -getCustomerHistories = undefined +getValueTransfers :: WalletState -> Map Read.Slot (Map Address ValueTransfer) +getValueTransfers state = TxHistory.getValueTransfers (txHistory state) + +getValueTransfersWithTxIds + :: WalletState + -> Map Read.Slot (Map Address (Map Read.TxId ValueTransfer)) +getValueTransfersWithTxIds state = + restrictByTxId <$> eventsByTime (TxHistory.txIds history) + where + history = txHistory state + restrictByTxId :: Set Read.TxId -> Map Address (Map Read.TxId ValueTransfer) + restrictByTxId txIds = Map.unionsWith (<>) $ do + txId <- Set.toList txIds + x <- maybeToList $ Map.lookup txId $ mab (TxHistory.txTransfers history) + pure $ fmap (Map.singleton txId) x {----------------------------------------------------------------------------- Operations @@ -270,20 +296,21 @@ getCustomerHistories = undefined createPayment :: [(Address, Write.Value)] -> WalletState -> Maybe Write.TxBody createPayment = undefined - -- needs balanceTx - -- needs to sign the transaction + +-- needs balanceTx +-- needs to sign the transaction getBIP32PathsForOwnedInputs :: Write.TxBody -> WalletState -> [BIP32Path] getBIP32PathsForOwnedInputs txbody w = getBIP32Paths w - . resolveInputAddresses - $ Write.spendInputs txbody <> Write.collInputs txbody + . resolveInputAddresses + $ Write.spendInputs txbody <> Write.collInputs txbody where resolveInputAddresses :: Set Read.TxIn -> [Read.Address] resolveInputAddresses ins = map (Read.address . snd) - . UTxO.toList - $ UTxO.restrictedBy (availableUTxO w) ins + . UTxO.toList + $ UTxO.restrictedBy (availableUTxO w) ins getBIP32Paths :: WalletState -> [Read.Address] -> [BIP32Path] getBIP32Paths w = diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure/API/TxHistory.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure/API/TxHistory.hs new file mode 100644 index 00000000000..048abd413a6 --- /dev/null +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure/API/TxHistory.hs @@ -0,0 +1,134 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} + +module Cardano.Wallet.Deposit.Pure.API.TxHistory + where + +import Prelude + +import Cardano.Wallet.Deposit.Map + ( K + , Map (..) + , W + , singletonMap + , singletonPatched + ) +import Cardano.Wallet.Deposit.Pure + ( Customer + , ValueTransfer + ) +import Cardano.Wallet.Deposit.Read + ( Address + ) +import Cardano.Wallet.Read + ( Slot + , TxId + , WithOrigin + ) +import Data.Foldable + ( Foldable (..) + ) +import Data.Maps.PairMap + ( PairMap (..) + ) +import Data.Maps.Timeline + ( Timeline (..) + ) +import Data.Maybe + ( maybeToList + ) +import Data.Monoid + ( First (..) + ) +import Data.Ord + ( Down (..) + ) +import Data.Time + ( UTCTime + ) + +import qualified Cardano.Wallet.Deposit.Pure.TxHistory as H +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set + +firstJust :: a -> First a +firstJust = First . Just + +transfers :: Foldable (Map xs) => Map xs ValueTransfer -> ValueTransfer +transfers = fold + +type DownTime = Down (WithOrigin UTCTime) + +type ByCustomer = + Map + '[ K Customer + , W (First Address) DownTime + , W (First Slot) TxId + ] + ValueTransfer + +type ByTime = + Map + '[ K DownTime + , W (First Slot) Customer + , W (First Address) TxId + ] + ValueTransfer + +data TxHistory = TxHistory + { byCustomer :: ByCustomer + , bySlot :: ByTime + } + +inefficientlyMkTxHistory + :: ResolveAddress + -> ResolveSlot + -> H.TxHistory + -> TxHistory +inefficientlyMkTxHistory resolveAddress resolveSlot h = + TxHistory + { byCustomer = inefficientByCustomer resolveAddress resolveSlot h + , bySlot = inefficientBySlot resolveAddress resolveSlot h + } + +type ResolveAddress = Address -> Maybe Customer +type ResolveSlot = Slot -> Maybe DownTime + +inefficientBySlot + :: ResolveAddress + -> ResolveSlot + -> H.TxHistory + -> ByTime +inefficientBySlot resolveAddress resolveSlots history = fold $ do + (slot, txIds) <- Map.assocs $ eventsByTime (H.txIds history) + time <- maybeToList $ resolveSlots slot + txId <- Set.toList txIds + addressToTransaction <- + maybeToList + $ Map.lookup txId + $ mab (H.txTransfers history) + (address, transaction) <- Map.assocs addressToTransaction + customer <- maybeToList $ resolveAddress address + pure + $ singletonMap time + $ singletonPatched (firstJust slot) customer + $ singletonPatched (firstJust address) txId + $ Value transaction + +inefficientByCustomer + :: ResolveAddress + -> ResolveSlot + -> H.TxHistory + -> ByCustomer +inefficientByCustomer resolveAddress resolveSlot history = fold $ do + (a, txIds) <- Map.assocs $ mba $ H.txTransfers history + customer <- maybeToList $ resolveAddress a + (txId, (slot, value)) <- + Map.assocs + $ Map.intersectionWith (,) (events (H.txIds history)) txIds + time <- maybeToList $ resolveSlot slot + pure + $ singletonMap customer + $ singletonPatched (firstJust a) time + $ singletonPatched (firstJust slot) txId + $ Value value diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure/API/TxHistory/Mock.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure/API/TxHistory/Mock.hs new file mode 100644 index 00000000000..f6075d7cab2 --- /dev/null +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure/API/TxHistory/Mock.hs @@ -0,0 +1,146 @@ +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Wallet.Deposit.Pure.API.TxHistory.Mock + ( mockTxHistoryByTime + ) +where + +import Prelude + +import Cardano.Wallet.Deposit.IO.Network.Mock + ( unsafeSlotOfUTCTime + ) +import Cardano.Wallet.Deposit.Map + ( Map (..) + , singletonMap + , singletonPatched + ) +import Cardano.Wallet.Deposit.Pure + ( ValueTransfer (..) + ) +import Cardano.Wallet.Deposit.Pure.API.TxHistory + ( ByTime + , ResolveAddress + , ResolveSlot + ) +import Cardano.Wallet.Deposit.Read + ( Address + , Value + , WithOrigin (..) + ) +import Cardano.Wallet.Read + ( Coin + , SlotNo (..) + , TxId + , Value (..) + , txIdFromHash + ) +import Cardano.Wallet.Read.Hash + ( hashFromStringAsHex + ) +import Control.Monad + ( replicateM + ) +import Data.Maybe + ( fromJust + ) +import Data.Monoid + ( First (..) + ) +import Data.Time + ( UTCTime (..) + ) +import System.Random.MWC.Distributions + ( standard + ) +import System.Random.Stateful + ( StatefulGen + , UniformRange (uniformRM) + , mkStdGen + , runStateGen_ + ) + +mockTxHistoryByTime + :: UTCTime + -- ^ Current time. + -> ResolveAddress + -- ^ Compute a customer from an address. + -> ResolveSlot + -- ^ Compute a time from a slot. + -> [Address] + -- ^ List of addresses to use. + -> Int + -- ^ Number of deposits to create. + -> ByTime +mockTxHistoryByTime now solveAddress solveSlot addresses ns = + runStateGen_ (mkStdGen 0) $ \g -> do + fmap mconcat + $ replicateM ns + $ do + slot <- case unsafeSlotOfUTCTime now of + Origin -> pure Origin + At (SlotNo n) -> do + slotInt <- + floor + . (fromIntegral n -) + . (* fromIntegral n) + . (abs) + <$> standard g + pure + $ if slotInt < 0 + then Origin + else At (SlotNo $ fromIntegral @Integer slotInt) + addressNumber <- uniformRM (0, length addresses - 1) g + let address = addresses !! addressNumber + value <- valueTransferG g + txId <- txIdR g + let customer = case solveAddress address of + Just c -> c + Nothing -> error "fakeDepositsCreate: address not found" + let time = case solveSlot slot of + Just t -> t + Nothing -> error "fakeDepositsCreate: slot not found" + pure + $ singletonMap time + $ singletonPatched (First $ Just slot) customer + $ singletonPatched (First $ Just address) txId + $ Value value + +txIdR :: StatefulGen g m => g -> m TxId +txIdR g = do + ls <- + fmap (concatMap $ replicate 8) + $ replicateM 8 + $ hexOfInt <$> uniformRM (0, 15) g + pure $ unsafeMkTxId ls + +valueTransferG :: StatefulGen g m => g -> m ValueTransfer +valueTransferG g = do + spentOrReceived <- uniformRM (0, 11) g + spent <- createSpent g spentOrReceived + received <- createReceived g spentOrReceived + pure $ ValueTransfer{..} + +unsafeMkTxId :: String -> TxId +unsafeMkTxId = txIdFromHash . fromJust . hashFromStringAsHex + +hexOfInt :: Int -> Char +hexOfInt n = "0123456789abcdef" !! (n `mod` 16) + +randomValue :: StatefulGen g f => g -> Coin -> f Value +randomValue g l = ValueC <$> uniformRM (0, l) g <*> pure mempty + +maxLovelaces :: Coin +maxLovelaces = 1_000_000_000 + +createSpent :: StatefulGen g f => g -> Int -> f Value +createSpent g r = randomValue g l + where + l = if r >= 0 && r < 5 || r == 11 then maxLovelaces else 0 + +createReceived :: StatefulGen g f => g -> Int -> f Value +createReceived g r = randomValue g l + where + l = if r >= 5 && r <= 11 then maxLovelaces else 0 diff --git a/lib/customer-deposit-wallet/test/scenario/Test/Scenario/Wallet/Deposit/Exchanges.lhs.md b/lib/customer-deposit-wallet/test/scenario/Test/Scenario/Wallet/Deposit/Exchanges.lhs.md index 25aead0bb0e..0e9700d1d81 100644 --- a/lib/customer-deposit-wallet/test/scenario/Test/Scenario/Wallet/Deposit/Exchanges.lhs.md +++ b/lib/customer-deposit-wallet/test/scenario/Test/Scenario/Wallet/Deposit/Exchanges.lhs.md @@ -48,6 +48,10 @@ import Test.Scenario.Blockchain , signTx , submitTx ) +import Data.Foldable + ( toList + , fold + ) import qualified Cardano.Wallet.Deposit.IO as Wallet import qualified Data.Map as Map @@ -60,6 +64,12 @@ depositFundsAt :: ScenarioEnv -> Address -> Value -> IO () depositFundsAt env address value = payFromFaucet env [(address, value)] ``` +We ignore the mapping from TxId when retrieving the customer history +```haskell +getCustomerHistory :: Customer -> WalletInstance -> IO [TxSummary] +getCustomerHistory customer w = toList <$> Wallet.getCustomerHistory customer w +``` + ## 0. Start a Wallet A `WalletInstance` denotes a mutable wallet that is actively synchronizing to the blockchain, continuously writes its state to a database file, and responds to queries. @@ -126,17 +136,17 @@ scenarioTrackDepositOne env w = do Just address <- Wallet.customerAddress customer w -- no deposits - txsummaries0<- Wallet.getCustomerHistory customer w + txsummaries0 <- getCustomerHistory customer w assert $ null txsummaries0 -- first deposit depositFundsAt env address coin - txsummaries1 <- Wallet.getCustomerHistory customer w + txsummaries1 <- getCustomerHistory customer w assert $ map (received . txTransfer) txsummaries1 == [coin] -- second deposit depositFundsAt env address coin - txsummaries2 <- Wallet.getCustomerHistory customer w + txsummaries2 <- getCustomerHistory customer w assert $ map (received . txTransfer) txsummaries2 == [coin, coin] where customer = 7 :: Customer @@ -161,19 +171,17 @@ scenarioTrackDepositAll env w = do Just address1 <- Wallet.customerAddress customer1 w Just address2 <- Wallet.customerAddress customer2 w - from <- Wallet.getWalletTip w depositFundsAt env address1 coin depositFundsAt env address2 coin depositFundsAt env address1 (coin <> coin) - to <- Wallet.getWalletTip w - history <- Wallet.getCustomerHistories (from,to) w + history <- fold <$> Wallet.getValueTransfers w assert $ Map.map received history == Map.fromList - [ (customer1, coin <> coin <> coin) - , (customer2, coin) + [ (address1, coin <> coin <> coin) + , (address2, coin) ] where customer1, customer2 :: Customer @@ -209,7 +217,7 @@ scenarioCreatePayment xprv env destination w = do assert $ value2 <> coin == value1 -- but the original deposit amount is still recorded - txsummaries <- Wallet.getCustomerHistory customer w + txsummaries <- getCustomerHistory customer w assert $ value1 `elem` map (received . txTransfer) txsummaries where customer :: Customer