Skip to content

Commit 58c6e49

Browse files
committed
Initial commit of decimals support
1 parent 80e5bfa commit 58c6e49

File tree

9 files changed

+124
-30
lines changed

9 files changed

+124
-30
lines changed

metadata-lib/test/Test/Cardano/Metadata/Validation.hs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module Test.Cardano.Metadata.Validation
1111
import qualified Data.Aeson as Aeson
1212
import Data.Foldable
1313
( forM_, traverse_ )
14+
import Data.Maybe (fromJust)
1415
import Data.Int
1516
( Int32, Int64, Int8 )
1617
import qualified Data.List.NonEmpty as NE
@@ -48,6 +49,7 @@ import Cardano.Metadata.Types.Common
4849
, File (File)
4950
, Subject (Subject)
5051
, attestedSignatures
52+
, attestedSequenceNumber
5153
, attestedValue
5254
, deserialiseAttestationSignature
5355
, deserialiseBase16
@@ -60,6 +62,8 @@ import Cardano.Metadata.Types.Common
6062
import Cardano.Metadata.Validation.Rules
6163
( ValidationError (ErrorMetadataFileBaseNameLengthBounds, ErrorMetadataFileExpectedExtension, ErrorMetadataFileNameDoesntMatchSubject, ErrorMetadataFileTooBig, ErrorMetadataPropertySequenceNumberMustBeLarger)
6264
, ValidationError_
65+
, defaultRules
66+
, apply
6367
, baseFileNameLengthBounds
6468
, isJSONFile
6569
, maxFileSizeBytes
@@ -93,6 +97,7 @@ tests = testGroup "Validation tests"
9397
, testProperty "Validation/rules/isJSONFile" prop_rules_isJSONFile
9498
, testProperty "Validation/rules/baseFileNameLengthBounds" prop_rules_baseFileNameLengthBounds
9599
, testProperty "Validation/helpers/toAttestedPropertyDiffs" prop_helpers_toAttestedPropertyDiffs
100+
, testCase "Unknown but well-formed properties have sequence numbers validated" unit_sequence_number_of_unknown_property_validated
96101
]
97102
]
98103

@@ -232,6 +237,34 @@ prop_rules_subjectMatchesFileName = property $ do
232237
subjectMatchesFileName goodDiff
233238
=== (valid :: Validation (NE.NonEmpty (ValidationError ())) ())
234239

240+
unit_sequence_number_of_unknown_property_validated :: Assertion
241+
unit_sequence_number_of_unknown_property_validated = do
242+
let
243+
before :: Metadata
244+
before = fromJust $ Aeson.decode' [r|
245+
{
246+
"subject": "1234",
247+
"prop": {
248+
"value": "hello",
249+
"sequenceNumber": 0
250+
}
251+
}
252+
|]
253+
after :: Metadata
254+
after = fromJust $ Aeson.decode' [r|
255+
{
256+
"subject": "1234",
257+
"prop": {
258+
"value": "goodbye",
259+
"sequenceNumber": 0
260+
}
261+
}
262+
|]
263+
diff = Changed (File before undefined undefined) (File after undefined undefined)
264+
265+
defaultRules `apply` diff
266+
@?= (Failure (ErrorMetadataPropertySequenceNumberMustBeLarger (AttestedProperty {attestedValue = Aeson.String "hello", attestedSignatures = [], attestedSequenceNumber = seqFromNatural 0}) (AttestedProperty {attestedValue = Aeson.String "goodbye", attestedSignatures = [], attestedSequenceNumber = seqFromNatural 0}) (seqFromNatural 0) (seqFromNatural 0) NE.:| []) :: Validation (NE.NonEmpty (ValidationError ())) ())
267+
235268
prop_rules_sequenceNumber :: H.Property
236269
prop_rules_sequenceNumber = property $ do
237270
-- Properties added or removed are always valid

