Skip to content

Commit

Permalink
add fingerprint to all asset models next to the policy_id and asset_name
Browse files Browse the repository at this point in the history
  Motivation, design and rationale explained in: cardano-foundation/CIPs#64
  • Loading branch information
KtorZ committed Feb 11, 2021
1 parent 247b5f6 commit 9f446af
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/core/cardano-wallet-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ test-suite unit
Cardano.Wallet.Primitive.Types.TokenBundleSpec
Cardano.Wallet.Primitive.Types.TokenMapSpec
Cardano.Wallet.Primitive.Types.TokenMapSpec.TypeErrorSpec
Cardano.Wallet.Primitive.Types.TokenPolicySpec
Cardano.Wallet.Primitive.Types.TokenQuantitySpec
Cardano.Wallet.Primitive.Types.UTxOIndexSpec
Cardano.Wallet.Primitive.Types.UTxOIndex.TypeErrorSpec
Expand Down
7 changes: 7 additions & 0 deletions lib/core/src/Cardano/Wallet/Api/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ newtype ApiMaintenanceAction = ApiMaintenanceAction
data ApiAsset = ApiAsset
{ policyId :: ApiT W.TokenPolicyId
, assetName :: ApiT W.TokenName
, fingerprint :: ApiT W.TokenFingerprint
, metadata :: Maybe (ApiT W.AssetMetadata)
} deriving (Eq, Generic, Ord, Show)
deriving anyclass NFData
Expand All @@ -457,6 +458,7 @@ toApiAsset :: Maybe W.AssetMetadata -> W.AssetId -> ApiAsset
toApiAsset metadata_ (W.AssetId policyId_ assetName_) = ApiAsset
{ policyId = ApiT policyId_
, assetName = ApiT assetName_
, fingerprint = ApiT $ W.mkTokenFingerprint policyId_ assetName_
, metadata = ApiT <$> metadata_
}

Expand Down Expand Up @@ -1277,6 +1279,11 @@ instance FromJSON (ApiT W.TokenName) where
instance ToJSON (ApiT W.TokenName) where
toJSON = toJSON . hexText . W.unTokenName . getApiT

instance FromJSON (ApiT W.TokenFingerprint) where
parseJSON = fromTextJSON "TokenFingerprint"
instance ToJSON (ApiT W.TokenFingerprint) where
toJSON = toTextJSON

instance FromJSON (ApiT W.AssetMetadata) where
parseJSON = fmap ApiT . genericParseJSON defaultRecordTypeOptions
instance ToJSON (ApiT W.AssetMetadata) where
Expand Down
58 changes: 58 additions & 0 deletions lib/core/src/Cardano/Wallet/Primitive/Types/TokenPolicy.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeApplications #-}

