From df685aa906b6b27378c48df33621ffd68f124a71 Mon Sep 17 00:00:00 2001 From: devfull <26844641+devfull@users.noreply.github.com> Date: Tue, 5 Jul 2022 17:20:01 +0200 Subject: [PATCH] [consolidation] add MultiAsset --- src/Tokenomia/Common/Arbitrary/AssetClass.hs | 31 +++++++ src/Tokenomia/Common/Arbitrary/Builtins.hs | 22 +++++ src/Tokenomia/Common/Arbitrary/Modifiers.hs | 8 ++ src/Tokenomia/Common/Arbitrary/MultiAsset.hs | 50 +++++++++++ src/Tokenomia/Common/Arbitrary/Value.hs | 65 ++++++++++++++ src/Tokenomia/Common/MultiAsset.hs | 89 ++++++++++++++++++++ test/Spec.hs | 2 + test/Spec/Tokenomia/Common/MultiAsset.hs | 44 ++++++++++ tokenomia.cabal | 11 ++- 9 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 src/Tokenomia/Common/Arbitrary/AssetClass.hs create mode 100644 src/Tokenomia/Common/Arbitrary/Builtins.hs create mode 100644 src/Tokenomia/Common/Arbitrary/Modifiers.hs create mode 100644 src/Tokenomia/Common/Arbitrary/MultiAsset.hs create mode 100644 src/Tokenomia/Common/Arbitrary/Value.hs create mode 100644 src/Tokenomia/Common/MultiAsset.hs create mode 100644 test/Spec/Tokenomia/Common/MultiAsset.hs diff --git a/src/Tokenomia/Common/Arbitrary/AssetClass.hs b/src/Tokenomia/Common/Arbitrary/AssetClass.hs new file mode 100644 index 00000000..88ec7927 --- /dev/null +++ b/src/Tokenomia/Common/Arbitrary/AssetClass.hs @@ -0,0 +1,31 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module Tokenomia.Common.Arbitrary.AssetClass + () where + +import Plutus.V1.Ledger.Value + ( AssetClass(..) + , CurrencySymbol (..) + , TokenName (..) + ) + +import Test.Tasty.QuickCheck + ( Arbitrary + , arbitrary + , shrink + ) + +import Tokenomia.Common.Arbitrary.Builtins () + + +instance Arbitrary CurrencySymbol where + arbitrary = CurrencySymbol <$> arbitrary + shrink x = CurrencySymbol <$> shrink (unCurrencySymbol x) + +instance Arbitrary TokenName where + arbitrary = TokenName <$> arbitrary + shrink x = TokenName <$> shrink (unTokenName x) + +instance Arbitrary AssetClass where + arbitrary = AssetClass <$> arbitrary + shrink x = AssetClass <$> shrink (unAssetClass x) diff --git a/src/Tokenomia/Common/Arbitrary/Builtins.hs b/src/Tokenomia/Common/Arbitrary/Builtins.hs new file mode 100644 index 00000000..130e7255 --- /dev/null +++ b/src/Tokenomia/Common/Arbitrary/Builtins.hs @@ -0,0 +1,22 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module Tokenomia.Common.Arbitrary.Builtins + () where + +import PlutusTx.Builtins.Internal + ( BuiltinByteString(..) ) + +import Test.QuickCheck.Instances.ByteString () +import Test.Tasty.QuickCheck + ( Arbitrary + , arbitrary + , resize + , shrink + ) + + +instance Arbitrary BuiltinByteString where + arbitrary = BuiltinByteString <$> resize 64 arbitrary + shrink x + | x == mempty = mempty + | otherwise = pure mempty diff --git a/src/Tokenomia/Common/Arbitrary/Modifiers.hs b/src/Tokenomia/Common/Arbitrary/Modifiers.hs new file mode 100644 index 00000000..f7475754 --- /dev/null +++ b/src/Tokenomia/Common/Arbitrary/Modifiers.hs @@ -0,0 +1,8 @@ +module Tokenomia.Common.Arbitrary.Modifiers + ( Restricted(..) + ) where + + +newtype Restricted a + = Restricted { getRestricted :: a } + deriving (Show, Eq ) diff --git a/src/Tokenomia/Common/Arbitrary/MultiAsset.hs b/src/Tokenomia/Common/Arbitrary/MultiAsset.hs new file mode 100644 index 00000000..d4002abc --- /dev/null +++ b/src/Tokenomia/Common/Arbitrary/MultiAsset.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +module Tokenomia.Common.Arbitrary.MultiAsset + ( Restricted(..) + ) where + +import Data.Functor.Syntax ( (<$$>) ) +import Data.Set qualified as Set ( toList ) +import Data.Map.Strict qualified as Map ( fromList ) + +import Ledger.Value + ( AssetClass + ) + +import Test.Tasty.QuickCheck + ( Arbitrary + , Gen + , arbitrary + , getPositive + , shrink + ) + +import Tokenomia.Common.MultiAsset + ( MultiAsset(..) + , MultiAssetFormat(..) + ) + +import Tokenomia.Common.Arbitrary.Modifiers ( Restricted(..) ) +import Tokenomia.Common.Arbitrary.AssetClass () + + +instance Arbitrary MultiAsset where + arbitrary = MultiAsset <$> arbitrary + shrink x = MultiAsset <$> shrink (unMultiAsset x) + +instance Arbitrary (Restricted MultiAsset) where + arbitrary = Restricted . MultiAsset . Map.fromList <$> gen + where + gen :: Gen [(AssetClass, Integer)] + gen = + zip + <$> (Set.toList <$> arbitrary) + <*> (getPositive <$$> arbitrary) + shrink x = Restricted <$> shrink (getRestricted x) + +instance Arbitrary MultiAssetFormat where + arbitrary = MultiAssetFormat <$> arbitrary + shrink x = MultiAssetFormat <$> shrink (unMultiAssetFormat x) diff --git a/src/Tokenomia/Common/Arbitrary/Value.hs b/src/Tokenomia/Common/Arbitrary/Value.hs new file mode 100644 index 00000000..a430d416 --- /dev/null +++ b/src/Tokenomia/Common/Arbitrary/Value.hs @@ -0,0 +1,65 @@ +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +module Tokenomia.Common.Arbitrary.Value + ( Restricted(..) + ) where + +import Data.Functor.Syntax ( (<$$>) ) +import Data.Set qualified as Set ( toList ) + +import Plutus.V1.Ledger.Value + ( AssetClass(..) + , Value(..) + , assetClassValue + ) + +import PlutusTx.AssocMap qualified as AssocMap + ( Map + , fromList + , toList + ) + +import Tokenomia.Common.MultiAsset + ( MultiAsset(..) + , FromValue(..) + , ToValue(..) + ) + +import Test.QuickCheck.Instances.ByteString () +import Test.Tasty.QuickCheck + ( Arbitrary + , Gen + , arbitrary + , getPositive + , shrink + ) + +import Tokenomia.Common.Arbitrary.Modifiers ( Restricted(..) ) +import Tokenomia.Common.Arbitrary.AssetClass () +import Tokenomia.Common.Arbitrary.MultiAsset () + + +instance (Arbitrary k, Arbitrary v) => Arbitrary (AssocMap.Map k v) where + arbitrary = AssocMap.fromList <$> arbitrary + shrink x = AssocMap.fromList <$> shrink (AssocMap.toList x) + +instance Arbitrary Value where + arbitrary = Value <$> arbitrary + shrink x = Value <$> shrink (getValue x) + +instance Arbitrary (Restricted Value) where + arbitrary = + Restricted . mconcat + <$> uncurry assetClassValue <$$> gen + where + gen :: Gen [(AssetClass, Integer)] + gen = + zip + <$> (Set.toList <$> arbitrary) + <*> (getPositive <$$> arbitrary) + shrink x = + Restricted . toValue + <$> shrink (fromValue @MultiAsset $ getRestricted x) diff --git a/src/Tokenomia/Common/MultiAsset.hs b/src/Tokenomia/Common/MultiAsset.hs new file mode 100644 index 00000000..8c62ff24 --- /dev/null +++ b/src/Tokenomia/Common/MultiAsset.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeApplications #-} + +module Tokenomia.Common.MultiAsset + ( MultiAsset(..) + , MultiAssetFormat(..) + , FromValue(..) + , ToValue(..) + , adjustAda + , groupByAssetClass + ) where + +-- +-- This wrapper module allows working on multiformat values +-- with a flat representation as a map indexed by asset classes +-- instead of the default nested maps representation +-- indexed in turns by the components of the asset class tuple. +-- +-- Also, the underlying representation of plutus values +-- are not true maps but lists of (key, value) pairs. +-- This module provides a way to express multiformat values +-- that can be indexed by unique asset class keys. +-- +-- Therefore, the type Value holds more inhabitants than MultiAsset. +-- When duplicate keys occur in a Value, since the map is built fromList +-- only the last value for the key is retained. +-- In other words, the conversion fromValue :: Value -> MultiAssetFormat +-- can suffer loss of data. +-- +-- Otherwise, for any well-formed value, there is a bijection +-- between Value and MultiAsset. The following properies holds : +-- fromValue . toValue == id +-- toValue . fromValue == id +-- + +import Data.List.NonEmpty ( NonEmpty, groupAllWith ) +import Data.Map ( Map, fromList, toList, adjust, keysSet) +import Data.Set ( Set ) + +import Ledger.Value + ( AssetClass(..) + , Value(..) + , assetClassValue + , flattenValue + ) + +import Tokenomia.Common.AssetClass ( adaAssetClass ) + + +newtype MultiAsset + = MultiAsset { unMultiAsset :: Map AssetClass Integer } + deriving (Show, Eq, Ord ) + +newtype MultiAssetFormat + = MultiAssetFormat { unMultiAssetFormat :: Set AssetClass } + deriving (Show, Eq, Ord) + +class FromValue a where + fromValue :: Value -> a + +instance FromValue MultiAsset where + fromValue :: Value -> MultiAsset + fromValue value = + MultiAsset . fromList $ + (\(cs, tn, a) -> (AssetClass (cs, tn), a)) <$> flattenValue value + +instance FromValue MultiAssetFormat where + fromValue :: Value -> MultiAssetFormat + fromValue = + MultiAssetFormat . keysSet . unMultiAsset . fromValue + +class ToValue a where + toValue :: a -> Value + +instance ToValue MultiAsset where + toValue :: MultiAsset -> Value + toValue (MultiAsset xs) = + mconcat $ uncurry assetClassValue <$> toList xs + +groupByAssetClass :: (a -> Value) -> [a] -> [NonEmpty a] +groupByAssetClass value = groupAllWith $ fromValue @MultiAssetFormat . value + +adjustAda :: Integer -> MultiAsset -> MultiAsset +adjustAda amount = + wrap $ adjust (const amount) adaAssetClass + where + wrap f = MultiAsset . f . unMultiAsset diff --git a/test/Spec.hs b/test/Spec.hs index 5c0bfa77..32e0671c 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -4,6 +4,7 @@ module Main(main) where import qualified Spec.Tokenomia.Common.Data.Function.Memoize import qualified Spec.Tokenomia.Common.Data.Sequence.IntegerPartitions +import qualified Spec.Tokenomia.Common.MultiAsset import Test.Tasty ( TestTree, defaultMain, testGroup ) main :: IO () @@ -13,4 +14,5 @@ tests :: TestTree tests = testGroup "use cases" [ Spec.Tokenomia.Common.Data.Function.Memoize.tests , Spec.Tokenomia.Common.Data.Sequence.IntegerPartitions.tests + , Spec.Tokenomia.Common.MultiAsset.tests ] diff --git a/test/Spec/Tokenomia/Common/MultiAsset.hs b/test/Spec/Tokenomia/Common/MultiAsset.hs new file mode 100644 index 00000000..f7d3091b --- /dev/null +++ b/test/Spec/Tokenomia/Common/MultiAsset.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE TypeApplications #-} + +module Spec.Tokenomia.Common.MultiAsset + ( tests + ) where + +-- +-- This module tests the `MultiAsset` wrapper. +-- + +import Test.Tasty ( TestTree, testGroup ) +import Test.Tasty.QuickCheck ( testProperty ) + +import Tokenomia.Common.MultiAsset + ( MultiAsset(..) + , FromValue(..) + , ToValue(..) + ) + +import Tokenomia.Common.Arbitrary.Modifiers ( Restricted(..) ) +import Tokenomia.Common.Arbitrary.MultiAsset () +import Tokenomia.Common.Arbitrary.Value () + + +tests :: TestTree +tests = testGroup "Common.MultiAsset" [ properties ] + +properties :: TestTree +properties = testGroup "Properties" + [ testGroup "MultiAsset" + [ testGroup "Bijection Laws" + [ testProperty "fromValue . toValue == id" + (\restricted -> + let x = getRestricted restricted + in (fromValue . toValue @MultiAsset $ x) == x + ) + , testProperty "toValue . fromValue == id" + (\restricted -> + let x = getRestricted restricted + in (toValue @MultiAsset . fromValue $ x) == x + ) + ] + ] + ] diff --git a/tokenomia.cabal b/tokenomia.cabal index 24038416..3718d936 100644 --- a/tokenomia.cabal +++ b/tokenomia.cabal @@ -68,6 +68,11 @@ library Tokenomia.Vesting.Vest Tokenomia.Vesting.Retrieve Tokenomia.Tokenomic.CLAP.Simulation + Tokenomia.Common.Arbitrary.AssetClass + Tokenomia.Common.Arbitrary.Builtins + Tokenomia.Common.Arbitrary.Modifiers + Tokenomia.Common.Arbitrary.MultiAsset + Tokenomia.Common.Arbitrary.Value Tokenomia.Common.AssetClass Tokenomia.Common.Blockfrost Tokenomia.Common.Value @@ -81,6 +86,7 @@ library Tokenomia.Common.Datum Tokenomia.Common.Address Tokenomia.Common.Asset + Tokenomia.Common.MultiAsset Tokenomia.Common.Hash Tokenomia.Common.Token Tokenomia.Common.PageNumber @@ -194,7 +200,9 @@ library streamly, deepseq, hashable, - hex-text + hex-text, + quickcheck-instances, + tasty-quickcheck hs-source-dirs: src test-suite tokenomia-tests @@ -215,6 +223,7 @@ test-suite tokenomia-tests Spec.Tokenomia.ICO.Funds.Validation.CardanoCLI.Plan Spec.Tokenomia.Common.Data.Function.Memoize Spec.Tokenomia.Common.Data.Sequence.IntegerPartitions + Spec.Tokenomia.Common.MultiAsset build-depends: plutus-tx -any, plutus-tx-plugin,