diff --git a/lib/core-integration/src/Test/Integration/Framework/TestData.hs b/lib/core-integration/src/Test/Integration/Framework/TestData.hs index fe9ec9f71cc..961f0c497f5 100644 --- a/lib/core-integration/src/Test/Integration/Framework/TestData.hs +++ b/lib/core-integration/src/Test/Integration/Framework/TestData.hs @@ -187,7 +187,7 @@ steveToken = ApiAssetMetadata "SteveToken" "A sample description" (Just "STV") (Just (ApiT (unsafeFromText "https://iohk.io/stevetoken"))) (Just (ApiT (W.AssetLogo "Almost a logo"))) - (Just (ApiT (W.AssetUnit "MegaSteve" 6))) + (Just (ApiT (W.AssetUnit "MegaSteve" 6 Nothing))) --- --- Helpers diff --git a/lib/core/src/Cardano/Wallet/Primitive/Types/TokenPolicy.hs b/lib/core/src/Cardano/Wallet/Primitive/Types/TokenPolicy.hs index 21fc8817f7e..2acfc4228b9 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/Types/TokenPolicy.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/Types/TokenPolicy.hs @@ -217,6 +217,7 @@ instance NFData AssetMetadata data AssetUnit = AssetUnit { name :: Text -- ^ Name of the larger asset. , decimals :: Natural -- ^ Number of zeroes to add to base unit. + , ticker :: Maybe Text -- ^ Optional abbreviation of larger asset name } deriving (Generic, Show, Eq, Ord) instance NFData AssetUnit @@ -261,7 +262,7 @@ validateMetadataName :: Text -> Either String Text validateMetadataName = validateMinLength 1 >=> validateMaxLength 50 validateMetadataTicker :: Text -> Either String Text -validateMetadataTicker = validateMinLength 2 >=> validateMaxLength 4 +validateMetadataTicker = validateMinLength 2 >=> validateMaxLength 5 validateMetadataDescription :: Text -> Either String Text validateMetadataDescription = validateMaxLength 500 @@ -278,8 +279,18 @@ validateMetadataURL = fmap AssetURL . | otherwise = Left $ "Scheme must be https: but got " ++ scheme validateMetadataUnit :: AssetUnit -> Either String AssetUnit -validateMetadataUnit assetUnit@AssetUnit{name} = - (validateMinLength 1 name >>= validateMaxLength 30) $> assetUnit +validateMetadataUnit = validateName >=> validateTicker >=> validateDecimals + where + validateName u@AssetUnit{name} = + (validateMinLength 1 name >>= validateMaxLength 30) $> u + validateTicker u@AssetUnit{ticker} = + case fmap validateMetadataTicker ticker of + Just (Left e) -> Left e + _ -> Right u + validateDecimals u@AssetUnit{decimals} + | decimals > 0 && decimals < 20 = Right u + | otherwise = + Left "AssetUnit decimals must be greater than 0 and less than 20" validateMetadataLogo :: AssetLogo -> Either String AssetLogo validateMetadataLogo logo diff --git a/lib/core/src/Cardano/Wallet/TokenMetadata.hs b/lib/core/src/Cardano/Wallet/TokenMetadata.hs index 356304e6a42..9d5acbc16d4 100644 --- a/lib/core/src/Cardano/Wallet/TokenMetadata.hs +++ b/lib/core/src/Cardano/Wallet/TokenMetadata.hs @@ -238,6 +238,8 @@ data Property name = Property -- ^ The result of JSON parsing and validating the property value. , signatures :: [Signature] -- ^ Zero or more signatures of the property value. + , sequenceNumber :: Int + -- ^ Counter to prevent replaying old signatures. } deriving (Generic) propertyName :: forall name. KnownSymbol name => Property name -> PropertyName @@ -268,7 +270,6 @@ type instance PropertyValue "logo" = AssetLogo class HasValidator (name :: Symbol) where -- TODO: requires AllowAmbiguousTypes extension validatePropertyValue :: PropertyValue name -> Either String (PropertyValue name) - validatePropertyValue = Right instance HasValidator "name" where validatePropertyValue = validateMetadataName @@ -278,6 +279,7 @@ instance HasValidator "ticker" where validatePropertyValue = validateMetadataTicker instance HasValidator "url" where -- validation is done before parsing + validatePropertyValue = Right instance HasValidator "logo" where validatePropertyValue = validateMetadataLogo instance HasValidator "unit" where @@ -553,7 +555,8 @@ instance FromJSON SubjectProperties where instance (HasValidator name, FromJSON (PropertyValue name)) => FromJSON (Property name) where parseJSON = withObject "Property value" $ \o -> Property <$> (validate <$> o .: "value") - <*> o .:? "anSignatures" .!= [] + <*> o .:? "signatures" .!= [] + <*> o .:? "sequenceNumber" .!= 0 where validate v = first (,v) $ (>>= validatePropertyValue @name) $ tryParse v tryParse = resultToEither . fromJSON @@ -581,6 +584,7 @@ instance FromJSON AssetUnit where parseJSON = withObject "AssetUnit" $ \o -> AssetUnit <$> o .: "name" <*> o .: "decimals" + <*> o .:? "ticker" -- -- Helpers diff --git a/lib/core/src/Cardano/Wallet/TokenMetadata/MockServer.hs b/lib/core/src/Cardano/Wallet/TokenMetadata/MockServer.hs index 883eaee4d13..7287c14bff0 100644 --- a/lib/core/src/Cardano/Wallet/TokenMetadata/MockServer.hs +++ b/lib/core/src/Cardano/Wallet/TokenMetadata/MockServer.hs @@ -218,9 +218,11 @@ instance ToJSON SubjectProperties where optionals = filter ((/= Null) . snd) instance ToJSON (PropertyValue name) => ToJSON (Property name) where - toJSON (Property v s) = object + toJSON (Property v s c) = object [ "value" .= either snd toJSON v - , "anSignatures" .= s ] + , "signatures" .= s + , "sequenceNumber" .= c + ] instance ToJSON Signature where toJSON (Signature s k) = object diff --git a/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden1.json b/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden1.json index 4defe3f875e..a6a20992647 100644 --- a/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden1.json +++ b/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden1.json @@ -9,7 +9,8 @@ }, "name": { "value": "SteveToken", - "anSignatures": [ + "sequenceNumber": 1, + "signatures": [ { "signature": "7ef6ed44ba9456737ef8d2e31596fdafb66d5775ac1a254086a553b666516e5895bb0c6b7ba8bef1f6b4d9bd9253b4449d1354de2f9e043ea4eb43fd42f87108", "publicKey": "0ee262f062528667964782777917cd7139e19e8eb2c591767629e4200070c661" @@ -34,7 +35,7 @@ }, "description": { "value": "A sample description", - "anSignatures": [ + "signatures": [ { "signature": "83ef5c04882e43e5f1c8e9bc386bd51cdda163f5cbd1996d1d066238de063d4b79b1648b48aec63dddff05649911ca116579842c8e9a08a3bc7ae1a0ec7ef000", "publicKey": "1446c9d327b0f07aa691014c08578867674f3a88b36f2017a58c37a8a7799058" @@ -62,7 +63,7 @@ "subject": "missing sigs", "name": { "value": "Token1", - "anSignatures": [] + "signatures": [] }, "description": { "value": "description1" diff --git a/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden2.json b/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden2.json index 889cc63366d..a508461ea09 100644 --- a/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden2.json +++ b/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden2.json @@ -9,7 +9,7 @@ }, "name": { "value": "SteveToken", - "anSignatures": [ + "signatures": [ { "signature": "7ef6ed44ba9456737ef8d2e31596fdafb66d5775ac1a254086a553b666516e5895bb0c6b7ba8bef1f6b4d9bd9253b4449d1354de2f9e043ea4eb43fd42f87108", "publicKey": "0ee262f062528667964782777917cd7139e19e8eb2c591767629e4200070c661" @@ -34,7 +34,7 @@ }, "description": { "value": "A sample description", - "anSignatures": [ + "signatures": [ { "signature": "83ef5c04882e43e5f1c8e9bc386bd51cdda163f5cbd1996d1d066238de063d4b79b1648b48aec63dddff05649911ca116579842c8e9a08a3bc7ae1a0ec7ef000", "publicKey": "1446c9d327b0f07aa691014c08578867674f3a88b36f2017a58c37a8a7799058" @@ -62,10 +62,14 @@ "subject": "missing sigs", "name": { "value": "Token1", - "anSignatures": [] + "signatures": [] }, "description": { "value": "description1" + }, + "ticker": { + "value": "tck1", + "sequenceNumber": 2 } }, { "subject": "bad00000000000000000000000000000000000000000000000000000", @@ -75,9 +79,22 @@ }, { "subject": "extra fields", - "name": { "value": "Token2" }, + "name": { "value": "Token2", "hello": true }, "description": { "value": "description2" }, - "ticker": { "value": "ticker2" } + "ticker": { "value": "ticker2" }, + "custom": { "value": "meta" } + }, + { + "subject": "unit with ticker", + "name": { "value": "Token3" }, + "description": { "value": "description3" }, + "unit": { + "value": { + "name": "BigToken3", + "decimals": 3, + "ticker": "tck3" + } + } } ] } diff --git a/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden4.json b/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden4.json index 35a4ef76154..c40636ab1bf 100644 --- a/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden4.json +++ b/lib/core/test/data/Cardano/Wallet/TokenMetadata/golden4.json @@ -15,7 +15,7 @@ { "subject": "malformed unit - wrong type", "name": { - "value": "token9" + "value": "Token9" }, "description": { "value": "description9" @@ -27,7 +27,7 @@ { "subject": "malformed unit - name too long", "name": { - "value": "token10" + "value": "Token10" }, "description": { "value": "description10" @@ -42,7 +42,7 @@ { "subject": "malformed url - wrong scheme", "name": { - "value": "token11" + "value": "Token11" }, "description": { "value": "description11" @@ -50,6 +50,30 @@ "url": { "value": "javascript:window.alert('hello')" } + }, + { + "subject": "malformed ticker - too short", + "name": { + "value": "Token12" + }, + "description": { + "value": "description12" + }, + "ticker": { + "value": "x" + } + }, + { + "subject": "malformed ticker - too long", + "name": { + "value": "Token13" + }, + "description": { + "value": "description13" + }, + "ticker": { + "value": "ticker13" + } } ] } diff --git a/lib/core/test/unit/Cardano/Wallet/TokenMetadataSpec.hs b/lib/core/test/unit/Cardano/Wallet/TokenMetadataSpec.hs index 2d9e784bc7f..fa437035501 100644 --- a/lib/core/test/unit/Cardano/Wallet/TokenMetadataSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/TokenMetadataSpec.hs @@ -39,7 +39,7 @@ import Data.Aeson import Data.Either ( isRight ) import Data.Maybe - ( isJust, isNothing ) + ( isNothing ) import Network.URI ( parseURI ) import System.FilePath @@ -55,44 +55,53 @@ spec :: Spec spec = do describe "JSON decoding" $ do describe "BatchResponse" $ do - it "golden1.json" $ do + it "golden1.json - Simple valid WKP" $ do decodeGoldenBatch golden1File `shouldReturn` golden1Properties - it "golden2.json" $ do + it "golden2.json - Valid WKP" $ do rs <- decodeGoldenBatch (dir "golden2.json") - length rs `shouldBe` 4 + length rs `shouldBe` 5 - it "golden3.json" $ do + it "golden3.json - Required WKP are invalid" $ do rs <- decodeGoldenBatch (dir "golden3.json") - rs `shouldNotBe` [] + length rs `shouldBe` 5 - it "golden4.json" $ do + it "golden4.json - Non-required WKP are invalid" $ do rs <- decodeGoldenBatch (dir "golden4.json") - rs `shouldNotBe` [] + length rs `shouldBe` 6 describe "metadataFromProperties" $ do - it "golden1.json" $ do + it "golden1.json - Simple valid WKP" $ do map metadataFromProperties golden1Properties `shouldBe` (Just <$> [golden1Metadata0,golden1Metadata1,golden1Metadata2]) - it "golden2.json" $ do + it "golden2.json - Valid WKP" $ do rs <- decodeGoldenBatch (dir "golden2.json") map metadataFromProperties rs `shouldBe` [ Just golden1Metadata0 - , Just (AssetMetadata "Token1" "description1" Nothing Nothing Nothing Nothing) + , Just (AssetMetadata "Token1" "description1" (Just "tck1") Nothing Nothing Nothing) , Nothing , Just (AssetMetadata "Token2" "description2" Nothing Nothing Nothing Nothing) + , Just (AssetMetadata "Token3" "description3" Nothing Nothing Nothing (Just (AssetUnit "BigToken3" 3 (Just "tck3")))) ] - it "golden3.json" $ do + it "golden3.json - Required WKP are invalid" $ do rs <- decodeGoldenBatch (dir "golden3.json") rs `shouldNotBe` [] map metadataFromProperties rs `shouldSatisfy` all isNothing - it "golden4.json" $ do + it "golden4.json - Non-required WKP are invalid" $ do rs <- decodeGoldenBatch (dir "golden4.json") rs `shouldNotBe` [] - map metadataFromProperties rs `shouldSatisfy` all isJust + map metadataFromProperties rs `shouldBe` + map Just + [ AssetMetadata "Token7" "description7" Nothing Nothing Nothing Nothing + , AssetMetadata "Token9" "description9" Nothing Nothing Nothing Nothing + , AssetMetadata "Token10" "description10" Nothing Nothing Nothing Nothing + , AssetMetadata "Token11" "description11" Nothing Nothing Nothing Nothing + , AssetMetadata "Token12" "description12" Nothing Nothing Nothing Nothing + , AssetMetadata "Token13" "description13" Nothing Nothing Nothing Nothing + ] traceSpec $ describe "Using mock server" $ do it "testing empty req" $ \tr -> @@ -145,7 +154,7 @@ spec = do (Just "acr2") (AssetURL <$> parseURI "https://iohk.io") (Just $ AssetLogo $ unsafeFromBase64 "QWxtb3N0IGEgbG9nbw==") - (Just $ AssetUnit "unit2" 14) + (Just $ AssetUnit "unit2" 14 Nothing) golden2File = dir "golden2.json" sig s k = Signature (unsafeFromHex s) (unsafeFromHex k) @@ -161,6 +170,7 @@ spec = do , sig "f88692b13212bac8121151a99a4de4d5244e5f63566babd2b8ac20950ede74073af0570772b3ce3d11b72e972079199f02306e947cd5fcca688a9d4664eddb04" "8899d0777f399fffd44f72c85a8aa51605123a7ebf20bba42650780a0c81096a" , sig "c2b30fa5f2c09323d81e5050af681c023089d832d0b85d05f60f4278fba3011ab03e6bd9bd2b8649080a368ecfe51573cd232efe8f1e7ca69ff8334ced7b6801" "d40688a3eeda1f229c64efc56dd53b363ff981f71a7462f78c8cc444117a03db" ] + 1 , Just $ Property (Right "A sample description") [ sig "83ef5c04882e43e5f1c8e9bc386bd51cdda163f5cbd1996d1d066238de063d4b79b1648b48aec63dddff05649911ca116579842c8e9a08a3bc7ae1a0ec7ef000" "1446c9d327b0f07aa691014c08578867674f3a88b36f2017a58c37a8a7799058" , sig "4e29a00feaeb24b25315f0eac28bbfc550dabfb847bf6a06cb8086120201f90c64fab778037d0ef009ab4669121a38fe9b8c0a6aec99c68366c5187c0889520a" "1910312a9a6998c7e4f585dc138f85a90f50a28397b8ea05eb23355fb8ea4fa0" @@ -168,6 +178,7 @@ spec = do , sig "5a1d55048234d92057dfd1938f49935a33751ee604b7dbd02a315418ced6f0836a51107512b192eae6133403bb437c6850b1af1c62c3b17a372acce77adf9903" "57fa73123c3b39489c4d6c2ff3cab9952e56e556daab9f8f333bc5ca6984fa5e" , sig "e13c9ba5b084dc126d34f3f1120fff75495b64a41a98a69071b5c5ed01bb9d273f51d570cf4fdaa42969fa2c775c12ec05c496cd8f61323d343970136781f60e" "8cc8963b65ddd0a49f7ce1acc2915d8baff505bbc4f8727a22bd1d28f8ad6632" ] + 0 , Nothing , Nothing , Nothing @@ -178,8 +189,8 @@ spec = do { subject = "missing sigs" , owner = Nothing , properties = - ( Just $ Property (Right "Token1") [] - , Just $ Property (Right "description1") [] + ( Just $ Property (Right "Token1") [] 0 + , Just $ Property (Right "description1") [] 0 , Nothing , Nothing , Nothing @@ -190,12 +201,12 @@ spec = do { subject = "extra fields" , owner = Nothing , properties = - ( Just $ Property (Right "Token2") [] - , Just $ Property (Right "description2") [] - , Just $ Property (Right "acr2") [] - , Just $ Property (parseAssetURL "https://iohk.io") [] - , Just $ Property (Right (AssetLogo $ unsafeFromBase64 "QWxtb3N0IGEgbG9nbw==")) [] - , Just $ Property (Right (AssetUnit "unit2" 14)) [] + ( Just $ Property (Right "Token2") [] 0 + , Just $ Property (Right "description2") [] 0 + , Just $ Property (Right "acr2") [] 0 + , Just $ Property (parseAssetURL "https://iohk.io") [] 0 + , Just $ Property (Right (AssetLogo $ unsafeFromBase64 "QWxtb3N0IGEgbG9nbw==")) [] 0 + , Just $ Property (Right (AssetUnit "unit2" 14 Nothing)) [] 0 ) } ] diff --git a/lib/shelley/test/data/token-metadata.json b/lib/shelley/test/data/token-metadata.json index db51eedd3c7..67cd44fd2c2 100644 --- a/lib/shelley/test/data/token-metadata.json +++ b/lib/shelley/test/data/token-metadata.json @@ -12,7 +12,7 @@ }, "name": { "value": "SteveToken", - "anSignatures": [ + "signatures": [ { "signature": "7ef6ed44ba9456737ef8d2e31596fdafb66d5775ac1a254086a553b666516e5895bb0c6b7ba8bef1f6b4d9bd9253b4449d1354de2f9e043ea4eb43fd42f87108", "publicKey": "0ee262f062528667964782777917cd7139e19e8eb2c591767629e4200070c661" @@ -21,7 +21,7 @@ }, "description": { "value": "A sample description", - "anSignatures": [ + "signatures": [ { "signature": "83ef5c04882e43e5f1c8e9bc386bd51cdda163f5cbd1996d1d066238de063d4b79b1648b48aec63dddff05649911ca116579842c8e9a08a3bc7ae1a0ec7ef000", "publicKey": "1446c9d327b0f07aa691014c08578867674f3a88b36f2017a58c37a8a7799058" @@ -30,48 +30,48 @@ }, "ticker": { "value": "STV", - "anSignatures": [] + "signatures": [] }, "unit": { "value": { "decimals": 6, "name": "MegaSteve" }, - "anSignatures": [] + "signatures": [] }, "url": { "value": "https://iohk.io/stevetoken", - "anSignatures": [] + "signatures": [] }, "logo": { "value": "QWxtb3N0IGEgbG9nbw==", - "anSignatures": [] + "signatures": [] } }, { "subject": "f4137b0691b01c7ca46c2fc05576f4f0ab8eebb8f8e4946cb9107e0f6170706c65", "name": { "value": "Apple", - "anSignatures": [] + "signatures": [] }, "description": { "value": "A healthy kind of fruit", - "anSignatures": [] + "signatures": [] } }, { "subject": "f4137b0691b01c7ca46c2fc05576f4f0ab8eebb8f8e4946cb9107e0f62616e616e61", "name": { "value": "Banana", - "anSignatures": [] + "signatures": [] }, "description": { "value": "A yummy kind of fruit", - "anSignatures": [] + "signatures": [] } }, { "subject": "f4137b0691b01c7ca46c2fc05576f4f0ab8eebb8f8e4946cb9107e0f62636865727279", "name": { "value": "Cherry", - "anSignatures": [] + "signatures": [] }, "description": { "value": "A special kind of fruit" diff --git a/specifications/api/swagger.yaml b/specifications/api/swagger.yaml index 477ccff83ea..c1916419416 100644 --- a/specifications/api/swagger.yaml +++ b/specifications/api/swagger.yaml @@ -518,21 +518,25 @@ x-assetMetadataName: &assetMetadataName maxLength: 50 minLength: 1 description: | - A human-readable name for the asset. Good for display in user interfaces. + A human-readable name for the asset, intended for display in user + interfaces. example: SwaggerCoin x-assetMetadataTicker: &assetMetadataTicker type: string - maxLength: 4 + maxLength: 5 minLength: 2 description: | - A human-readable short name for the asset. Good for display in - user interfaces. + An optional human-readable very short name or acronym for the + asset, intended for display in user interfaces. If `ticker` is not + present, then `name` will be used, but it might be truncated to + fit within the available space. example: SWAG x-assetMetadataDescription: &assetMetadataDescription description: | - A human-readable description for the asset. Good for display in user interfaces. + A human-readable description for the asset. Good for display in + user interfaces. type: string maxLength: 500 @@ -559,6 +563,8 @@ x-assetMetadataUnit: &assetMetadataUnit description: | The human-readable name for the larger unit of the asset. Used for display in user interfaces. + ticker: *assetMetadataTicker + example: name: API decimals: 3