Skip to content

Commit baacdf3

Browse files
committed
Implement CIP129 class
1 parent 3ac38f6 commit baacdf3

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-0
lines changed

cardano-api/cardano-api.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ library
186186
other-modules:
187187
Cardano.Api.Internal.Anchor
188188
Cardano.Api.Internal.Certificate
189+
Cardano.Api.Internal.CIP.CIP129
189190
Cardano.Api.Internal.Compatible.Tx
190191
Cardano.Api.Internal.Convenience.Construction
191192
Cardano.Api.Internal.Convenience.Query

cardano-api/src/Cardano/Api.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,11 @@ module Cardano.Api
709709
, Bech32DecodeError (..)
710710
, UsingBech32 (..)
711711

712+
-- ** Bech32 CIP-129
713+
, CIP129 (..)
714+
, deserialiseFromBech32CIP129
715+
, serialiseToBech32CIP129
716+
712717
-- ** Addresses
713718

714719
-- | Address serialisation is (sadly) special
@@ -1104,6 +1109,7 @@ where
11041109
import Cardano.Api.Internal.Address
11051110
import Cardano.Api.Internal.Anchor
11061111
import Cardano.Api.Internal.Block
1112+
import Cardano.Api.Internal.CIP.CIP129
11071113
import Cardano.Api.Internal.Certificate
11081114
import Cardano.Api.Internal.Convenience.Construction
11091115
import Cardano.Api.Internal.Convenience.Query
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE FlexibleInstances #-}
3+
{-# LANGUAGE RankNTypes #-}
4+
{-# LANGUAGE ScopedTypeVariables #-}
5+
{-# LANGUAGE TypeFamilies #-}
6+
{-# OPTIONS_GHC -Wno-orphans #-}
7+
8+
module Cardano.Api.Internal.CIP.CIP129
9+
( CIP129 (..)
10+
, deserialiseFromBech32CIP129
11+
, serialiseToBech32CIP129
12+
)
13+
where
14+
15+
import Cardano.Api.Internal.Governance.Actions.ProposalProcedure
16+
import Cardano.Api.Internal.HasTypeProxy
17+
import Cardano.Api.Internal.Orphans ()
18+
import Cardano.Api.Internal.SerialiseBech32
19+
import Cardano.Api.Internal.SerialiseRaw
20+
import Cardano.Api.Internal.TxIn
21+
import Cardano.Api.Internal.Utils
22+
23+
import Cardano.Binary qualified as CBOR
24+
import Cardano.Ledger.Conway.Governance qualified as Gov
25+
import Cardano.Ledger.Credential (Credential (..))
26+
import Cardano.Ledger.Credential qualified as L
27+
import Cardano.Ledger.Keys qualified as L
28+
29+
import Codec.Binary.Bech32 qualified as Bech32
30+
import Control.Monad (guard)
31+
import Data.Bifunctor
32+
import Data.ByteString (ByteString)
33+
import Data.ByteString qualified as BS
34+
import Data.ByteString.Base16 qualified as Base16
35+
import Data.ByteString.Char8 qualified as C8
36+
import Data.Text (Text)
37+
import Data.Text.Encoding qualified as Text
38+
import GHC.Exts (IsList (..))
39+
import Text.Read
40+
41+
class SerialiseAsRawBytes a => CIP129 a where
42+
cip129Bech32PrefixFor :: a -> Text
43+
cip129HeaderHexByte :: a -> ByteString
44+
cip129Bech32PrefixesPermitted :: AsType a -> [Text]
45+
46+
instance CIP129 (Credential L.ColdCommitteeRole) where
47+
cip129Bech32PrefixFor _ = "cc_cold"
48+
cip129Bech32PrefixesPermitted AsColdCommitteeCredential = ["cc_cold"]
49+
cip129HeaderHexByte c =
50+
case c of
51+
L.ScriptHashObj{} -> "\x13"
52+
L.KeyHashObj{} -> "\x12"
53+
54+
instance HasTypeProxy (Credential L.ColdCommitteeRole) where
55+
data AsType (Credential L.ColdCommitteeRole) = AsColdCommitteeCredential
56+
proxyToAsType _ = AsColdCommitteeCredential
57+
58+
instance SerialiseAsRawBytes (Credential L.ColdCommitteeRole) where
59+
serialiseToRawBytes = CBOR.serialize'
60+
deserialiseFromRawBytes AsColdCommitteeCredential =
61+
first
62+
( \e ->
63+
SerialiseAsRawBytesError
64+
("Unable to deserialise Credential ColdCommitteeRole: " ++ show e)
65+
)
66+
. CBOR.decodeFull'
67+
68+
instance CIP129 (Credential L.HotCommitteeRole) where
69+
cip129Bech32PrefixFor _ = "cc_hot"
70+
cip129Bech32PrefixesPermitted AsHotCommitteeCredential = ["cc_hot"]
71+
cip129HeaderHexByte c =
72+
case c of
73+
L.ScriptHashObj{} -> "\x03"
74+
L.KeyHashObj{} -> "\x02"
75+
76+
instance HasTypeProxy (Credential L.HotCommitteeRole) where
77+
data AsType (Credential L.HotCommitteeRole) = AsHotCommitteeCredential
78+
proxyToAsType _ = AsHotCommitteeCredential
79+
80+
instance SerialiseAsRawBytes (Credential L.HotCommitteeRole) where
81+
serialiseToRawBytes = CBOR.serialize'
82+
deserialiseFromRawBytes AsHotCommitteeCredential =
83+
first
84+
( \e ->
85+
SerialiseAsRawBytesError
86+
("Unable to deserialise Credential HotCommitteeRole: " ++ show e)
87+
)
88+
. CBOR.decodeFull'
89+
90+
instance CIP129 (Credential L.DRepRole) where
91+
cip129Bech32PrefixFor _ = "drep"
92+
cip129Bech32PrefixesPermitted AsDrepCredential = ["drep"]
93+
cip129HeaderHexByte c =
94+
case c of
95+
L.ScriptHashObj{} -> "\x23"
96+
L.KeyHashObj{} -> "\x22"
97+
98+
instance HasTypeProxy (Credential L.DRepRole) where
99+
data AsType (Credential L.DRepRole) = AsDrepCredential
100+
proxyToAsType _ = AsDrepCredential
101+
102+
instance SerialiseAsRawBytes (Credential L.DRepRole) where
103+
serialiseToRawBytes = CBOR.serialize'
104+
deserialiseFromRawBytes AsDrepCredential =
105+
first
106+
( \e ->
107+
SerialiseAsRawBytesError ("Unable to deserialise Credential DRepRole: " ++ show e)
108+
)
109+
. CBOR.decodeFull'
110+
111+
instance CIP129 Gov.GovActionId where
112+
cip129Bech32PrefixFor _ = "gov_action"
113+
cip129Bech32PrefixesPermitted AsGovActionId = ["gov_action"]
114+
cip129HeaderHexByte _ = "\x01"
115+
116+
instance HasTypeProxy Gov.GovActionId where
117+
data AsType (Gov.GovActionId) = AsGovActionId
118+
proxyToAsType _ = AsGovActionId
119+
120+
instance SerialiseAsRawBytes Gov.GovActionId where
121+
serialiseToRawBytes (Gov.GovActionId txid (Gov.GovActionIx ix)) =
122+
let hex = Base16.encode $ C8.pack $ show ix
123+
in mconcat [serialiseToRawBytes $ fromShelleyTxId txid, hex]
124+
deserialiseFromRawBytes AsGovActionId bytes = do
125+
let (txidBs, index) = BS.splitAt 32 bytes
126+
127+
txid <- deserialiseFromRawBytes AsTxId txidBs
128+
let asciiIndex = C8.unpack $ Base16.decodeLenient index
129+
case readMaybe asciiIndex of
130+
Just ix -> return $ Gov.GovActionId (toShelleyTxId txid) (Gov.GovActionIx ix)
131+
Nothing ->
132+
Left $ SerialiseAsRawBytesError $ "Unable to deserialise GovActionId: invalid index: " <> asciiIndex
133+
134+
serialiseToBech32CIP129 :: forall a. CIP129 a => a -> Text
135+
serialiseToBech32CIP129 a =
136+
Bech32.encodeLenient
137+
humanReadablePart
138+
(Bech32.dataPartFromBytes (cip129HeaderHexByte a <> serialiseToRawBytes a))
139+
where
140+
prefix = cip129Bech32PrefixFor a
141+
humanReadablePart =
142+
case Bech32.humanReadablePartFromText prefix of
143+
Right p -> p
144+
Left err ->
145+
error $
146+
"serialiseToBech32: invalid prefix "
147+
++ show prefix
148+
++ ", "
149+
++ show err
150+
151+
deserialiseFromBech32CIP129
152+
:: forall a
153+
. CIP129 a
154+
=> AsType a -> Text -> Either Bech32DecodeError a
155+
deserialiseFromBech32CIP129 asType bech32Str = do
156+
(prefix, dataPart) <-
157+
Bech32.decodeLenient bech32Str
158+
?!. Bech32DecodingError
159+
160+
let actualPrefix = Bech32.humanReadablePartToText prefix
161+
permittedPrefixes = cip129Bech32PrefixesPermitted asType
162+
guard (actualPrefix `elem` permittedPrefixes)
163+
?! Bech32UnexpectedPrefix actualPrefix (fromList permittedPrefixes)
164+
165+
payload <-
166+
Bech32.dataPartToBytes dataPart
167+
?! Bech32DataPartToBytesError (Bech32.dataPartToText dataPart)
168+
169+
let (header, credential) = BS.splitAt 1 payload
170+
171+
value <- case deserialiseFromRawBytes asType credential of
172+
Right a -> Right a
173+
Left _ -> Left $ Bech32DeserialiseFromBytesError payload
174+
175+
let expectedHeader = cip129HeaderHexByte value
176+
177+
guard (header == expectedHeader)
178+
?! Bech32UnexpectedHeader (toBase16Text expectedHeader) (toBase16Text header)
179+
180+
let expectedPrefix = cip129Bech32PrefixFor value
181+
guard (actualPrefix == expectedPrefix)
182+
?! Bech32WrongPrefix actualPrefix expectedPrefix
183+
184+
return value
185+
where
186+
toBase16Text = Text.decodeUtf8 . Base16.encode

cardano-api/src/Cardano/Api/Internal/Keys/Shelley.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,6 +2045,7 @@ instance HasTextEnvelope (SigningKey DRepKey) where
20452045
---
20462046
--- Drep extended keys
20472047
---
2048+
20482049
data DRepExtendedKey
20492050

20502051
instance HasTypeProxy DRepExtendedKey where

cardano-api/src/Cardano/Api/Internal/SerialiseBech32.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ data Bech32DecodeError
141141
| -- | The human-readable prefix in the Bech32-encoded string does not
142142
-- correspond to the prefix that should be used for the payload value.
143143
Bech32WrongPrefix !Text !Text
144+
| Bech32UnexpectedHeader
145+
!Text
146+
-- ^ Expected header
147+
!Text
148+
-- ^ Unexpected header
144149
deriving (Eq, Show, Data)
145150

146151
instance Error Bech32DecodeError where
@@ -168,3 +173,8 @@ instance Error Bech32DecodeError where
168173
[ "Mismatch in the Bech32 prefix: the actual prefix is " <> pshow actual
169174
, ", but the prefix for this payload value should be " <> pshow expected
170175
]
176+
Bech32UnexpectedHeader expected actual ->
177+
mconcat
178+
[ "Unexpected CIP-129 Bech32 header: the actual header is " <> pshow actual
179+
, ", but it was expected to be " <> pshow expected
180+
]

0 commit comments

Comments
 (0)