shell.nix

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@ let
1717
mkShell = name: project: project.shellFor rec {
1818
inherit name;
1919
packages = ps: lib.attrValues (selectProjectPackages ps);
20-
buildInputs = (with metadataPackages; [
21-
metadata-server
22-
metadata-validator-github
23-
metadata-webhook
24-
token-metadata-creator
25-
]) ++ (with pkgs; [
20+
buildInputs = (with pkgs; [
2621
haskellPackages.ghcid
2722
git
2823
hlint

token-metadata-creator/app/Config.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import qualified Options.Applicative as OA
3535

3636
import qualified Data.Aeson as Aeson
3737
import qualified Data.Aeson.Types as Aeson
38+
import qualified Data.ByteString.Char8 as BC8
3839
import qualified Data.Text as T
3940
import qualified Text.Megaparsec as P
4041

@@ -56,6 +57,7 @@ data AttestationField
5657
| AttestationFieldLogo
5758
| AttestationFieldUrl
5859
| AttestationFieldTicker
60+
| AttestationFieldDecimals
5961
deriving (Show, Eq, Ord)
6062

6163
data FileInfo = FileInfo
@@ -110,12 +112,14 @@ entryUpdateArgumentParser defaultSubject = EntryUpdateArguments
110112
, OA.flag' [AttestationFieldLogo] $ OA.long "attest-logo" <> OA.short 'L'
111113
, OA.flag' [AttestationFieldUrl] $ OA.long "attest-url" <> OA.short 'H'
112114
, OA.flag' [AttestationFieldTicker] $ OA.long "attest-ticker" <> OA.short 'T'
115+
, OA.flag' [AttestationFieldDecimals] $ OA.long "attest-decimals"
113116
, pure
114117
[ AttestationFieldName
115118
, AttestationFieldDescription
116119
, AttestationFieldLogo
117120
, AttestationFieldUrl
118121
, AttestationFieldTicker
122+
, AttestationFieldDecimals
119123
]
120124
]
121125

@@ -150,6 +154,7 @@ entryUpdateArgumentParser defaultSubject = EntryUpdateArguments
150154
<*> pure Nothing -- logo
151155
<*> optional (emptyAttested <$> wellKnownOption (OA.long "url" <> OA.short 'h' <> OA.metavar "URL"))
152156
<*> optional (emptyAttested <$> wellKnownOption (OA.long "ticker" <> OA.short 't' <> OA.metavar "TICKER"))
157+
<*> optional (emptyAttested <$> OA.option (OA.eitherReader ((Aeson.parseEither parseWellKnown =<<) . Aeson.eitherDecodeStrict . BC8.pack)) (OA.long "decimals" <> OA.metavar "DECIMALS"))
153158

154159
pLogSeverity :: OA.Parser Colog.Severity
155160
pLogSeverity = pDebug <|> pInfo <|> pWarning <|> pError <|> pure I

token-metadata-creator/app/token-metadata-creator.hs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import Cardano.Metadata.Validation.Wallet
7373

7474
import Config
7575
( Arguments (ArgumentsEntryUpdate, ArgumentsValidate)
76-
, AttestationField (AttestationFieldDescription, AttestationFieldLogo, AttestationFieldName, AttestationFieldTicker, AttestationFieldUrl)
76+
, AttestationField (AttestationFieldDescription, AttestationFieldLogo, AttestationFieldName, AttestationFieldTicker, AttestationFieldUrl, AttestationFieldDecimals)
7777
, DraftStatus (DraftStatusDraft, DraftStatusFinal)
7878
, EntryOperation (EntryOperationInitialize, EntryOperationRevise)
7979
, EntryUpdateArguments (EntryUpdateArguments)
@@ -123,6 +123,8 @@ combineRegistryEntries new old = GoguenRegistryEntry
123123
_goguenRegistryEntry_url new `combineAttestedEntry` _goguenRegistryEntry_url old
124124
, _goguenRegistryEntry_ticker =
125125
_goguenRegistryEntry_ticker new `combineAttestedEntry` _goguenRegistryEntry_ticker old
126+
, _goguenRegistryEntry_decimals =
127+
_goguenRegistryEntry_decimals new `combineAttestedEntry` _goguenRegistryEntry_decimals old
126128
}
127129
where
128130
combineAttestedEntry a b = case (a, b) of
@@ -153,6 +155,8 @@ attestFields (SomeSigningKey someSigningKey) props old = do
153155
attestField AttestationFieldUrl subj <$> _goguenRegistryEntry_url old
154156
, _goguenRegistryEntry_ticker =
155157
attestField AttestationFieldTicker subj <$> _goguenRegistryEntry_ticker old
158+
, _goguenRegistryEntry_decimals =
159+
attestField AttestationFieldDecimals subj <$> _goguenRegistryEntry_decimals old
156160
}
157161
where
158162
attestField
@@ -189,6 +193,7 @@ handleEntryUpdateArguments (EntryUpdateArguments fInfo keyfile props newEntryInf
189193
, _goguenRegistryEntry_logo = Nothing
190194
, _goguenRegistryEntry_url = Nothing
191195
, _goguenRegistryEntry_ticker = Nothing
196+
, _goguenRegistryEntry_decimals = Nothing
192197
}
193198

194199
policy <- case policyM of

token-metadata-creator/src/Cardano/Metadata/GoguenRegistry.hs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Cardano.Metadata.Types
2121
, Subject (..)
2222
, Ticker (..)
2323
, Url (..)
24+
, Decimals (..)
2425
, WellKnownProperty (..)
2526
, evaluatePolicy
2627
, hashesForAttestation
@@ -53,6 +54,7 @@ data GoguenRegistryEntry f = GoguenRegistryEntry
5354
, _goguenRegistryEntry_logo :: f (Attested Logo)
5455
, _goguenRegistryEntry_url :: f (Attested Url)
5556
, _goguenRegistryEntry_ticker :: f (Attested Ticker)
57+
, _goguenRegistryEntry_decimals :: f (Attested Decimals)
5658
}
5759

5860
deriving instance
@@ -63,8 +65,20 @@ deriving instance
6365
, Show (f (Attested Logo))
6466
, Show (f (Attested Url))
6567
, Show (f (Attested Ticker))
68+
, Show (f (Attested Decimals))
6669
) => Show (GoguenRegistryEntry f)
6770

71+
deriving instance
72+
( Eq (f Subject)
73+
, Eq (f Policy)
74+
, Eq (f (Attested Name))
75+
, Eq (f (Attested Description))
76+
, Eq (f (Attested Logo))
77+
, Eq (f (Attested Url))
78+
, Eq (f (Attested Ticker))
79+
, Eq (f (Attested Decimals))
80+
) => Eq (GoguenRegistryEntry f)
81+
6882
instance ToJSON (GoguenRegistryEntry Maybe) where
6983
toJSON r = Aeson.object $ mconcat
7084
[ [ "subject" .= _goguenRegistryEntry_subject r
@@ -82,6 +96,8 @@ instance ToJSON (GoguenRegistryEntry Maybe) where
8296
<$> (_goguenRegistryEntry_url r)
8397
, (\x -> unProperty (wellKnownPropertyName (Proxy @Ticker)) .= fmap wellKnownToJSON x)
8498
<$> (_goguenRegistryEntry_ticker r)
99+
, (\x -> unProperty (wellKnownPropertyName (Proxy @Decimals)) .= fmap wellKnownToJSON x)
100+
<$> (_goguenRegistryEntry_decimals r)
85101
]
86102
]
87103

@@ -96,17 +112,19 @@ parseRegistryEntry = Aeson.withObject "GoguenRegistryEntry" $ \o -> do
96112
policyRaw <- o .:? unProperty (wellKnownPropertyName $ Proxy @Policy)
97113
policy <- mapM parseWellKnown policyRaw
98114

99-
nameField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Name)
100-
descField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Description)
101-
logoField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Logo)
102-
urlField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Url)
103-
tickerField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Ticker)
115+
nameField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Name)
116+
descField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Description)
117+
logoField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Logo)
118+
urlField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Url)
119+
tickerField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Ticker)
120+
decimalsField <- o .:? unProperty (wellKnownPropertyName $ Proxy @Decimals)
104121