module Cardano.Wallet.Primitive.Types.TokenPolicy
(
Expand All @@ -15,6 +17,10 @@ module Cardano.Wallet.Primitive.Types.TokenPolicy
, nullTokenName
, maxLengthTokenName

-- * Token Fingerprints
, TokenFingerprint (..)
, mkTokenFingerprint

-- * Token Metadata
, AssetMetadata (..)
, AssetLogo (..)
Expand All @@ -25,18 +31,28 @@ import Prelude

import Cardano.Wallet.Primitive.Types.Hash
( Hash (..) )
import Codec.Binary.Bech32.TH
( humanReadablePart )
import Control.DeepSeq
( NFData )
import Control.Monad
( (>=>) )
import Crypto.Hash
( hash )
import Crypto.Hash.Algorithms
( Blake2b_160 )
import Data.Aeson
( FromJSON (..), ToJSON (..) )
import Data.Bifunctor
( first )
import Data.ByteArray
( convert )
import Data.ByteArray.Encoding
( Base (Base16), convertFromBase, convertToBase )
import Data.ByteString
( ByteString )
import Data.Function
( (&) )
import Data.Hashable
( Hashable )
import Data.Text
Expand All @@ -52,9 +68,11 @@ import Numeric.Natural
import Quiet
( Quiet (..) )

import qualified Codec.Binary.Bech32 as Bech32
import qualified Data.ByteString as BS
import qualified Data.Text.Encoding as T


-- | Token policy identifiers, represented by the hash of the monetary policy
-- script.
newtype TokenPolicyId =
Expand Down Expand Up @@ -128,6 +146,46 @@ instance FromText TokenName where
. convertFromBase Base16
. T.encodeUtf8

newtype TokenFingerprint =
UnsafeTokenFingerprint { unTokenFingerprint :: Text }
deriving stock (Eq, Ord, Generic)
deriving (Read, Show) via (Quiet TokenFingerprint)
deriving anyclass Hashable

instance NFData TokenFingerprint

-- | Construct a fingerprint from a 'TokenPolicyId' and 'TokenName'. The
-- fingerprint is not necessarily unique, but can be used in user-facing
-- interfaces as a comparison mechanism.
mkTokenFingerprint :: TokenPolicyId -> TokenName -> TokenFingerprint
mkTokenFingerprint (UnsafeTokenPolicyId (Hash p)) (UnsafeTokenName n)
= (p <> n)
& convert . hash @_ @Blake2b_160
& Bech32.encodeLenient tokenFingerprintHrp . Bech32.dataPartFromBytes
& UnsafeTokenFingerprint

tokenFingerprintHrp :: Bech32.HumanReadablePart
tokenFingerprintHrp = [humanReadablePart|asset|]

instance ToText TokenFingerprint where
toText = unTokenFingerprint

instance FromText TokenFingerprint where
fromText txt = case Bech32.decodeLenient txt of
Left{} -> Left invalidBech32String
Right (hrp, dp)
| hrp /= tokenFingerprintHrp -> Left unrecognizedHrp
| otherwise -> case BS.length <$> Bech32.dataPartToBytes dp of
Just 20 -> Right (UnsafeTokenFingerprint txt)
_ -> Left invalidDatapart
where
invalidBech32String = TextDecodingError
"A 'TokenFingerprint' must be a valid bech32-encoded string."
unrecognizedHrp = TextDecodingError
"Expected 'asset' as a human-readable part, but got something else."
invalidDatapart = TextDecodingError
"Expected a Blake2b-160 digest as data payload, but got something else."

-- | Information about an asset, from a source external to the chain.
data AssetMetadata = AssetMetadata
{ name :: Text
Expand Down
55 changes: 55 additions & 0 deletions lib/core/test/data/Cardano/Wallet/Api/ApiAsset.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ spec = parallel $ do
jsonRoundtripAndGolden $ Proxy @ApiTxMetadata
jsonRoundtripAndGolden $ Proxy @ApiMaintenanceAction
jsonRoundtripAndGolden $ Proxy @ApiMaintenanceActionPostData
jsonRoundtripAndGolden $ Proxy @ApiAsset

describe "Textual encoding" $ do
describe "Can perform roundtrip textual encoding & decoding" $ do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

module Cardano.Wallet.Primitive.Types.TokenPolicySpec
( spec
) where

import Prelude

import Cardano.Wallet.Primitive.Types.Hash
( Hash (..) )
import Cardano.Wallet.Primitive.Types.TokenPolicy
( TokenFingerprint (..)
, TokenName (..)
, TokenPolicyId (..)
, mkTokenFingerprint
)
import Cardano.Wallet.Unsafe
( unsafeFromHex )
import Data.ByteString
( ByteString )
import Data.Text
( Text )
import Test.Hspec
( Spec, SpecWith, describe, it, parallel, shouldBe )

import qualified Data.Text as T

spec :: Spec
spec = parallel $ describe "mkAssetFingerprint" $ do
goldenTestCIP14
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373"
""
"asset1rjklcrnsdzqp65wjgrg55sy9723kw09mlgvlc3"

goldenTestCIP14
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc37e"
""
"asset1nl0puwxmhas8fawxp8nx4e2q3wekg969n2auw3"

goldenTestCIP14
"1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209"
""
"asset1uyuxku60yqe57nusqzjx38aan3f2wq6s93f6ea"

goldenTestCIP14
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373"
"504154415445"
"asset13n25uv0yaf5kus35fm2k86cqy60z58d9xmde92"

goldenTestCIP14
"1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209"
"504154415445"
"asset1hv4p5tv2a837mzqrst04d0dcptdjmluqvdx9k3"

goldenTestCIP14
"1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209"
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373"
"asset1aqrdypg669jgazruv5ah07nuyqe0wxjhe2el6f"

goldenTestCIP14
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373"
"1e349c9bdea19fd6c147626a5260bc44b71635f398b67c59881df209"
"asset17jd78wukhtrnmjh3fngzasxm8rck0l2r4hhyyt"

goldenTestCIP14
"7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373"
"0000000000000000000000000000000000000000000000000000000000000000"
"asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w"

goldenTestCIP14
:: ByteString -- Base16-encoded PolicyId
-> ByteString -- Base16-encoded AssetName
-> Text -- Bech32-encoded TokenFingerprint
-> SpecWith ()
goldenTestCIP14 rawPolicyId rawAssetName rawFingerprint =
it ("golden test CIP-0014 - " <> T.unpack rawFingerprint) $ do
let policyId = UnsafeTokenPolicyId $ Hash $ unsafeFromHex rawPolicyId
let assetName = UnsafeTokenName $ unsafeFromHex rawAssetName
let fingerprint = UnsafeTokenFingerprint rawFingerprint
mkTokenFingerprint policyId assetName `shouldBe` fingerprint
16 changes: 16 additions & 0 deletions specifications/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,19 @@ x-assetPolicyId: &assetPolicyId
maxLength: 56
example: 65ab82542b0ca20391caaf66a4d4d7897d281f9c136cd3513136945b

x-assetFingerprint: &assetFingerprint
type: string
description: |
A user-facing short fingerprint which combines the `policy_id` and `asset_name`
to allow for an easier human comparison of assets. Note that it is generally
**not okay** to use this fingerprint as a unique identifier for it is not collision
resistant. Yet within the context of a single wallet, it makes for a (rather)
short user-facing comparison mean.
pattern: "^(asset)1[0-9a-z]*$"
maxLength: 44
minLength: 44
example: token1rjklcrnsdzqp65wjgrg55sy9723kw09m5z9489

This comment has been minimized.

Copy link
@mmahut

mmahut Feb 19, 2021

Looks like the example doesn't match the pattern?

This comment has been minimized.

Copy link
@KtorZ

KtorZ Feb 19, 2021

Author Member

:| ... Good catch.


x-assetMetadataName: &assetMetadataName
type: string
maxLength: 50
Expand Down Expand Up @@ -715,6 +728,7 @@ x-walletAsset: &walletAsset
properties:
policy_id: *assetPolicyId
asset_name: *assetName
fingerprint: *assetFingerprint
quantity: *assetQuantity

x-walletAssets: &walletAssets
Expand All @@ -731,6 +745,7 @@ x-assetMint: &assetMint
properties:
policy_id: *assetPolicyId
asset_name: *assetName
fingerprint: *assetFingerprint
quantity:
type: integer
description: |
Expand Down Expand Up @@ -1780,6 +1795,7 @@ components:
properties:
policy_id: *assetPolicyId
asset_name: *assetName
fingerprint: *assetFingerprint
metadata: *assetMetadata

ApiWalletMigrationInfo: &ApiWalletMigrationInfo
Expand Down

0 comments on commit 9f446af

Please sign in to comment.