Skip to content

Commit

Permalink
Merge #2507
Browse files Browse the repository at this point in the history
2507: Optionally parse non-required well-known token metadata properties from server r=rvl a=KtorZ

# Issue Number

<!-- Put here a reference to the issue that this PR relates to and which requirements it tackles. Jira issues of the form ADP- will be auto-linked. -->

ADP-413

# Overview

<!-- Detail in a few bullet points the work accomplished in this PR -->

- [ ] I have added the missing parser for all well-known properties. 


Co-authored-by: KtorZ <matthias.benkort@gmail.com>
Co-authored-by: Johannes Lund <johannes.lund@iohk.io>
Co-authored-by: Rodney Lorrimar <rodney.lorrimar@iohk.io>
  • Loading branch information
4 people authored Feb 11, 2021
2 parents 1ea5e88 + ddab305 commit 247b5f6
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import Cardano.Wallet.Primitive.AddressDerivation.Icarus
import Cardano.Wallet.Primitive.Types.Hash
( Hash (..) )
import Cardano.Wallet.Primitive.Types.TokenPolicy
( AssetMetadata (AssetMetadata) )
( AssetLogo (..), AssetMetadata (AssetMetadata), AssetUnit (AssetUnit) )
import Cardano.Wallet.Primitive.Types.Tx
( Direction (..), TxStatus (..) )
import Cardano.Wallet.Unsafe
Expand Down Expand Up @@ -253,7 +253,9 @@ spec = describe "BYRON_TRANSACTIONS" $ do
pickAnAsset assetsSrc
let ep = Link.getByronAsset wal polId assName
r <- request @(ApiAsset) ctx ep Default Empty
let meta = ApiT (AssetMetadata "SteveToken" "A sample description")
let meta = ApiT $ AssetMetadata "SteveToken" "A sample description"
(Just "STV") (Just "https://iohk.io/stevetoken")
(Just (AssetLogo "Almost a logo")) (Just (AssetUnit "MegaSteve" 6))
verify r
[ expectSuccess
, expectField #policyId (`shouldBe` ApiT polId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLabels #-}
Expand Down Expand Up @@ -48,7 +49,7 @@ import Cardano.Wallet.Primitive.Types.Address
import Cardano.Wallet.Primitive.Types.Hash
( Hash (..) )
import Cardano.Wallet.Primitive.Types.TokenPolicy
( AssetMetadata (AssetMetadata) )
( AssetLogo (..), AssetMetadata (..), AssetUnit (..) )
import Cardano.Wallet.Primitive.Types.Tx
( Direction (..), TxMetadata (..), TxMetadataValue (..), TxStatus (..) )
import Cardano.Wallet.Unsafe
Expand Down Expand Up @@ -600,7 +601,7 @@ spec = describe "SHELLEY_TRANSACTIONS" $ do
)
]

forM_ matrix $ \(name, nonJson) -> it name $ \ctx -> runResourceT $ do
forM_ matrix $ \(title, nonJson) -> it title $ \ctx -> runResourceT $ do
w <- emptyWallet ctx
let payload = nonJson
r <- request @(ApiTransaction n) ctx
Expand All @@ -616,6 +617,8 @@ spec = describe "SHELLEY_TRANSACTIONS" $ do
]

let meta = ApiT $ AssetMetadata "SteveToken" "A sample description"
(Just "STV") (Just "https://iohk.io/stevetoken")
(Just (AssetLogo "Almost a logo")) (Just (AssetUnit "MegaSteve" 6))
r2 <- request @[ApiAsset] ctx (Link.listAssets w) Default Empty
verify r2
[ expectListField 0 #metadata (`shouldBe` Just meta)
Expand Down Expand Up @@ -803,7 +806,14 @@ spec = describe "SHELLEY_TRANSACTIONS" $ do
pickAnAsset assetsSrc
let ep = Link.getAsset wal polId assName
r <- request @(ApiAsset) ctx ep Default Empty
let meta = ApiT $ AssetMetadata "SteveToken" "A sample description"
let meta = ApiT $ AssetMetadata
{ name = "SteveToken"
, description = "A sample description"
, acronym = Just "STV"
, url = Just "https://iohk.io/stevetoken"
, unit = Just $ AssetUnit "MegaSteve" 6
, logo = Just $ AssetLogo "Almost a logo"
}
verify r
[ expectSuccess
, expectField #policyId (`shouldBe` ApiT polId)
Expand Down Expand Up @@ -1786,7 +1796,7 @@ spec = describe "SHELLEY_TRANSACTIONS" $ do
)
]

