Skip to content

Commit fb8d48f

Browse files
committed
Add FromJSON instance for new experimental TxOut
Add per-era FromJSON instances for the experimental TxOut type, mirroring the ToJSON structure. Pre-Alonzo eras parse address and value only; Alonzo adds datum hash support; Babbage+ adds inline datum (parsed from inlineDatumRaw with hash validation) and reference script support. Supplemental datums are deliberately unsupported as the ledger TxOut does not carry them.
1 parent aefe5de commit fb8d48f

2 files changed

Lines changed: 149 additions & 1 deletion

File tree

  • cardano-api
    • src/Cardano/Api/Experimental/Tx/Internal/BodyContent
    • test/cardano-api-test/Test/Cardano/Api

cardano-api/src/Cardano/Api/Experimental/Tx/Internal/BodyContent/New.hs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
{-# LANGUAGE TupleSections #-}
1010
{-# LANGUAGE TypeApplications #-}
1111
{-# LANGUAGE TypeFamilies #-}
12+
{-# LANGUAGE TypeOperators #-}
1213

1314
module Cardano.Api.Experimental.Tx.Internal.BodyContent.New
1415
( TxCertificates (..)
@@ -112,6 +113,7 @@ import Cardano.Api.Plutus.Internal.Script
112113
, ScriptInAnyLang (..)
113114
, ScriptLanguage (..)
114115
, fromAllegraTimelock
116+
, toAllegraTimelock
115117
)
116118
import Cardano.Api.Plutus.Internal.Script qualified as OldScript
117119
import Cardano.Api.Plutus.Internal.ScriptData qualified as Api
@@ -138,6 +140,7 @@ import Cardano.Api.Value.Internal
138140
)
139141

140142
import Cardano.Binary qualified as CBOR
143+
import Cardano.Ledger.Allegra.Scripts (Timelock)
141144
import Cardano.Ledger.Alonzo.Scripts qualified as L
142145
import Cardano.Ledger.Alonzo.Tx qualified as L
143146
import Cardano.Ledger.Alonzo.TxBody qualified as L
@@ -149,8 +152,9 @@ import Cardano.Ledger.Plutus.Language (PlutusBinary (..), plutusLanguage)
149152
import Cardano.Ledger.Plutus.Language qualified as Plutus
150153

151154
import Control.Monad
152-
import Data.Aeson (ToJSON (..), (.=))
155+
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.:?), (.=))
153156
import Data.Aeson qualified as Aeson
157+
import Data.Aeson.Types (Parser)
154158
import Data.ByteString.Base16 qualified as Base16
155159
import Data.ByteString.Short qualified as SBS
156160
import Data.Functor
@@ -475,6 +479,122 @@ deriving instance (Show (TxOut era))
475479

476480
deriving instance (Eq (TxOut era))
477481

482+
-- | Pre-Alonzo eras have no datums or reference scripts, so parsing
483+
-- only needs address and value.
484+
instance FromJSON (TxOut L.ShelleyEra) where
485+
parseJSON = Aeson.withObject "TxOut" $ fmap TxOut . txOutBaseParseJson
486+
487+
instance FromJSON (TxOut L.AllegraEra) where
488+
parseJSON = Aeson.withObject "TxOut" $ fmap TxOut . txOutBaseParseJson
489+
490+
instance FromJSON (TxOut L.MaryEra) where
491+
parseJSON = Aeson.withObject "TxOut" $ fmap TxOut . txOutBaseParseJson
492+
493+
-- | Alonzo supports datum hashes but not inline datums or reference scripts.
494+
instance FromJSON (TxOut L.AlonzoEra) where
495+
parseJSON = Aeson.withObject "TxOut" $ \o -> do
496+
baseTxOut <- txOutBaseParseJson o
497+
mDatumHash <- o .:? "datumhash"
498+
pure . TxOut $ case mDatumHash of
499+
Nothing -> baseTxOut
500+
Just dh -> baseTxOut & L.dataHashTxOutL .~ SJust dh
501+
502+
-- | Babbage and later eras support inline datums and reference scripts.
503+
instance FromJSON (TxOut L.BabbageEra) where
504+
parseJSON = Aeson.withObject "TxOut" babbageOnwardsTxOutParseJson
505+
506+
instance FromJSON (TxOut L.ConwayEra) where
507+
parseJSON = Aeson.withObject "TxOut" babbageOnwardsTxOutParseJson
508+
509+
-- | Parse the base fields (address and value) shared by all eras.
510+
txOutBaseParseJson :: L.EraTxOut era => Aeson.Object -> Parser (L.TxOut era)
511+
txOutBaseParseJson o = do
512+
addr <- addrFromJson =<< o .: "address"
513+
apiVal <- parseJSON =<< o .: "value"
514+
let mv = toMaryValue apiVal
515+
val <- case cast mv of
516+
Just v -> pure v
517+
Nothing -> case cast (L.coin mv) of
518+
Just v -> pure v
519+
Nothing -> fail "Unsupported value type for era"
520+
pure $ L.mkBasicTxOut addr val
521+
522+
-- | Parse a ledger 'L.Addr' from JSON. Reverse of 'addrToJson'.
523+
addrFromJson :: Aeson.Value -> Parser L.Addr
524+
addrFromJson = Aeson.withText "Address" $ \txt ->
525+
case deserialiseAddress AsAddressAny txt of
526+
Nothing -> fail "Invalid address"
527+
Just addrAny -> pure $ case addrAny of
528+
AddressByron (ByronAddress addr) -> L.AddrBootstrap (L.BootstrapAddress addr)
529+
AddressShelley (ShelleyAddress nw pc scr) -> L.Addr nw pc scr
530+
531+
-- | Parse a Babbage+ TxOut with datum and reference script support.
532+
babbageOnwardsTxOutParseJson
533+
:: forall era
534+
. ( L.BabbageEraTxOut era
535+
, L.NativeScript era ~ Timelock era
536+
)
537+
=> Aeson.Object -> Parser (TxOut era)
538+
babbageOnwardsTxOutParseJson o = do
539+
baseTxOut <- txOutBaseParseJson o
540+
-- Parse datum fields
541+
mDatumHash <- o .:? "datumhash"
542+
mInlineDatumRaw <- o .:? "inlineDatumRaw"
543+
mInlineDatumHash <- o .:? "inlineDatumhash"
544+
-- Parse reference script
545+
mRefScript <- o .:? "referenceScript"
546+
-- Determine datum
547+
datum <- case mInlineDatumRaw of
548+
Just rawHex -> do
549+
expectedHash <- case mInlineDatumHash of
550+
Nothing -> fail "inlineDatumRaw present without inlineDatumhash"
551+
Just h -> pure h
552+
rawBytes <- case Base16.decode (Text.encodeUtf8 rawHex) of
553+
Left err -> fail $ "Error decoding inlineDatumRaw hex: " <> show err
554+
Right bs -> pure bs
555+
binaryData <- case L.makeBinaryData (SBS.toShort rawBytes) of
556+
Left err -> fail $ "Error decoding inlineDatumRaw CBOR: " <> err
557+
Right bd -> pure bd
558+
when (L.hashBinaryData binaryData /= expectedHash) $
559+
fail "Inline datum hash does not match inlineDatumRaw"
560+
pure $ L.Datum binaryData
561+
Nothing -> case mDatumHash of
562+
Just dh -> pure $ L.DatumHash dh
563+
Nothing -> pure L.NoDatum
564+
-- Determine reference script
565+
refScript <- case mRefScript of
566+
Nothing -> pure SNothing
567+
Just script -> SJust <$> scriptInAnyLangToLedgerScript script
568+
-- Construct TxOut
569+
pure . TxOut $
570+
baseTxOut
571+
& L.datumTxOutL .~ datum
572+
& L.referenceScriptTxOutL .~ refScript
573+
574+
-- | Convert a 'ScriptInAnyLang' to a ledger 'L.Script'. Reverse of 'ledgerScriptToScriptInAnyLang'.
575+
scriptInAnyLangToLedgerScript
576+
:: forall era
577+
. ( L.AlonzoEraScript era
578+
, L.NativeScript era ~ Timelock era
579+
)
580+
=> ScriptInAnyLang -> Parser (L.Script era)
581+
scriptInAnyLangToLedgerScript (ScriptInAnyLang lang script) =
582+
case (lang, script) of
583+
(SimpleScriptLanguage, OldScript.SimpleScript ss) ->
584+
pure $ L.fromNativeScript (toAllegraTimelock ss)
585+
(PlutusScriptLanguage PlutusScriptV1, OldScript.PlutusScript _ (PlutusScriptSerialised sbs)) ->
586+
L.fromPlutusScript
587+
<$> L.mkPlutusScript (Plutus.Plutus (PlutusBinary sbs) :: Plutus.Plutus 'Plutus.PlutusV1)
588+
(PlutusScriptLanguage PlutusScriptV2, OldScript.PlutusScript _ (PlutusScriptSerialised sbs)) ->
589+
L.fromPlutusScript
590+
<$> L.mkPlutusScript (Plutus.Plutus (PlutusBinary sbs) :: Plutus.Plutus 'Plutus.PlutusV2)
591+
(PlutusScriptLanguage PlutusScriptV3, OldScript.PlutusScript _ (PlutusScriptSerialised sbs)) ->
592+
L.fromPlutusScript
593+
<$> L.mkPlutusScript (Plutus.Plutus (PlutusBinary sbs) :: Plutus.Plutus 'Plutus.PlutusV3)
594+
(PlutusScriptLanguage PlutusScriptV4, OldScript.PlutusScript _ (PlutusScriptSerialised sbs)) ->
595+
L.fromPlutusScript
596+
<$> L.mkPlutusScript (Plutus.Plutus (PlutusBinary sbs) :: Plutus.Plutus 'Plutus.PlutusV4)
597+
478598
data Datum ctx era where
479599
TxOutDatumHash
480600
:: L.DataHash

cardano-api/test/cardano-api-test/Test/Cardano/Api/Json.hs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,33 @@ go sbe = do
9393
newTxOut = Exp.TxOut ledgerTxOut
9494
toJSON oldTxOut === toJSON newTxOut
9595

96+
-- | Verify that the new experimental 'TxOut' round-trips through JSON
97+
-- (encode then decode) for all Shelley-based eras.
98+
prop_new_txout_json_roundtrip :: Property
99+
prop_new_txout_json_roundtrip = H.property $ do
100+
AnyShelleyBasedEra sbe <- forAll $ Gen.element [minBound .. maxBound]
101+
case sbe of
102+
ShelleyBasedEraShelley -> goRoundtrip sbe
103+
ShelleyBasedEraAllegra -> goRoundtrip sbe
104+
ShelleyBasedEraMary -> goRoundtrip sbe
105+
ShelleyBasedEraAlonzo -> goRoundtrip sbe
106+
ShelleyBasedEraBabbage -> goRoundtrip sbe
107+
ShelleyBasedEraConway -> goRoundtrip sbe
108+
ShelleyBasedEraDijkstra -> pure ()
109+
110+
goRoundtrip
111+
:: ( L.EraTxOut (ShelleyLedgerEra era)
112+
, ToJSON (Exp.TxOut (ShelleyLedgerEra era))
113+
, FromJSON (Exp.TxOut (ShelleyLedgerEra era))
114+
)
115+
=> ShelleyBasedEra era
116+
-> H.PropertyT IO ()
117+
goRoundtrip sbe = do
118+
oldTxOut <- forAll $ genTxOutUTxOContext sbe
119+
let ledgerTxOut = toShelleyTxOut sbe oldTxOut
120+
newTxOut = Exp.TxOut ledgerTxOut
121+
tripping newTxOut encode eitherDecode
122+
96123
tests :: TestTree
97124
tests =
98125
testGroup
@@ -106,4 +133,5 @@ tests =
106133
, testProperty "json roundtrip scriptdata detailed json" prop_json_roundtrip_scriptdata_detailed_json
107134
, testProperty "json roundtrip praos nonce" prop_roundtrip_praos_nonce_JSON
108135
, testProperty "new TxOut ToJSON matches legacy" prop_new_txout_json_matches_legacy
136+
, testProperty "new TxOut JSON roundtrip" prop_new_txout_json_roundtrip
109137
]

0 commit comments

Comments
 (0)