Skip to content

Commit 45fa413

Browse files
Merge #23
23: Support for decimals r=sevanspowell a=sevanspowell ADP-915 - Provide support for the decimals field in offchain-metadata-tools. - Add a test to demonstrate the _**existing**_ behaviour that sequence numbers present in unknown properties are validated. Co-authored-by: Samuel Evans-Powell <mail@sevanspowell.net>
2 parents 80e5bfa + 83866da commit 45fa413

File tree

10 files changed

+140
-30
lines changed

10 files changed

+140
-30
lines changed

metadata-lib/metadata-lib.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ test-suite unit-tests
103103
, QuickCheck
104104
, aeson
105105
, aeson-pretty
106+
, aeson-qq
106107
, base >=4.12 && <5
107108
, base64-bytestring
108109
, bytestring

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ module Test.Cardano.Metadata.Validation
99
) where
1010

1111
import qualified Data.Aeson as Aeson
12+
import qualified Data.Aeson.Encode.Pretty as Aeson
13+
import Data.Aeson.QQ
14+
( aesonQQ )
15+
import qualified Data.ByteString.Lazy as BSL
1216
import Data.Foldable
1317
( forM_, traverse_ )
1418
import Data.Int
@@ -47,6 +51,7 @@ import Cardano.Metadata.Types.Common
4751
, AttestedProperty (AttestedProperty)
4852
, File (File)
4953
, Subject (Subject)
54+
, attestedSequenceNumber
5055
, attestedSignatures
5156
, attestedValue
5257
, deserialiseAttestationSignature
@@ -56,11 +61,14 @@ import Cardano.Metadata.Types.Common
5661
, seqSucc
5762
, seqZero
5863
, unSequenceNumber
64+
, unSubject
5965
)
6066
import Cardano.Metadata.Validation.Rules
6167
( ValidationError (ErrorMetadataFileBaseNameLengthBounds, ErrorMetadataFileExpectedExtension, ErrorMetadataFileNameDoesntMatchSubject, ErrorMetadataFileTooBig, ErrorMetadataPropertySequenceNumberMustBeLarger)
6268
, ValidationError_
69+
, apply
6370
, baseFileNameLengthBounds
71+
, defaultRules
6472
, isJSONFile
6573
, maxFileSizeBytes
6674
, sequenceNumber
@@ -73,6 +81,7 @@ import Cardano.Metadata.Validation.Types
7381
, Metadata (Metadata)
7482
, invalid
7583
, metaAttestedProperties
84+
, metaSubject
7685
, metaVerifiableProperties
7786
, onMatchingAttestedProperties
7887
, valid
@@ -93,9 +102,25 @@ tests = testGroup "Validation tests"
93102
, testProperty "Validation/rules/isJSONFile" prop_rules_isJSONFile
94103
, testProperty "Validation/rules/baseFileNameLengthBounds" prop_rules_baseFileNameLengthBounds
95104
, testProperty "Validation/helpers/toAttestedPropertyDiffs" prop_helpers_toAttestedPropertyDiffs
105+
, testCase "Unknown but well-formed properties have sequence numbers validated" unit_sequence_number_of_unknown_property_validated
96106
]
97107
]
98108

109+
-- | From a JSON value, parse some Metadata and use the length of the
110+
-- pretty encoded JSON value and parsed Subject to make a well-formed
111+
-- File.
112+
asMetadataFile :: Aeson.Value -> File Metadata
113+
asMetadataFile json =
114+
let
115+
-- Prettily print the JSON to get a realistic and consistent file size
116+
fileSize = BSL.length (Aeson.encodePretty json)
117+
metadata = case Aeson.fromJSON json of
118+
Aeson.Error err -> error err
119+
Aeson.Success a -> a
120+
fileName = unSubject (metaSubject metadata) <> ".json"
121+
in
122+
File metadata (fromIntegral fileSize) (T.unpack fileName)
123+
99124
prop_rules_isJSONFile :: H.Property
100125
prop_rules_isJSONFile = property $ do
101126
-- All files with the ".json" extension should pass
@@ -232,6 +257,32 @@ prop_rules_subjectMatchesFileName = property $ do
232257
subjectMatchesFileName goodDiff
233258
=== (valid :: Validation (NE.NonEmpty (ValidationError ())) ())
234259

260+
unit_sequence_number_of_unknown_property_validated :: Assertion
261+
unit_sequence_number_of_unknown_property_validated = do
262+
let
263+
before = [aesonQQ|
264+
{
265+
"subject": "1234",
266+
"prop": {
267+
"value": "hello",
268+
"sequenceNumber": 0
269+
}
270+
}
271+
|]
272+
after = [aesonQQ|
273+
{
274+
"subject": "1234",
275+
"prop": {
276+
"value": "goodbye",
277+
"sequenceNumber": 0
278+
}
279+
}
280+
|]
281+
diff = Changed (asMetadataFile before) (asMetadataFile after)
282+
283+
defaultRules `apply` diff
284+
@?= (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 ())) ())
285+
235286
prop_rules_sequenceNumber :: H.Property
236287
prop_rules_sequenceNumber = property $ do
237288
-- 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 (AttestationFieldDecimals, AttestationFieldDescription, AttestationFieldLogo, AttestationFieldName, AttestationFieldTicker, AttestationFieldUrl)
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
@@ -12,6 +12,7 @@ import Cardano.Prelude
1212

