diff --git a/pkgs/default.nix b/pkgs/default.nix index 56e37aba3b1..ab4a1be39db 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -17579,6 +17579,7 @@ license = stdenv.lib.licenses.mit; , monad-control , MonadRandom , mtl +, pvss , QuickCheck , random , reflection @@ -17715,10 +17716,12 @@ lens log-warper MonadRandom mtl +pvss QuickCheck safe-exceptions safecopy serokell-util +servant servant-server stm universum @@ -17762,7 +17765,6 @@ license = stdenv.lib.licenses.mit; , cardano-sl-util , cardano-sl-util-test , cardano-sl-wallet -, cardano-sl-wallet-test , cassava , conduit , connection @@ -17774,8 +17776,10 @@ license = stdenv.lib.licenses.mit; , directory , exceptions , filepath +, foldl , formatting , gauge +, generic-arbitrary , generics-sop , hedgehog , hspec @@ -17874,7 +17878,6 @@ cardano-sl-networking cardano-sl-node-ipc cardano-sl-util cardano-sl-wallet -cardano-sl-wallet-test conduit connection containers @@ -17883,6 +17886,7 @@ data-default data-default-class directory exceptions +foldl formatting generics-sop http-api-data @@ -17997,9 +18001,9 @@ data-default directory filepath formatting +generic-arbitrary hedgehog hspec -ixset-typed lens log-warper mtl @@ -18030,7 +18034,6 @@ base bytestring cardano-sl-client cardano-sl-core -cardano-sl-db cardano-sl-wallet cassava connection diff --git a/wallet-new/cardano-sl-wallet-new.cabal b/wallet-new/cardano-sl-wallet-new.cabal index 38baa1770ff..26b2bf8b392 100755 --- a/wallet-new/cardano-sl-wallet-new.cabal +++ b/wallet-new/cardano-sl-wallet-new.cabal @@ -118,6 +118,7 @@ library Cardano.Wallet.Server.CLI Cardano.Wallet.Server.Plugins Cardano.Wallet.TypeLits + Cardano.Wallet.Types.UtxoStatistics Cardano.Wallet.Client Cardano.Wallet.Client.Http @@ -161,7 +162,6 @@ library , cardano-sl-node-ipc , cardano-sl-util , cardano-sl-wallet - , cardano-sl-wallet-test , conduit , connection , containers @@ -179,6 +179,7 @@ library , http-types , ixset-typed , json-sop + , foldl , lens , log-warper , memory @@ -485,7 +486,6 @@ test-suite wallet-unit-tests , data-default , formatting , hspec - , ixset-typed , lens , log-warper , mtl @@ -574,6 +574,7 @@ test-suite wallet-new-specs , formatting , hedgehog , hspec + , generic-arbitrary , lens , QuickCheck , quickcheck-instances @@ -620,7 +621,6 @@ benchmark cardano-sl-wallet-new-bench , bytestring , cardano-sl-client , cardano-sl-core - , cardano-sl-db , cardano-sl-wallet , cassava , connection diff --git a/wallet-new/integration/TransactionSpecs.hs b/wallet-new/integration/TransactionSpecs.hs index 3e1955d5e71..360e908f10c 100644 --- a/wallet-new/integration/TransactionSpecs.hs +++ b/wallet-new/integration/TransactionSpecs.hs @@ -8,13 +8,17 @@ import Universum import Cardano.Wallet.API.V1.Errors hiding (describe) import Cardano.Wallet.Client.Http import Control.Lens -import qualified Pos.Core as Core import Test.Hspec import Control.Concurrent (threadDelay) import Text.Show.Pretty (ppShow) import Util +import qualified Data.Map.Strict as Map +import qualified Pos.Core as Core +import qualified Pos.Core.Txp as Txp + + {-# ANN module ("HLint: ignore Reduce duplication" :: Text) #-} log :: MonadIO m => Text -> m () @@ -24,7 +28,7 @@ ppShowT :: Show a => a -> Text ppShowT = fromString . ppShow transactionSpecs :: WalletRef -> WalletClient IO -> Spec -transactionSpecs wRef wc = do +transactionSpecs wRef wc = describe "Transactions" $ do it "posted transactions appear in the index" $ do genesis <- genesisWallet wc @@ -187,3 +191,43 @@ transactionSpecs wRef wc = do etxn <- postTransaction wc payment void $ etxn `shouldPrism` _Left + + xit "posted transactions gives rise to nonempty Utxo histogram" $ do + genesis <- genesisWallet wc + (fromAcct, _) <- firstAccountAndId wc genesis + + wallet <- sampleWallet wRef wc + (_, toAddr) <- firstAccountAndId wc wallet + + let payment val = Payment + { pmtSource = PaymentSource + { psWalletId = walId genesis + , psAccountIndex = accIndex fromAcct + } + , pmtDestinations = pure PaymentDistribution + { pdAddress = addrId toAddr + , pdAmount = V1 (Core.mkCoin val) + } + , pmtGroupingPolicy = Nothing + , pmtSpendingPassword = Nothing + } + + eresp0 <- getUtxoStatistics wc (walId wallet) + utxoStatistics0 <- fmap wrData eresp0 `shouldPrism` _Right + let utxoStatistics0Expected = computeUtxoStatistics log10 [] + utxoStatistics0 `shouldBe` utxoStatistics0Expected + + void $ postTransaction wc (payment 1) + threadDelay 120000000 + + let txIn = Txp.TxInUnknown 0 "test" + let txOut = Txp.TxOutAux Txp.TxOut + { Txp.txOutAddress = unV1 (addrId toAddr) + , Txp.txOutValue = Core.mkCoin 1 + } + let utxos = [Map.fromList [(txIn, txOut)]] + + eresp <- getUtxoStatistics wc (walId wallet) + utxoStatistics <- fmap wrData eresp `shouldPrism` _Right + let utxoStatisticsExpected = computeUtxoStatistics log10 utxos + utxoStatistics `shouldBe` utxoStatisticsExpected diff --git a/wallet-new/integration/WalletSpecs.hs b/wallet-new/integration/WalletSpecs.hs index 5d881f88a08..87bfd85612c 100644 --- a/wallet-new/integration/WalletSpecs.hs +++ b/wallet-new/integration/WalletSpecs.hs @@ -15,7 +15,7 @@ import Util walletSpecs :: WalletRef -> WalletClient IO -> Spec -walletSpecs _ wc = do +walletSpecs _ wc = describe "Wallets" $ do it "Creating a wallet makes it available." $ do newWallet <- randomWallet CreateWallet @@ -53,6 +53,15 @@ walletSpecs _ wc = do } eresp `shouldPrism_` _Right + + it "creating wallet gives rise to an empty Utxo histogram" $ do + newWallet <- randomWallet CreateWallet + wallet <- createWalletCheck wc newWallet + + eresp <- getUtxoStatistics wc (walId wallet) + utxoStatistics <- fmap wrData eresp `shouldPrism` _Right + let utxoStatisticsExpected = computeUtxoStatistics log10 [] + utxoStatistics `shouldBe` utxoStatisticsExpected where testWalletAlreadyExists action = do newWallet1 <- randomWallet action diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Wallets.hs b/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Wallets.hs index f5a17e3b6dd..ce3e345c86e 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Wallets.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Handlers/Wallets.hs @@ -2,18 +2,18 @@ module Cardano.Wallet.API.V1.Handlers.Wallets where import Universum +import Servant + import Cardano.Wallet.API.Request import Cardano.Wallet.API.Response import Cardano.Wallet.API.V1.Types as V1 -import qualified Cardano.Wallet.API.V1.Wallets as Wallets - import Cardano.Wallet.WalletLayer (PassiveWalletLayer (..)) -import qualified Cardano.Wallet.WalletLayer.Types as WalletLayer +import qualified Cardano.Wallet.API.V1.Wallets as Wallets import qualified Cardano.Wallet.Kernel.DB.Util.IxSet as KernelIxSet +import qualified Cardano.Wallet.WalletLayer.Types as WalletLayer import qualified Data.IxSet.Typed as IxSet -import Servant -- | All the @Servant@ handlers for wallet-specific operations. handlers :: PassiveWalletLayer IO -> ServerT Wallets.API Handler @@ -23,7 +23,7 @@ handlers pwl = newWallet pwl :<|> deleteWallet pwl :<|> getWallet pwl :<|> updateWallet pwl - + :<|> getUtxoStatistics pwl -- | Creates a new or restores an existing @wallet@ given a 'NewWallet' payload. -- Returns to the client the representation of the created or restored @@ -97,3 +97,14 @@ updateWallet pwl wid walletUpdateRequest = do case res of Left e -> throwM e Right w -> return $ single w + +getUtxoStatistics + :: PassiveWalletLayer IO + -> WalletId + -> Handler (WalletResponse UtxoStatistics) +getUtxoStatistics pwl wid = do + res <- liftIO $ WalletLayer.getUtxos pwl wid + case res of + Left e -> throwM e + Right w -> + return $ single $ V1.computeUtxoStatistics V1.log10 (map snd w) diff --git a/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Wallets.hs b/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Wallets.hs index 7972e24f415..0e9f9b36610 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Wallets.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/LegacyHandlers/Wallets.hs @@ -32,6 +32,7 @@ import Pos.Wallet.Web.Methods.Logic (MonadWalletLogic, import Pos.Wallet.Web.Tracking.Types (SyncQueue) import Servant + -- | All the @Servant@ handlers for wallet-specific operations. handlers :: HasConfigurations => ServerT Wallets.API MonadV1 @@ -41,7 +42,7 @@ handlers = newWallet :<|> deleteWallet :<|> getWallet :<|> updateWallet - + :<|> getUtxoStatistics -- | Pure function which returns whether or not the underlying node is -- \"synced enough\" to allow wallet creation/restoration. The notion of @@ -185,3 +186,12 @@ updateWallet wid WalletUpdate{..} = do -- reacquire the snapshot because we did an update ws' <- V0.askWalletSnapshot addWalletInfo ws' updated + +-- | Gets Utxo statistics for a wallet. +-- | Stub, not calling data layer. +getUtxoStatistics + :: (MonadWalletLogic ctx m) + => WalletId + -> m (WalletResponse UtxoStatistics) +getUtxoStatistics _ = + return $ single (V1.computeUtxoStatistics V1.log10 []) diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs b/wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs index c61ca68fe56..33347f9809b 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Swagger.hs @@ -733,8 +733,8 @@ curl -X POST \ --cacert ./scripts/tls-files/ca.crt \ --cert ./scripts/tls-files/client.pem \ -d '{ - "walletId": "Ae2tdPwUPE...V3AVTnqGZ4", - "accountIndex": 2147483648 + "walletId": "Ae2tdPwUPE...V3AVTnqGZ4", + "accountIndex": 2147483648 }' ``` @@ -829,6 +829,24 @@ curl -X GET 'https://127.0.0.1:8090/api/v1/transactions?wallet_id=Ae2tdPwU...3AV --cert ./scripts/tls-files/client.pem ``` + +Getting Utxo statistics +--------------------------------- + +You can get Utxo statistics of a given wallet using + [`GET /api/v1/wallets/{{walletId}}/statistics/utxos`](#tag/Accounts%2Fpaths%2F~1api~1v1~1wallets~1{walletId}~1statistics~1utxos%2Fget) +``` +curl -X GET \ + https://127.0.0.1:8090/api/v1/wallets/Ae2tdPwUPE...8V3AVTnqGZ/statistics/utxos \ + -H 'Accept: application/json;charset=utf-8' \ + --cacert ./scripts/tls-files/ca.crt \ + --cert ./scripts/tls-files/client.pem +``` + +```json +$readUtxoStatistics +``` + Make sure to carefully read the section about [Pagination](#section/Pagination) to fully leverage the API capabilities. |] @@ -843,7 +861,7 @@ leverage the API capabilities. readFees = decodeUtf8 $ encodePretty $ genExample @(WalletResponse EstimatedFees) readNodeInfo = decodeUtf8 $ encodePretty $ genExample @(WalletResponse NodeInfo) readTransactions = decodeUtf8 $ encodePretty $ genExample @(WalletResponse [Transaction]) - + readUtxoStatistics = decodeUtf8 $ encodePretty $ genExample @(WalletResponse UtxoStatistics) -- | Provide an alternative UI (ReDoc) for rendering Swagger documentation. swaggerSchemaUIServer diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Types.hs b/wallet-new/src/Cardano/Wallet/API/V1/Types.hs index d7c05af42f1..4dba860b2f6 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Types.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Types.hs @@ -87,6 +87,9 @@ module Cardano.Wallet.API.V1.Types ( , CaptureAccountId -- * Core re-exports , Core.Address + + , module Cardano.Wallet.Types.UtxoStatistics + ) where import Universum @@ -129,6 +132,7 @@ import Cardano.Wallet.API.Types.UnitOfMeasure (MeasuredIn (..), import Cardano.Wallet.Kernel.DB.Util.IxSet (HasPrimKey (..), IndicesOf, OrdByPrimKey, ixFun, ixList) import Cardano.Wallet.Orphans.Aeson () +import Cardano.Wallet.Types.UtxoStatistics -- V0 logic import Pos.Util.Mnemonic (Mnemonic) @@ -852,6 +856,7 @@ instance BuildableSafeGen Wallet where instance Buildable [Wallet] where build = bprint listJson + -------------------------------------------------------------------------------- -- Addresses -------------------------------------------------------------------------------- diff --git a/wallet-new/src/Cardano/Wallet/API/V1/Wallets.hs b/wallet-new/src/Cardano/Wallet/API/V1/Wallets.hs index 19fa8559524..6b5901a2b13 100644 --- a/wallet-new/src/Cardano/Wallet/API/V1/Wallets.hs +++ b/wallet-new/src/Cardano/Wallet/API/V1/Wallets.hs @@ -37,4 +37,7 @@ type API = Tags '["Wallets"] :> :> Summary "Update the Wallet identified by the given walletId." :> ReqBody '[ValidJSON] (Update Wallet) :> Put '[ValidJSON] (WalletResponse Wallet) + :<|> "wallets" :> CaptureWalletId :> "statistics" :> "utxos" + :> Summary "Returns Utxo statistics for the Wallet identified by the given walletId." + :> Get '[ValidJSON] (WalletResponse UtxoStatistics) ) diff --git a/wallet-new/src/Cardano/Wallet/Client.hs b/wallet-new/src/Cardano/Wallet/Client.hs index eaf4b9bd48c..d2a7d46b85d 100644 --- a/wallet-new/src/Cardano/Wallet/Client.hs +++ b/wallet-new/src/Cardano/Wallet/Client.hs @@ -88,6 +88,8 @@ data WalletClient m :: WalletId -> Resp m Wallet , updateWallet :: WalletId -> Update Wallet -> Resp m Wallet + , getUtxoStatistics + :: WalletId -> Resp m UtxoStatistics -- account endpoints , deleteAccount :: WalletId -> AccountIndex -> m (Either ClientError ()) @@ -211,6 +213,8 @@ hoistClient phi wc = WalletClient phi . getWallet wc , updateWallet = \x -> phi . updateWallet wc x + , getUtxoStatistics = + phi . getUtxoStatistics wc , deleteAccount = \x -> phi . deleteAccount wc x , getAccount = diff --git a/wallet-new/src/Cardano/Wallet/Client/Http.hs b/wallet-new/src/Cardano/Wallet/Client/Http.hs index 386a6d8fe26..ca05548a219 100644 --- a/wallet-new/src/Cardano/Wallet/Client/Http.hs +++ b/wallet-new/src/Cardano/Wallet/Client/Http.hs @@ -105,6 +105,8 @@ mkHttpClient baseUrl manager = WalletClient = run . getWalletR , updateWallet = \x -> run . updateWalletR x + , getUtxoStatistics + = run . getUtxoStatisticsR -- account endpoints , deleteAccount = \x -> unNoContent . run . deleteAccountR x @@ -165,6 +167,7 @@ mkHttpClient baseUrl manager = WalletClient :<|> deleteWalletR :<|> getWalletR :<|> updateWalletR + :<|> getUtxoStatisticsR = walletsAPI deleteAccountR diff --git a/wallet-new/src/Cardano/Wallet/Types/UtxoStatistics.hs b/wallet-new/src/Cardano/Wallet/Types/UtxoStatistics.hs new file mode 100644 index 00000000000..7016febdabb --- /dev/null +++ b/wallet-new/src/Cardano/Wallet/Types/UtxoStatistics.hs @@ -0,0 +1,316 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE ViewPatterns #-} + +module Cardano.Wallet.Types.UtxoStatistics + ( -- * Types + UtxoStatistics + , BoundType + , UtxoStatisticsError(..) + + -- * Constructing 'UtxoStatistics' + , computeUtxoStatistics + + -- * Constructing 'BoundType' + , log10 + ) where + + +import Universum + +import Control.Lens (at, (?~)) +import Data.Aeson (FromJSON (..), Object, ToJSON (..), Value (..), + genericParseJSON, genericToJSON, object, withObject, (.:), + (.=)) +import Data.Aeson.Types (Parser) +import Data.Swagger (NamedSchema (..), Referenced (..), + SwaggerType (..), ToSchema (..), declareSchemaRef, + genericDeclareNamedSchema, minimum_, properties, required, + type_) +import Data.Word (Word64) +import Formatting (bprint, build, formatToString, (%)) +import Serokell.Util (listJson) +import Test.QuickCheck (Arbitrary (..), arbitrary, choose, elements, + infiniteListOf, shuffle) + +import Cardano.Wallet.API.V1.Swagger.Example (Example) +import Pos.Chain.Txp (Utxo) +import Pos.Core.Common (Coin (..)) +import Pos.Core.Txp (TxOut (..), TxOutAux (..)) +import Pos.Infra.Util.LogSafe (BuildableSafeGen (..), + deriveSafeBuildable) + +import qualified Control.Foldl as L +import qualified Data.Aeson as Aeson +import qualified Data.HashMap.Strict as HMS +import qualified Data.List.NonEmpty as NL +import qualified Data.Map.Strict as Map +import qualified Data.Swagger as Swagger +import qualified Formatting.Buildable + + +-- +-- TYPES +-- + +data UtxoStatistics = UtxoStatistics + { theHistogram :: ![HistogramBar] + , theAllStakes :: !Word64 + } deriving (Show, Generic, Ord) + +data UtxoStatisticsError + = ErrEmptyHistogram + | ErrInvalidBounds !Text + | ErrInvalidTotalStakes !Text + deriving (Eq, Show, Read, Generic) + +-- Buckets boundaries can be constructed in different ways +data BoundType = Log10 deriving (Eq, Show, Read, Generic) + +instance ToJSON BoundType where + toJSON = genericToJSON aesonEnumOpts + +instance FromJSON BoundType where + parseJSON = genericParseJSON aesonEnumOpts + +instance ToSchema BoundType where + declareNamedSchema = genericDeclareNamedSchema Swagger.defaultSchemaOptions + +instance Buildable UtxoStatisticsError where + build = \case + ErrEmptyHistogram -> + bprint "Utxo statistics histogram cannot be empty." + ErrInvalidBounds err -> + bprint ("Utxo statistics have invalid bounds: "%build%".") err + ErrInvalidTotalStakes err -> + bprint ("Utxo statistics have invalid total stakes: "%build%".") err + +instance Eq UtxoStatistics where + (UtxoStatistics h s) == (UtxoStatistics h' s') = + s == s' && sorted h == sorted h' + where + sorted :: [HistogramBar] -> [HistogramBar] + sorted = sortOn (\(HistogramBarCount key _) -> key) + +instance ToJSON UtxoStatistics where + toJSON (UtxoStatistics bars allStakes) = + let + histogramObject = + Object . HMS.fromList . map extractBarKey + + extractBarKey (HistogramBarCount bound stake) = + show bound .= stake + in + object + [ "histogram" .= histogramObject bars + , "allStakes" .= allStakes + , "boundType" .= log10 + ] + +instance FromJSON UtxoStatistics where + parseJSON = withObject "UtxoStatistics" parseUtxoStatistics + where + parseUtxoStatistics :: Object -> Parser UtxoStatistics + parseUtxoStatistics o = + eitherToParser =<< mkUtxoStatistics + <$> (o .: "boundType") + <*> (o .: "histogram") + <*> (o .: "allStakes") + + eitherToParser :: Buildable a => Either a b -> Parser b + eitherToParser = + either (fail . formatToString build) pure + +instance Arbitrary UtxoStatistics where + arbitrary = do + upperBounds <- shuffle (NL.toList $ generateBounds Log10) + counts <- infiniteListOf arbitrary + let histogram = zip upperBounds counts + let histoBars = map (uncurry HistogramBarCount) histogram + allStakes <- choose (getPossibleBounds $ Map.fromList histogram) + return $ UtxoStatistics histoBars allStakes + +instance BuildableSafeGen UtxoStatistics where + buildSafeGen _ UtxoStatistics{..} = bprint ("{" + %" histogram="%build + %" allStakes="%build + %" }") + theHistogram + theAllStakes + +instance Example UtxoStatistics + +instance ToSchema UtxoStatistics where + declareNamedSchema _ = do + wordRef <- declareSchemaRef (Proxy :: Proxy Word64) + btypeRef <- declareSchemaRef (Proxy :: Proxy BoundType) + pure $ NamedSchema (Just "UtxoStatistics") $ mempty + & type_ .~ SwaggerObject + & required .~ ["histogram", "allStakes"] + & properties .~ (mempty + & at "boundType" ?~ btypeRef + & at "allStakes" ?~ (Inline $ mempty + & type_ .~ SwaggerNumber + & minimum_ .~ Just 0 + ) + & at "histogram" ?~ Inline (mempty + & type_ .~ SwaggerObject + & properties .~ (mempty + & at "10" ?~ wordRef + & at "100" ?~ wordRef + & at "1000" ?~ wordRef + & at "10000" ?~ wordRef + & at "100000" ?~ wordRef + & at "1000000" ?~ wordRef + & at "10000000" ?~ wordRef + & at "100000000" ?~ wordRef + & at "1000000000" ?~ wordRef + & at "10000000000" ?~ wordRef + & at "100000000000" ?~ wordRef + & at "1000000000000" ?~ wordRef + & at "10000000000000" ?~ wordRef + & at "100000000000000" ?~ wordRef + & at "1000000000000000" ?~ wordRef + & at "10000000000000000" ?~ wordRef + & at "45000000000000000" ?~ wordRef + ) + ) + ) + +-- +-- CONSTRUCTING +-- + +-- | Smart-constructor to create bounds using a log-10 scale +log10 :: BoundType +log10 = Log10 +{-# INLINE log10 #-} + +-- | Compute UtxoStatistics from a bunch of UTXOs +computeUtxoStatistics :: BoundType -> [Utxo] -> UtxoStatistics +computeUtxoStatistics btype = + L.fold foldStatistics . concatMap getCoins + where + getCoins :: Utxo -> [Word64] + getCoins = + map (getCoin . txOutValue . toaOut) . Map.elems + + foldStatistics :: L.Fold Word64 UtxoStatistics + foldStatistics = UtxoStatistics + <$> foldBuckets (generateBounds btype) + <*> L.sum + + foldBuckets :: NonEmpty Word64 -> L.Fold Word64 [HistogramBar] + foldBuckets bounds = + let + step :: Map Word64 Word64 -> Word64 -> Map Word64 Word64 + step x a = + case Map.lookupGE a x of + Just (k, v) -> Map.insert k (v+1) x + Nothing -> Map.adjust (+1) (head bounds) x + initial :: Map Word64 Word64 + initial = + Map.fromList $ zip (NL.toList bounds) (repeat 0) + extract :: Map Word64 Word64 -> [HistogramBar] + extract = + map (uncurry HistogramBarCount) . Map.toList + in + L.Fold step initial extract + +-- +-- INTERNALS +-- + +-- Utxo statistics for the wallet. +-- Histogram is composed of bars that represent the bucket. The bucket is tagged by upper bound of a given bucket. +-- The bar value corresponds to the number of stakes +-- In the future the bar value could be different things: +-- (a) sum of stakes in a bucket +-- (b) avg or std of stake in a bucket +-- (c) topN buckets +-- to name a few +data HistogramBar = HistogramBarCount + { bucketUpperBound :: !Word64 + , bucketCount :: !Word64 + } deriving (Show, Eq, Ord, Generic) + +instance Example HistogramBar + +instance Arbitrary HistogramBar where + arbitrary = do + upperBound <- elements (NL.toList $ generateBounds log10) + count <- arbitrary + pure (HistogramBarCount upperBound count) + +instance Buildable [HistogramBar] where + build = + bprint listJson + +instance BuildableSafeGen HistogramBar where + buildSafeGen _ HistogramBarCount{..} = + bprint ("{" + %" upperBound="%build + %" count="%build + %" }") + bucketUpperBound + bucketCount + +mkUtxoStatistics + :: BoundType + -> Map Word64 Word64 + -> Word64 + -> Either UtxoStatisticsError UtxoStatistics +mkUtxoStatistics btype histogram allStakes = do + let (histoKeys, histoElems) = (Map.keys histogram, Map.elems histogram) + let acceptedKeys = NL.toList $ generateBounds btype + let (minPossibleValue, maxPossibleValue) = getPossibleBounds histogram + let constructHistogram = uncurry HistogramBarCount + let histoBars = map constructHistogram $ Map.toList histogram + + when (length histoKeys <= 0) $ + Left ErrEmptyHistogram + when (any (`notElem` acceptedKeys) histoKeys) $ + Left $ ErrInvalidBounds $ "given bounds are incompatible with bound type (" <> show btype <> ")" + when (any (< 0) histoElems) $ + Left $ ErrInvalidBounds "encountered negative bound" + when (allStakes < 0) $ + Left $ ErrInvalidTotalStakes "total stakes is negative" + when (allStakes < minPossibleValue && allStakes > maxPossibleValue) $ + Left $ ErrInvalidTotalStakes "inconsistent total stakes & histogram" + + pure UtxoStatistics + { theHistogram = histoBars + , theAllStakes = allStakes + } + +generateBounds :: BoundType -> NonEmpty Word64 +generateBounds bType = + let (^!) :: Word64 -> Word64 -> Word64 + (^!) = (^) + in case bType of + Log10 -> NL.fromList $ map (\toPower -> 10 ^! toPower) [1..16] ++ [45 * (10 ^! 15)] + +getPossibleBounds :: Map Word64 Word64 -> (Word64, Word64) +getPossibleBounds histogram = + (calculatePossibleBound fst, calculatePossibleBound snd) + where + createBracketPairs :: Num a => [a] -> [(a,a)] + createBracketPairs (reverse -> (x:xs)) = zip (map (+1) $ reverse (xs ++ [0])) (reverse (x:xs)) + createBracketPairs _ = [] + matching fromPair (key,value) = + map ( (*value) . fromPair ) . filter (\(_,upper) -> key == upper) + acceptedKeys = NL.toList $ generateBounds log10 + calculatePossibleBound fromPair = + sum . + concatMap (\pair -> matching fromPair pair $ createBracketPairs acceptedKeys) $ + Map.toList histogram + +aesonEnumOpts :: Aeson.Options +aesonEnumOpts = Aeson.defaultOptions + { Aeson.tagSingleConstructors = True + } + + +-- | TH at the end because it needs mostly everything to be declared first +deriveSafeBuildable ''UtxoStatistics +deriveSafeBuildable ''HistogramBar diff --git a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs index 1f3b2d02b24..ba26d984034 100644 --- a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs +++ b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs @@ -102,6 +102,11 @@ bracketPassiveWallet logFunction keystore rocksDB f = , _pwlCreateAddress = Addresses.createAddress wallet , _pwlGetAddresses = error "Not implemented!" + , _pwlGetUtxos = + \walletId -> do + snapshot <- liftIO (Kernel.getWalletSnapshot wallet) + return (Wallets.getWalletUtxos snapshot walletId) + , _pwlApplyBlocks = invokeIO . Actions.ApplyBlocks , _pwlRollbackBlocks = invokeIO . Actions.RollbackBlocks } diff --git a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Wallets.hs b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Wallets.hs index a53f9f46aa0..603d3c5c104 100644 --- a/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Wallets.hs +++ b/wallet-new/src/Cardano/Wallet/WalletLayer/Kernel/Wallets.hs @@ -5,6 +5,7 @@ module Cardano.Wallet.WalletLayer.Kernel.Wallets ( , deleteWallet , getWallet , getWallets + , getWalletUtxos ) where import Universum @@ -14,6 +15,7 @@ import Data.Coerce (coerce) import Data.Time.Units (Second) import Formatting (build, sformat) +import Pos.Chain.Txp (Utxo) import Pos.Core (decodeTextAddress, mkCoin) import Pos.Crypto.Signing @@ -25,7 +27,7 @@ import qualified Cardano.Wallet.Kernel.DB.HdWallet as HD import Cardano.Wallet.Kernel.DB.HdWallet.Read (readAllHdRoots, readHdRoot) import Cardano.Wallet.Kernel.DB.InDb (InDb (..), fromDb) -import Cardano.Wallet.Kernel.DB.Read (hdWallets) +import Cardano.Wallet.Kernel.DB.Read (accountUtxo, hdWallets) import Cardano.Wallet.Kernel.DB.Util.IxSet (IxSet) import qualified Cardano.Wallet.Kernel.DB.Util.IxSet as IxSet import Cardano.Wallet.Kernel.Types (WalletId (..)) @@ -33,9 +35,12 @@ import Cardano.Wallet.Kernel.Util.Core (getCurrentTimestamp) import qualified Cardano.Wallet.Kernel.Wallets as Kernel import Cardano.Wallet.WalletLayer.ExecutionTimeLimit (limitExecutionTimeTo) +import Cardano.Wallet.WalletLayer.Kernel.Accounts (getAccounts) import Cardano.Wallet.WalletLayer.Types (CreateWalletError (..), - DeleteWalletError (..), GetWalletError (..), - UpdateWalletError (..), UpdateWalletPasswordError (..)) + DeleteWalletError (..), GetUtxosError (..), + GetWalletError (..), UpdateWalletError (..), + UpdateWalletPasswordError (..)) + createWallet :: MonadIO m => Kernel.PassiveWallet @@ -159,6 +164,22 @@ getWallets db = let allRoots = readAllHdRoots (hdWallets db) in IxSet.fromList . map (toV1Wallet db) . IxSet.toList $ allRoots + +-- | Gets Utxos per account of a wallet. +getWalletUtxos :: Kernel.DB + -> V1.WalletId + -> Either GetUtxosError [(V1.Account, Utxo)] +getWalletUtxos db (V1.WalletId wId) = + case decodeTextAddress wId of + Left _ -> Left (GetWalletUtxosWalletIdDecodingFailed wId) + Right rootAddr -> do + case getAccounts db (V1.WalletId wId) of + Left accountsError -> Left (GetUtxosErrorFromGetAccountsError accountsError) + Right accountsIxSet -> + let hdRootId = HD.HdRootId . InDb $ rootAddr + hdAccountId accountIndex = HD.HdAccountId hdRootId (HD.HdAccountIx accountIndex) + in Right ( map (\acc -> (acc, accountUtxo db (hdAccountId $ V1.accIndex acc) ) ) $ IxSet.toList accountsIxSet ) + {------------------------------------------------------------------------------ General utility functions on the wallets. ------------------------------------------------------------------------------} @@ -196,4 +217,3 @@ toV1Wallet db hdRoot = fromV1AssuranceLevel :: V1.AssuranceLevel -> HD.AssuranceLevel fromV1AssuranceLevel V1.NormalAssurance = HD.AssuranceLevelNormal fromV1AssuranceLevel V1.StrictAssurance = HD.AssuranceLevelStrict - diff --git a/wallet-new/src/Cardano/Wallet/WalletLayer/Legacy.hs b/wallet-new/src/Cardano/Wallet/WalletLayer/Legacy.hs index 576be059aae..0ed125fc421 100644 --- a/wallet-new/src/Cardano/Wallet/WalletLayer/Legacy.hs +++ b/wallet-new/src/Cardano/Wallet/WalletLayer/Legacy.hs @@ -94,6 +94,8 @@ bracketPassiveWallet = , _pwlCreateAddress = pwlCreateAddress , _pwlGetAddresses = pwlGetAddresses + , _pwlGetUtxos = error "Method not implemented for legacy handler" + , _pwlApplyBlocks = pwlApplyBlocks , _pwlRollbackBlocks = pwlRollbackBlocks } diff --git a/wallet-new/src/Cardano/Wallet/WalletLayer/Types.hs b/wallet-new/src/Cardano/Wallet/WalletLayer/Types.hs index b544955d9ed..c99799df334 100644 --- a/wallet-new/src/Cardano/Wallet/WalletLayer/Types.hs +++ b/wallet-new/src/Cardano/Wallet/WalletLayer/Types.hs @@ -17,6 +17,7 @@ module Cardano.Wallet.WalletLayer.Types , createAddress , getAddresses + , getUtxos , applyBlocks , rollbackBlocks -- * Errors @@ -34,6 +35,7 @@ module Cardano.Wallet.WalletLayer.Types , GetAccountsError(..) , DeleteAccountError(..) , UpdateAccountError(..) + , GetUtxosError(..) ) where import qualified Prelude @@ -64,6 +66,7 @@ import Cardano.Wallet.Kernel.CoinSelection.FromGeneric (ExpenseRegulation, InputGrouping) import Pos.Chain.Block (Blund) +import Pos.Chain.Txp (Utxo) import Pos.Core (Coin) import Pos.Core.Chrono (NE, NewestFirst (..), OldestFirst (..)) import Pos.Core.Txp (Tx) @@ -120,6 +123,23 @@ instance Buildable GetWalletError where build (GetWalletWalletIdDecodingFailed txt) = bprint ("GetWalletWalletIdDecodingFailed " % build) txt +data GetUtxosError = + GetWalletUtxosWalletIdDecodingFailed Text + | GetUtxosErrorFromGetAccountsError GetAccountsError + deriving Eq + +instance Show GetUtxosError where + show = formatToString build + +instance Exception GetUtxosError + +instance Buildable GetUtxosError where + build (GetUtxosErrorFromGetAccountsError getAccountsError) = + bprint build getAccountsError + build (GetWalletUtxosWalletIdDecodingFailed txt) = + bprint ("GetWalletUtxosWalletIdDecodingFailed " % build) txt + + data UpdateWalletError = UpdateWalletError (V1 Kernel.UnknownHdRoot) | UpdateWalletErrorNotFound WalletId @@ -352,6 +372,8 @@ data PassiveWalletLayer m = PassiveWalletLayer , _pwlCreateAddress :: NewAddress -> m (Either CreateAddressError Address) , _pwlGetAddresses :: WalletId -> m [Address] + -- * utxos + , _pwlGetUtxos :: WalletId -> m (Either GetUtxosError [(Account, Utxo)]) -- * core API , _pwlApplyBlocks :: OldestFirst NE Blund -> m () , _pwlRollbackBlocks :: NewestFirst NE Blund -> m () @@ -430,6 +452,11 @@ createAddress pwl = pwl ^. pwlCreateAddress getAddresses :: forall m. PassiveWalletLayer m -> WalletId -> m [Address] getAddresses pwl = pwl ^. pwlGetAddresses +getUtxos :: forall m. PassiveWalletLayer m + -> WalletId + -> m (Either GetUtxosError [(Account, Utxo)]) +getUtxos pwl = pwl ^. pwlGetUtxos + applyBlocks :: forall m. PassiveWalletLayer m -> OldestFirst NE Blund -> m () applyBlocks pwl = pwl ^. pwlApplyBlocks diff --git a/wallet-new/test/Cardano/Wallet/WalletLayer/QuickCheck.hs b/wallet-new/test/Cardano/Wallet/WalletLayer/QuickCheck.hs index 77156990c30..132f4f8832e 100644 --- a/wallet-new/test/Cardano/Wallet/WalletLayer/QuickCheck.hs +++ b/wallet-new/test/Cardano/Wallet/WalletLayer/QuickCheck.hs @@ -13,15 +13,19 @@ import Cardano.Wallet.Orphans.Arbitrary () import Cardano.Wallet.WalletLayer.Types (ActiveWalletLayer (..), CreateAccountError (..), DeleteAccountError (..), DeleteWalletError (..), GetAccountError (..), - GetAccountsError (..), GetWalletError (..), - PassiveWalletLayer (..), UpdateAccountError (..), - UpdateWalletError (..), UpdateWalletPasswordError (..)) + GetAccountsError (..), GetUtxosError (..), + GetWalletError (..), PassiveWalletLayer (..), + UpdateAccountError (..), UpdateWalletError (..), + UpdateWalletPasswordError (..)) import Cardano.Wallet.API.V1.Types (V1 (..)) import qualified Cardano.Wallet.Kernel.Accounts as Kernel +import Pos.Core.Txp (TxIn (..), TxOut, TxOutAux) import Pos.Core () -import Test.QuickCheck (Arbitrary, arbitrary, generate, oneof) +import Test.QuickCheck (Arbitrary (..), arbitrary, choose, generate, + genericShrink, oneof, scale) +import Test.QuickCheck.Arbitrary.Generic (genericArbitrary) -- | Initialize the passive wallet. -- The passive wallet cannot send new transactions. @@ -51,6 +55,8 @@ bracketPassiveWallet = , _pwlCreateAddress = \_ -> liftedGen , _pwlGetAddresses = \_ -> liftedGen + , _pwlGetUtxos = \_ -> liftedGen + , _pwlApplyBlocks = \_ -> liftedGen , _pwlRollbackBlocks = \_ -> liftedGen } @@ -132,3 +138,24 @@ instance Arbitrary DeleteWalletError where instance Arbitrary UpdateWalletError where arbitrary = oneof [ UpdateWalletWalletIdDecodingFailed <$> arbitrary ] + + +instance Arbitrary GetUtxosError where + arbitrary = oneof [ GetWalletUtxosWalletIdDecodingFailed <$> arbitrary + , GetUtxosErrorFromGetAccountsError <$> arbitrary + ] + +instance Arbitrary TxIn where + arbitrary = oneof + [ TxInUtxo <$> arbitrary <*> arbitrary + , TxInUnknown <$> choose (1, 255) <*> scale (min 150) arbitrary + ] + shrink = genericShrink + +instance Arbitrary TxOutAux where + arbitrary = genericArbitrary + shrink = genericShrink + +instance Arbitrary TxOut where + arbitrary = genericArbitrary + shrink = genericShrink diff --git a/wallet/cardano-sl-wallet.cabal b/wallet/cardano-sl-wallet.cabal index 8e4c4af08e1..fda934cf959 100644 --- a/wallet/cardano-sl-wallet.cabal +++ b/wallet/cardano-sl-wallet.cabal @@ -236,8 +236,6 @@ test-suite cardano-wallet-test type: exitcode-stdio-1.0 build-depends: base - , MonadRandom - , QuickCheck , aeson , bytestring , cardano-crypto @@ -247,29 +245,33 @@ test-suite cardano-wallet-test , cardano-sl-core , cardano-sl-core-test , cardano-sl-crypto - , cardano-sl-crypto-test , cardano-sl-db , cardano-sl-generator , cardano-sl-infra , cardano-sl-util , cardano-sl-util-test , cardano-sl-wallet + , cardano-sl-crypto-test , containers + , safe-exceptions , data-default + , servant + , servant-server , deepseq , ekg-core , ether , formatting + , formatting , hspec , lens , log-warper + , MonadRandom , mtl - , safe-exceptions + , pvss + , QuickCheck , safecopy - , serokell-util >= 0.1.3.4 - , servant-server + , serokell-util , stm - , formatting , universum >= 0.1.11 , unordered-containers