forM_ matrix $ \(name, nonJson) -> it name $ \ctx -> runResourceT $ do
forM_ matrix $ \(title, nonJson) -> it title $ \ctx -> runResourceT $ do
w <- emptyWallet ctx
let payload = nonJson
r <- request @ApiFee ctx
Expand Down
2 changes: 2 additions & 0 deletions lib/core/src/Cardano/Wallet/Api/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ import Cardano.Wallet.Primitive.Types.Tx
( Direction (..), TxIn (..), TxMetadata, TxStatus (..), txMetadataIsNull )
import Cardano.Wallet.Primitive.Types.UTxO
( BoundType, HistogramBar (..), UTxOStatistics (..) )
import Cardano.Wallet.TokenMetadata
()
import Codec.Binary.Bech32
( dataPartFromBytes, dataPartToBytes )
import Codec.Binary.Bech32.TH
Expand Down
32 changes: 27 additions & 5 deletions lib/core/src/Cardano/Wallet/Primitive/Types/TokenPolicy.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}

module Cardano.Wallet.Primitive.Types.TokenPolicy
(
Expand All @@ -16,6 +17,8 @@ module Cardano.Wallet.Primitive.Types.TokenPolicy

-- * Token Metadata
, AssetMetadata (..)
, AssetLogo (..)
, AssetUnit (..)
) where

import Prelude
Expand Down Expand Up @@ -44,6 +47,8 @@ import Fmt
( Buildable (..) )
import GHC.Generics
( Generic )
import Numeric.Natural
( Natural )
import Quiet
( Quiet (..) )

Expand Down Expand Up @@ -126,12 +131,29 @@ instance FromText TokenName where
-- | Information about an asset, from a source external to the chain.
data AssetMetadata = AssetMetadata
{ name :: Text
-- , acronym :: Text -- TODO: needs metadata-server support
, description :: Text
-- , url :: Text -- TODO: needs metadata-server support
-- , logoBase64 :: ByteString -- TODO: needs metadata-server support
-- , unit :: AssetUnit -- TODO: needs metadata-server support
, acronym :: Maybe Text
, url :: Maybe Text
, logo :: Maybe AssetLogo
, unit :: Maybe AssetUnit
} deriving stock (Eq, Ord, Generic)
deriving (Read, Show) via (Quiet AssetMetadata)
deriving (Show) via (Quiet AssetMetadata)

instance NFData AssetMetadata

-- | Specification of a larger unit for an asset. For example, the "lovelace"
-- asset has the larger unit "ada" with 6 zeroes.
data AssetUnit = AssetUnit
{ name :: Text -- ^ Name of the larger asset.
, decimals :: Natural -- ^ Number of zeroes to add to base unit.
} deriving (Generic, Show, Eq, Ord)

instance NFData AssetUnit

-- | Specify an asset logo as an image data payload
newtype AssetLogo = AssetLogo
{ unAssetLogo :: ByteString
} deriving (Eq, Ord, Generic)
deriving (Show) via (Quiet AssetLogo)

instance NFData AssetLogo
115 changes: 81 additions & 34 deletions lib/core/src/Cardano/Wallet/TokenMetadata.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
Expand All @@ -12,10 +13,13 @@
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}