1313
import Cardano.Metadata.Types
1414
( Attested (..)
15+
, Decimals (..)
1516
, Description (..)
1617
, Logo (..)
1718
, Logo (..)
@@ -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: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
{-# LANGUAGE LambdaCase #-}
99
{-# LANGUAGE RankNTypes #-}
1010
{-# LANGUAGE ScopedTypeVariables #-}
11+
{-# LANGUAGE StandaloneDeriving #-}
1112
{-# LANGUAGE TypeApplications #-}
1213
{-# LANGUAGE ViewPatterns #-}
1314

@@ -33,6 +34,7 @@ module Cardano.Metadata.Types
3334
, Logo (..)
3435
, Url(..)
3536
, Ticker(..)
37+
, Decimals(..)
3638

3739
-- * Attestation
3840
, Attested (..)
@@ -131,7 +133,7 @@ import qualified Shelley.Spec.Ledger.Keys as Shelley
131133
--
132134

133135
newtype Subject = Subject { unSubject :: Text }
134-
deriving stock Show
136+
deriving stock (Eq, Show)
135137
deriving newtype (ToJSON)
136138

137139
hashSubject :: Subject -> Hash Blake2b_256 Subject
@@ -158,10 +160,23 @@ newtype Property = Property { unProperty :: Text }
158160
hashProperty :: Property -> Hash Blake2b_256 Property
159161
hashProperty = hashWith (CBOR.toStrictByteString . CBOR.encodeString . unProperty)
160162

163+
newtype Decimals = Decimals { unDecimals :: Int }
164+
deriving (Eq, Show)
165+
166+
instance WellKnownProperty Decimals where
167+
wellKnownPropertyName _ =
168+
Property "decimals"
169+
wellKnownToBytes =
170+
CBOR.encodeInt . unDecimals
171+
wellKnownToJSON =
172+
toJSON . unDecimals
173+
parseWellKnown =
174+
parseJSON >=> validateMetadataDecimals
175+
161176
data Policy = Policy
162177
{ rawPolicy :: Text
163178
, getPolicy :: ScriptInEra MaryEra
164-
} deriving Show
179+
} deriving (Eq, Show)
165180

166181
instance WellKnownProperty Policy where
167182
wellKnownPropertyName _ =
@@ -372,6 +387,11 @@ validateMetadataDescription :: MonadFail f => Text -> f Description
372387
validateMetadataDescription = fmap Description .
373388
validateMaxLength 500
374389

390+
validateMetadataDecimals :: MonadFail f => Int -> f Decimals
391+
validateMetadataDecimals i
392+
| i >= 0 && i <= 255 = pure $ Decimals i
393+
| otherwise = fail $ "Decimal value must be in the range [0, 255] (inclusive)"
394+
375395
validateMetadataLogo :: MonadFail f => ByteString -> f Logo
376396
validateMetadataLogo bytes
377397
| len <= maxLen =
@@ -404,7 +424,7 @@ data Attested a = Attested
404424
{ _attested_signatures :: [AttestationSignature]
405425
, _attested_sequence_number :: SequenceNumber
406426
, _attested_property :: a
407-
} deriving (Functor, Show)
427+
} deriving (Eq, Functor, Show)
408428

409429
instance ToJSON a => ToJSON (Attested a) where
410430
toJSON a = Aeson.object
@@ -466,7 +486,7 @@ parseWithAttestation = Aeson.withObject "property with attestation" $ \o -> do
466486
data AttestationSignature = AttestationSignature
467487
{ _attestationSignature_publicKey :: VerKeyDSIGN Ed25519DSIGN
468488
, _attestationSignature_signature :: SigDSIGN Ed25519DSIGN
469-
} deriving Show
489+
} deriving (Eq, Show)
470490

471491
instance ToJSON AttestationSignature where
472492
toJSON a = Aeson.object
@@ -513,10 +533,12 @@ hashSequenceNumber =
513533

514534
data SomeSigningKey where
515535
SomeSigningKey
516-
:: forall keyrole. (MakeAttestationSignature keyrole)
536+
:: forall keyrole. (MakeAttestationSignature keyrole, Show (SigningKey keyrole))
517537
=> SigningKey keyrole
518538
-> SomeSigningKey
519539

540+
deriving instance Show (SomeSigningKey)
541+
520542
-- | Hashes required to produce a message for attestation purposes
521543
data HashesForAttestation = HashesForAttestation
522544
{ _hashesForAttestation_subject :: Hash Blake2b_256 Subject

0 commit comments

Comments
 (0)