Skip to content

Commit

Permalink
Merge #2092
Browse files Browse the repository at this point in the history
2092: Implement Multi-Asset JSON instances r=dcoutts a=Jimbo4350

Multi-Asset JSON is of the following format:
```json
{
  "fooPolicyId": { "fooAssetName": 42 },
  "barPolicyId": { "barAssetName": 84 },
  "lovelace": 3
}
```

Co-authored-by: Jordan Millar <[email protected]>
  • Loading branch information
iohk-bors[bot] and Jimbo4350 authored Nov 25, 2020
2 parents 0738ec0 + e7334bd commit da23daf
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 3 deletions.
1 change: 1 addition & 0 deletions cardano-api/cardano-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ test-suite cardano-api-test
Test.Cardano.Api.Typed.MultiSig.Shelley
Test.Cardano.Api.Typed.Orphans
Test.Cardano.Api.Typed.RawBytes
Test.Cardano.Api.Typed.Value
Test.Tasty.Hedgehog.Group

default-language: Haskell2010
Expand Down
6 changes: 6 additions & 0 deletions cardano-api/src/Cardano/Api/Typed.hs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ module Cardano.Api.Typed (
selectLovelace,
lovelaceToValue,

-- ** Alternative nested representation
ValueNestedRep(..),
ValueNestedBundle(..),
valueToNestedRep,
valueFromNestedRep,

-- * Building transactions
-- | Constructing and inspecting transactions
TxBody(..),
Expand Down
106 changes: 104 additions & 2 deletions cardano-api/src/Cardano/Api/Value.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Currency values
Expand All @@ -26,6 +27,12 @@ module Cardano.Api.Value
, selectLovelace
, lovelaceToValue

-- ** Alternative nested representation
, ValueNestedRep(..)
, ValueNestedBundle(..)
, valueToNestedRep
, valueFromNestedRep

-- * Internal conversion functions
, toByronLovelace
, toShelleyLovelace
Expand All @@ -36,11 +43,18 @@ module Cardano.Api.Value

import Prelude

import Data.Aeson hiding (Value)
import qualified Data.Aeson as Aeson
import Data.Aeson.Types (Parser, toJSONKeyText)
import Data.ByteString (ByteString)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Merge.Strict as Map
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text

import qualified Cardano.Chain.Common as Byron

Expand All @@ -51,6 +65,7 @@ import qualified Cardano.Ledger.Mary.Value as Mary
import Ouroboros.Consensus.Shelley.Protocol.Crypto (StandardCrypto)

import Cardano.Api.Script
import Cardano.Api.SerialiseRaw (deserialiseFromRawBytesHex, serialiseToRawBytesHex)


-- ----------------------------------------------------------------------------
Expand All @@ -59,7 +74,7 @@ import Cardano.Api.Script

newtype Lovelace = Lovelace Integer
deriving stock (Show)
deriving newtype (Eq, Ord, Enum, Num)
deriving newtype (Eq, Ord, Enum, Num, ToJSON, FromJSON)

instance Semigroup Lovelace where
Lovelace a <> Lovelace b = Lovelace (a + b)
Expand Down Expand Up @@ -87,7 +102,7 @@ fromShelleyLovelace (Shelley.Coin l) = Lovelace l
--

newtype Quantity = Quantity Integer
deriving newtype (Eq, Ord, Num, Show)
deriving newtype (Eq, Ord, Num, Show, ToJSON, FromJSON)

instance Semigroup Quantity where
Quantity a <> Quantity b = Quantity (a + b)
Expand All @@ -110,6 +125,19 @@ newtype AssetName = AssetName ByteString
deriving stock (Show)
deriving newtype (Eq, Ord, IsString)

instance ToJSON AssetName where
toJSON (AssetName an) = Aeson.String $ Text.decodeUtf8 an

instance FromJSON AssetName where
parseJSON = withText "AssetName" (return . AssetName . Text.encodeUtf8)

instance ToJSONKey AssetName where
toJSONKey = toJSONKeyText (\(AssetName asset) -> Text.decodeUtf8 asset)

instance FromJSONKey AssetName where
fromJSONKey = FromJSONKeyText (AssetName . Text.encodeUtf8)


data AssetId = AdaAssetId
| AssetId !PolicyId !AssetName
deriving (Eq, Ord, Show)
Expand Down Expand Up @@ -145,6 +173,13 @@ mergeAssetMaps =
Quantity 0 -> Nothing
c -> Just c

instance ToJSON Value where
toJSON = toJSON . valueToNestedRep

instance FromJSON Value where
parseJSON v = valueFromNestedRep <$> parseJSON v


selectAsset :: Value -> (AssetId -> Quantity)
selectAsset (Value m) a = Map.findWithDefault mempty a m

Expand Down Expand Up @@ -207,3 +242,70 @@ fromMaryValue (Mary.Value lovelace other) =

fromMaryAssetName :: Mary.AssetName -> AssetName
fromMaryAssetName (Mary.AssetName n) = AssetName n


-- ----------------------------------------------------------------------------
-- An alternative nested representation
--

-- | An alternative nested representation for 'Value' that groups assets that
-- share a 'PolicyId'.
--
newtype ValueNestedRep = ValueNestedRep [ValueNestedBundle]
deriving (Eq, Ord, Show)

-- | A bundle within a 'ValueNestedRep' for a single 'PolicyId', or for the
-- special case of ada.
--
data ValueNestedBundle = ValueNestedBundleAda Quantity
| ValueNestedBundle PolicyId (Map AssetName Quantity)
deriving (Eq, Ord, Show)


valueToNestedRep :: Value -> ValueNestedRep
valueToNestedRep v =
-- unflatten all the non-ada assets, and add ada separately
ValueNestedRep $
[ ValueNestedBundleAda q | let q = selectAsset v AdaAssetId, q /= 0 ]
++ [ ValueNestedBundle pId qs | (pId, qs) <- Map.toList nonAdaAssets ]
where
nonAdaAssets :: Map PolicyId (Map AssetName Quantity)
nonAdaAssets =
Map.fromListWith (Map.unionWith (<>))
[ (pId, Map.singleton aName q)
| (AssetId pId aName, q) <- valueToList v ]

valueFromNestedRep :: ValueNestedRep -> Value
valueFromNestedRep (ValueNestedRep bundles) =
valueFromList
[ (aId, q)
| bundle <- bundles
, (aId, q) <- case bundle of
ValueNestedBundleAda q -> [ (AdaAssetId, q) ]
ValueNestedBundle pId qs -> [ (AssetId pId aName, q)
| (aName, q) <- Map.toList qs ]
]

instance ToJSON ValueNestedRep where
toJSON (ValueNestedRep bundles) = object $ map toPair bundles
where
toPair :: ValueNestedBundle -> (Text, Aeson.Value)
toPair (ValueNestedBundleAda q) = ("lovelace", toJSON q)
toPair (ValueNestedBundle pid assets) = (renderPolicyId pid, toJSON assets)

renderPolicyId :: PolicyId -> Text
renderPolicyId (PolicyId sh) = Text.decodeUtf8 (serialiseToRawBytesHex sh)

instance FromJSON ValueNestedRep where
parseJSON =
withObject "ValueNestedRep" $ \obj ->
ValueNestedRep <$> sequenceA [ parsePid keyValTuple
| keyValTuple <- HashMap.toList obj ]
where
parsePid :: (Text, Aeson.Value) -> Parser ValueNestedBundle
parsePid ("lovelace", q) = ValueNestedBundleAda <$> parseJSON q
parsePid (pid, q) =
case deserialiseFromRawBytesHex AsScriptHash (Text.encodeUtf8 pid) of
Just sHash -> ValueNestedBundle (PolicyId sHash) <$> (parseJSON q)
Nothing -> fail $ "Failure when deserialising PolicyId: "
<> Text.unpack pid
56 changes: 56 additions & 0 deletions cardano-api/test/Test/Cardano/Api/Typed/Gen.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{-# LANGUAGE OverloadedStrings #-}

module Test.Cardano.Api.Typed.Gen
( genAddressByron
, genAddressShelley
, genValueNestedRep
, genValueNestedBundle
, genByronKeyWitness
, genRequiredSig
, genMofNRequiredSig
Expand All @@ -19,6 +23,7 @@ module Test.Cardano.Api.Typed.Gen
, genTxShelley
, genTxBodyByron
, genTxBodyShelley
, genValue
, genVerificationKey
) where

Expand All @@ -27,6 +32,8 @@ import Cardano.Api.Typed
import Cardano.Prelude

import Control.Monad.Fail (fail)
import qualified Data.Map.Strict as Map
import Data.String

import qualified Cardano.Binary as CBOR
import qualified Cardano.Crypto.Hash as Crypto
Expand Down Expand Up @@ -149,6 +156,55 @@ genMultiSigScriptsMary =

]

genAssetName :: Gen AssetName
genAssetName =
Gen.frequency
-- mostly from a small number of choices, so we get plenty of repetition
[ (9, Gen.element ["", "a", "b", "c"])
, (1, AssetName <$> Gen.utf8 (Range.singleton 32) Gen.alphaNum)
, (1, AssetName <$> Gen.utf8 (Range.constant 1 31) Gen.alphaNum)
]

genPolicyId :: Gen PolicyId
genPolicyId =
Gen.frequency
-- mostly from a small number of choices, so we get plenty of repetition
[ (9, Gen.element [ fromString (x : replicate 55 '0') | x <- ['a'..'c'] ])

-- and some from the full range of the type
, (1, PolicyId <$> genScriptHash)
]

genAssetId :: Gen AssetId
genAssetId = Gen.choice [ AssetId <$> genPolicyId <*> genAssetName
, return AdaAssetId
]

genQuantity :: Gen Quantity
genQuantity = fromInteger <$> Gen.integral (Range.constantFrom 0 (-2) 2)

genValue :: Gen Value
genValue = do
valueFromList <$>
Gen.list (Range.constant 0 10)
((,) <$> genAssetId <*> genQuantity)


-- We do not generate duplicate keys as 'ValueNestedRep' is created via
-- flattening a 'Map'
genValueNestedRep :: Gen ValueNestedRep
genValueNestedRep =
ValueNestedRep <$> Gen.list (Range.constant 0 5) genValueNestedBundle

genValueNestedBundle :: Gen ValueNestedBundle
genValueNestedBundle =
Gen.choice
[ ValueNestedBundleAda <$> genQuantity
, ValueNestedBundle <$> genPolicyId
<*> Gen.map (Range.constant 0 5)
((,) <$> genAssetName <*> genQuantity)
]

genAllRequiredSig :: Gen (MultiSigScript ShelleyEra)
genAllRequiredSig =
RequireAllOf <$> Gen.list (Range.constant 1 10) (genRequiredSig SignaturesInShelleyEra)
Expand Down
78 changes: 78 additions & 0 deletions cardano-api/test/Test/Cardano/Api/Typed/Value.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{-# LANGUAGE TemplateHaskell #-}

module Test.Cardano.Api.Typed.Value
( tests
) where

import Prelude

import Data.List (sort, groupBy)
import Data.Aeson
import qualified Data.Map.Strict as Map

import Cardano.Api.Typed

import Hedgehog (Property, discover, forAll, property, tripping, (===))
import Test.Cardano.Api.Typed.Gen

import Test.Tasty (TestTree)
import Test.Tasty.Hedgehog.Group (fromGroup)

prop_roundtrip_Value_JSON :: Property
prop_roundtrip_Value_JSON =
property $ do v <- forAll genValue
tripping v encode eitherDecode


prop_roundtrip_Value_flatten_unflatten :: Property
prop_roundtrip_Value_flatten_unflatten =
property $ do v <- forAll genValue
valueFromNestedRep (valueToNestedRep v) === v

prop_roundtrip_Value_unflatten_flatten :: Property
prop_roundtrip_Value_unflatten_flatten =
property $ do
v <- forAll genValueNestedRep
canonicalise v === valueToNestedRep (valueFromNestedRep v)

canonicalise :: ValueNestedRep -> ValueNestedRep
canonicalise =
ValueNestedRep
. filter (not . isZeroOrEmpty)
. map filterZeros
. map (foldl1 mergeBundle)
. groupBy samePolicyId
. sort
. (\(ValueNestedRep bundles) -> bundles)
where
samePolicyId ValueNestedBundleAda{}
ValueNestedBundleAda{} = True
samePolicyId (ValueNestedBundle pid _)
(ValueNestedBundle pid' _) = pid == pid'
samePolicyId _ _ = False

-- Merge together bundles that have already been grouped by same PolicyId:
mergeBundle (ValueNestedBundleAda q)
(ValueNestedBundleAda q') =
ValueNestedBundleAda (q <> q')

mergeBundle (ValueNestedBundle pid as)
(ValueNestedBundle pid' as') | pid == pid' =
ValueNestedBundle pid (Map.unionWith (<>) as as')

mergeBundle _ _ = error "canonicalise.mergeBundle: impossible"

filterZeros b@ValueNestedBundleAda{} = b
filterZeros (ValueNestedBundle pid as) =
ValueNestedBundle pid (Map.filter (/=0) as)

isZeroOrEmpty (ValueNestedBundleAda q) = q == 0
isZeroOrEmpty (ValueNestedBundle _ as) = Map.null as



-- -----------------------------------------------------------------------------

tests :: TestTree
tests = fromGroup $$discover

4 changes: 3 additions & 1 deletion cardano-api/test/cardano-api-test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import qualified Test.Cardano.Api.Typed.Envelope
import qualified Test.Cardano.Api.Typed.MultiSig.Allegra
import qualified Test.Cardano.Api.Typed.MultiSig.Mary
import qualified Test.Cardano.Api.Typed.RawBytes
import qualified Test.Cardano.Api.Typed.Value

main :: IO ()
main = do
Expand All @@ -23,7 +24,8 @@ main = do
tests :: TestTree
tests =
testGroup "Cardano.Api"
[ Test.Cardano.Api.Crypto.tests
[ Test.Cardano.Api.Typed.Value.tests
, Test.Cardano.Api.Crypto.tests
, Test.Cardano.Api.Ledger.tests
, Test.Cardano.Api.MetaData.tests
, Test.Cardano.Api.Typed.Bech32.tests
Expand Down

0 comments on commit da23daf

Please sign in to comment.