-- |
-- Copyright: © 2018-2021 IOHK
-- License: Apache-2.0
Expand Down Expand Up @@ -67,7 +71,12 @@ import Cardano.Wallet.Primitive.Types.Hash
import Cardano.Wallet.Primitive.Types.TokenMap
( AssetId (..) )
import Cardano.Wallet.Primitive.Types.TokenPolicy
( AssetMetadata (..), TokenName (..), TokenPolicyId (..) )
( AssetLogo (..)
, AssetMetadata (..)
, AssetUnit (..)
, TokenName (..)
, TokenPolicyId (..)
)
import Control.Applicative
( (<|>) )
import Control.Monad
Expand All @@ -80,16 +89,18 @@ import Data.Aeson
, Value (..)
, eitherDecodeStrict'
, encode
, object
, withObject
, withText
, (.!=)
, (.:)
, (.:?)
, (.=)
)
import Data.Bifunctor
( first )
import Data.ByteArray.Encoding
( Base (Base16), convertFromBase, convertToBase )
( Base (Base16, Base64), convertFromBase, convertToBase )
import Data.ByteString
( ByteString )
import Data.Foldable
Expand Down Expand Up @@ -129,8 +140,6 @@ import Network.URI
( URI, relativeTo )
import Network.URI.Static
( relativeReference )
import Numeric.Natural
( Natural )
import UnliftIO.Exception
( SomeException, handle, handleAny )

Expand Down Expand Up @@ -188,13 +197,14 @@ data SubjectProperties = SubjectProperties
-- TODO: use Data.SOP.NP and parameterize type by property names
-- Name and description are required, both others may be missing the
-- response.
, properties :: ( Property "name"
-- , (PropertyValue "acronym", [Signature])
, Property "description"
-- , (PropertyValue "url", [Signature])
-- , (PropertyValue "logo", [Signature])
-- , (PropertyValue "unit", [Signature])
)
, properties ::
( Property "name"
, Property "description"
, Maybe (Property "acronym")
, Maybe (Property "url")
, Maybe (Property "logo")
, Maybe (Property "unit")
)
} deriving (Generic, Show, Eq)

-- | A property value and its signatures.
Expand All @@ -219,19 +229,12 @@ newtype PropertyName = PropertyName { unPropertyName :: Text }
-- | The type of a given property name.
type family PropertyValue (name :: Symbol) :: *
type instance PropertyValue "name" = Text
type instance PropertyValue "acronym" = Text
type instance PropertyValue "description" = Text
type instance PropertyValue "acronym" = Text
type instance PropertyValue "url" = Text
-- type instance PropertyValue "logo" = AssetLogoBase64
type instance PropertyValue "logo" = AssetLogo
type instance PropertyValue "unit" = AssetUnit

-- | Specification of a larger unit for an asset. For example, the "lovelace"
-- asset has the larger unit "ada" with 6 zeroes.
data AssetUnit = AssetUnit
{ name :: Text -- ^ Name of the larger asset.
, decimals :: Natural -- ^ Number of zeroes to add to base unit.
} deriving (Generic, Show, Eq)