105122
nameAnn <- mapM parseWithAttestation nameField
106123
descAnn <- mapM parseWithAttestation descField
107124
logoAnn <- mapM parseWithAttestation logoField
108125
urlAnn <- mapM parseWithAttestation urlField
109126
tickerAnn <- mapM parseWithAttestation tickerField
127+
decimalsAnn <- mapM parseWithAttestation decimalsField
110128

111129
pure $ GoguenRegistryEntry
112130
{ _goguenRegistryEntry_subject = Subject <$> subject
@@ -116,6 +134,7 @@ parseRegistryEntry = Aeson.withObject "GoguenRegistryEntry" $ \o -> do
116134
, _goguenRegistryEntry_logo = logoAnn
117135
, _goguenRegistryEntry_url = urlAnn
118136
, _goguenRegistryEntry_ticker = tickerAnn
137+
, _goguenRegistryEntry_decimals = decimalsAnn
119138
}
120139

121140
validateEntry
@@ -151,6 +170,7 @@ validateEntry record = do
151170
forM_ (_goguenRegistryEntry_logo record) $ verifyLocalAttestations "logo"
152171
forM_ (_goguenRegistryEntry_url record) $ verifyLocalAttestations "url"
153172
forM_ (_goguenRegistryEntry_ticker record) $ verifyLocalAttestations "ticker"
173+
forM_ (_goguenRegistryEntry_decimals record) $ verifyLocalAttestations "decimals"
154174
where
155175
verifyField :: (PartialGoguenRegistryEntry -> Maybe a) -> Either Text a
156176
verifyField field = maybe (Left missingFields) Right (field record)
@@ -159,7 +179,7 @@ validateEntry record = do
159179
missingFields = mconcat
160180
[ missingField "Missing field subject"
161181
_goguenRegistryEntry_subject
162-
, missingField "Missing field policy: Use -p to speciy"
182+
, missingField "Missing field policy: Use -p to specify"
163183
_goguenRegistryEntry_policy
164184
, missingField "Missing field name: Use -n to specify"
165185
_goguenRegistryEntry_name

token-metadata-creator/src/Cardano/Metadata/Types.hs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{-# LANGUAGE CPP #-}
22
{-# LANGUAGE DataKinds #-}
3+
{-# LANGUAGE StandaloneDeriving #-}
34
{-# LANGUAGE DeriveTraversable #-}
45
{-# LANGUAGE DerivingStrategies #-}
56
{-# LANGUAGE FlexibleContexts #-}
@@ -33,6 +34,7 @@ module Cardano.Metadata.Types
3334
, Logo (..)
3435
, Url(..)
3536
, Ticker(..)
37+
, Decimals(..)
3638

3739
-- * Attestation
3840
, Attested (..)
@@ -96,6 +98,7 @@ import Control.Category
9698
( id )
9799
import Control.Monad.Fail
98100
( fail )
101+
import Data.Scientific (toBoundedInteger, Scientific)
99102
import Data.Aeson
100103
( FromJSON (..), ToJSON (..), (.:), (.=) )
101104
import Data.Maybe
@@ -131,7 +134,7 @@ import qualified Shelley.Spec.Ledger.Keys as Shelley
131134
--
132135

133136
newtype Subject = Subject { unSubject :: Text }
134-
deriving stock Show
137+
deriving stock (Eq, Show)
135138
deriving newtype (ToJSON)
136139

137140
hashSubject :: Subject -> Hash Blake2b_256 Subject
@@ -158,10 +161,23 @@ newtype Property = Property { unProperty :: Text }
158161
hashProperty :: Property -> Hash Blake2b_256 Property
159162
hashProperty = hashWith (CBOR.toStrictByteString . CBOR.encodeString . unProperty)
160163

164+
newtype Decimals = Decimals { unDecimals :: Int }
165+
deriving (Eq, Show)
166+
167+
instance WellKnownProperty Decimals where
168+
wellKnownPropertyName _ =
169+
Property "decimals"
170+
wellKnownToBytes =
171+
CBOR.encodeInt . unDecimals
172+
wellKnownToJSON =
173+
toJSON . unDecimals
174+
parseWellKnown =
175+
Aeson.withScientific "decimals" validateMetadataDecimals
176+
161177
data Policy = Policy
162178
{ rawPolicy :: Text
163179
, getPolicy :: ScriptInEra MaryEra
164-
} deriving Show
180+
} deriving (Eq, Show)
165181

166182
instance WellKnownProperty Policy where
167183
wellKnownPropertyName _ =
@@ -372,6 +388,13 @@ validateMetadataDescription :: MonadFail f => Text -> f Description
372388
validateMetadataDescription = fmap Description .
373389
validateMaxLength 500
374390

391+
validateMetadataDecimals :: MonadFail f => Scientific -> f Decimals
392+
validateMetadataDecimals s =
393+
case toBoundedInteger s of
394+
Nothing -> fail $ "Expected integral, but encountered float: " <> show s
395+
Just i | i >= 0 && i <= 255 -> pure $ Decimals i
396+
Just _ | otherwise -> fail $ "Decimal value must be in the range [0, 255] (inclusive)"
397+
375398
validateMetadataLogo :: MonadFail f => ByteString -> f Logo
376399
validateMetadataLogo bytes
377400
| len <= maxLen =
@@ -404,7 +427,7 @@ data Attested a = Attested
404427
{ _attested_signatures :: [AttestationSignature]
405428
, _attested_sequence_number :: SequenceNumber
406429
, _attested_property :: a
407-
} deriving (Functor, Show)
430+
} deriving (Eq, Functor, Show)
408431

409432
instance ToJSON a => ToJSON (Attested a) where
410433
toJSON a = Aeson.object
@@ -466,7 +489,7 @@ parseWithAttestation = Aeson.withObject "property with attestation" $ \o -> do
466489
data AttestationSignature = AttestationSignature
467490
{ _attestationSignature_publicKey :: VerKeyDSIGN Ed25519DSIGN
468491
, _attestationSignature_signature :: SigDSIGN Ed25519DSIGN
469-
} deriving Show
492+
} deriving (Eq, Show)
470493

471494
instance ToJSON AttestationSignature where
472495
toJSON a = Aeson.object
@@ -513,10 +536,12 @@ hashSequenceNumber =
513536

514537
data SomeSigningKey where
515538
SomeSigningKey
516-
:: forall keyrole. (MakeAttestationSignature keyrole)
539+
:: forall keyrole. (MakeAttestationSignature keyrole, Show (SigningKey keyrole))
517540
=> SigningKey keyrole
518541
-> SomeSigningKey
519542

543+
deriving instance Show (SomeSigningKey)
544+
520545
-- | Hashes required to produce a message for attestation purposes
521546
data HashesForAttestation = HashesForAttestation
522547
{ _hashesForAttestation_subject :: Hash Blake2b_256 Subject

0 commit comments

Comments
 (0)