-- | Will be used in future for checking integrity and authenticity of metadata.
data Signature = Signature
{ signature :: ByteString
Expand Down Expand Up @@ -420,7 +423,9 @@ getTokenMetadata (TokenMetadataClient client) as =
subjects = map assetIdToSubject as
req = BatchRequest
{ subjects
, properties = [PropertyName "name", PropertyName "description"]
, properties = PropertyName <$>
[ "name", "description", "acronym"
, "url", "logo", "unit" ]
}
subjectAsset = HM.fromList $ zip subjects as
fromResponse :: BatchResponse -> [(AssetId, AssetMetadata)]
Expand All @@ -438,8 +443,16 @@ assetIdToSubject (AssetId (UnsafeTokenPolicyId (Hash p)) (UnsafeTokenName n)) =
-- | Convert metadata server properties response into an 'AssetMetadata' record.
-- Only the values are taken. Signatures are ignored (for now).
metadataFromProperties :: SubjectProperties -> AssetMetadata
metadataFromProperties (SubjectProperties _ _ ((Property n _, Property d _))) =
AssetMetadata n d
metadataFromProperties (SubjectProperties _ _ properties) =
AssetMetadata { name, description, acronym, url, logo, unit }
where
( pName, pDescription, pAcronym, pUrl, pLogo, pUnit ) = properties
name = value pName
description = value pDescription
acronym = value <$> pAcronym
url = value <$> pUrl
logo = value <$> pLogo
unit = value <$> pUnit

{-------------------------------------------------------------------------------
Aeson instances for metadata-server
Expand All @@ -464,10 +477,18 @@ instance FromJSON Subject where
parseJSON = withText "Subject" (pure . Subject)

instance FromJSON SubjectProperties where
parseJSON = withObject "SubjectProperties" $ \o -> SubjectProperties
<$> o .: "subject"
<*> o .:? "owner"
<*> ((,) <$> o .: "name" <*> o .: "description")
parseJSON = withObject "SubjectProperties" $ \o -> SubjectProperties
<$> o .: "subject"
<*> o .:? "owner"
<*> parseProperties o
where
parseProperties o = (,,,,,)
<$> o .: "name"
<*> o .: "description"
<*> o .:? "acronym"
<*> o .:? "url"
<*> o .:? "logo"
<*> o .:? "unit"

instance FromJSON (PropertyValue name) => FromJSON (Property name) where
parseJSON = withObject "Property" $ \o -> Property
Expand All @@ -476,14 +497,40 @@ instance FromJSON (PropertyValue name) => FromJSON (Property name) where

instance FromJSON Signature where
parseJSON = withObject "Signature" $ \o -> Signature
<$> fmap unHex (o .: "signature")
<*> fmap unHex (o .: "publicKey")
<$> fmap (raw @'Base16) (o .: "signature")
<*> fmap (raw @'Base16) (o .: "publicKey")

newtype Hex = Hex { unHex :: ByteString } deriving (Generic, Show, Eq)
instance FromJSON AssetLogo where
parseJSON =
fmap (AssetLogo . raw @'Base64) . parseJSON

instance FromJSON Hex where
parseJSON = withText "hex bytestring" $
either fail (pure . Hex) . convertFromBase Base16 . T.encodeUtf8
instance ToJSON AssetLogo where
toJSON =
toJSON . B8.unpack . convertToBase Base64 . unAssetLogo

instance FromJSON AssetUnit where
-- TODO: AssetUnit, when it's provided by the metadata server
parseJSON = withObject "AssetUnit" $ \o -> AssetUnit
<$> o .: "name"
<*> o .: "decimals"

instance ToJSON AssetUnit where
toJSON AssetUnit{name,decimals} = object
[ "name" .= name
, "decimals" .= decimals
]

--
-- Helpers
--

newtype Encoded (base :: Base) = Encoded
{ raw :: ByteString }
deriving (Generic, Show, Eq)

instance FromJSON (Encoded 'Base16) where
parseJSON = withText "base16 bytestring" $
either fail (pure . Encoded) . convertFromBase Base16 . T.encodeUtf8

instance FromJSON (Encoded 'Base64) where
parseJSON = withText "base64 bytestring" $
either fail (pure . Encoded) . convertFromBase Base64 . T.encodeUtf8
6 changes: 5 additions & 1 deletion lib/core/src/Cardano/Wallet/TokenMetadata/MockServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,15 @@ assetIdFromSubject =
instance FromJSON BatchRequest where

instance ToJSON SubjectProperties where
toJSON (SubjectProperties s o (n, d)) = object
toJSON (SubjectProperties s o (n,d,a,u,l,t)) = object
[ "subject" .= s
, "owner" .= o
, "name" .= n
, "description" .= d
, "acronym" .= a
, "url" .= u
, "logo" .= l
, "unit" .= t
]

instance ToJSON (PropertyValue name) => ToJSON (Property name) where
Expand Down
6 changes: 6 additions & 0 deletions lib/core/src/Cardano/Wallet/Unsafe.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

module Cardano.Wallet.Unsafe
( unsafeFromHex
, unsafeFromBase64
, unsafeFromHexFile
, unsafeDecodeAddress
, unsafeDecodeHex
Expand Down Expand Up @@ -92,6 +93,11 @@ unsafeFromHex :: HasCallStack => ByteString -> ByteString
unsafeFromHex =
either (error . show) id . convertFromBase @ByteString @ByteString Base16

-- | Decode a base64-encoded 'ByteString' into raw bytes, or fail.
unsafeFromBase64 :: HasCallStack => ByteString -> ByteString
unsafeFromBase64 =
either (error . show) id . convertFromBase @ByteString @ByteString Base64

-- | Load a hex string from file. Any non-hexadecimal characters are ignored.
unsafeFromHexFile :: HasCallStack => FilePath -> IO ByteString
unsafeFromHexFile = fmap (unsafeFromHex . B8.filter isHexDigit) . B8.readFile
Expand Down
Loading

0 comments on commit 247b5f6

Please sign in to comment.