Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cardano-api/cardano-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,12 @@ test-suite cardano-api-test
Test.Cardano.Api.Transaction.Autobalance
Test.Cardano.Api.Transaction.Body.Plutus.Scripts
Test.Cardano.Api.TxBody
Test.Cardano.Api.TxOut.Gen
Test.Cardano.Api.TxOut.Helpers
Test.Cardano.Api.TxOut.Json
Test.Cardano.Api.TxOut.JsonEdgeCases
Test.Cardano.Api.TxOut.JsonErrorCases
Test.Cardano.Api.TxOut.JsonRoundtrip
Test.Cardano.Api.Value

ghc-options:
Expand Down
3 changes: 3 additions & 0 deletions cardano-api/src/Cardano/Api/Era.hs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ module Cardano.Api.Era
, caseShelleyToMaryOrAlonzoEraOnwards
, caseShelleyToAlonzoOrBabbageEraOnwards
, caseShelleyToBabbageOrConwayEraOnwards

-- ** Case on BabbageEraOnwards
, caseBabbageOnlyOrConwayEraOnwards
)
where

Expand Down
15 changes: 15 additions & 0 deletions cardano-api/src/Cardano/Api/Era/Internal/Case.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ module Cardano.Api.Era.Internal.Case
, caseShelleyToMaryOrAlonzoEraOnwards
, caseShelleyToAlonzoOrBabbageEraOnwards
, caseShelleyToBabbageOrConwayEraOnwards
-- Case on BabbageEraOnwards
, caseBabbageOnlyOrConwayEraOnwards
-- Conversions
, shelleyToAlonzoEraToShelleyToBabbageEra
, alonzoEraOnwardsToMaryEraOnwards
Expand Down Expand Up @@ -157,6 +159,19 @@ caseShelleyToBabbageOrConwayEraOnwards l r = \case
ShelleyBasedEraConway -> r ConwayEraOnwardsConway
ShelleyBasedEraDijkstra -> error "caseShelleyToBabbageOrConwayEraOnwards: DijkstraEra is not supported"

-- | @caseBabbageOnlyOrConwayEraOnwards f g era@ applies @f@ to babbage era only;
-- and applies @g@ to conway and later eras.
caseBabbageOnlyOrConwayEraOnwards
:: ()
=> a
-> (ConwayEraOnwardsConstraints era => ConwayEraOnwards era -> a)
-> BabbageEraOnwards era
-> a
caseBabbageOnlyOrConwayEraOnwards l r = \case
BabbageEraOnwardsBabbage -> l
BabbageEraOnwardsConway -> r ConwayEraOnwardsConway
BabbageEraOnwardsDijkstra -> error "caseBabbageOnlyOrConwayEraOnwards: DijkstraEra is not supported"

{-# DEPRECATED shelleyToAlonzoEraToShelleyToBabbageEra "Use convert instead" #-}
shelleyToAlonzoEraToShelleyToBabbageEra
:: ()
Expand Down
240 changes: 128 additions & 112 deletions cardano-api/src/Cardano/Api/Tx/Internal/Output.hs
Original file line number Diff line number Diff line change
Expand Up @@ -455,133 +455,136 @@ instance IsShelleyBasedEra era => FromJSON (TxOut CtxTx era) where
<*> return TxOutDatumNone
<*> return ReferenceScriptNone
ShelleyBasedEraAlonzo -> alonzoTxOutParser AlonzoEraOnwardsAlonzo o
ShelleyBasedEraBabbage -> do
alonzoTxOutInBabbage <- alonzoTxOutParser AlonzoEraOnwardsBabbage o

-- We check for the existence of inline datums
inlineDatumHash <- o .:? "inlineDatumhash"
inlineDatum <- o .:? "inlineDatum"
mInlineDatum <-
case (inlineDatum, inlineDatumHash) of
(Just dVal, Just h) -> do
case scriptDataJsonToHashable ScriptDataJsonDetailedSchema dVal of
Left err ->
fail $ "Error parsing TxOut JSON: " <> displayError err
Right hashableData -> do
if hashScriptDataBytes hashableData /= h
then fail "Inline datum not equivalent to inline datum hash"
else return $ TxOutDatumInline BabbageEraOnwardsBabbage hashableData
(Nothing, Nothing) -> return TxOutDatumNone
(_, _) ->
fail
"Should not be possible to create a tx output with either an inline datum hash or an inline datum"

mReferenceScript <- o .:? "referenceScript"

reconcileBabbage alonzoTxOutInBabbage mInlineDatum mReferenceScript
ShelleyBasedEraConway -> do
alonzoTxOutInConway <- alonzoTxOutParser AlonzoEraOnwardsConway o

-- We check for the existence of inline datums
inlineDatumHash <- o .:? "inlineDatumhash"
inlineDatum <- o .:? "inlineDatum"
mInlineDatum <-
case (inlineDatum, inlineDatumHash) of
(Just dVal, Just h) ->
case scriptDataFromJson ScriptDataJsonDetailedSchema dVal of
Left err ->
fail $ "Error parsing TxOut JSON: " <> displayError err
Right sData ->
if hashScriptDataBytes sData /= h
then fail "Inline datum not equivalent to inline datum hash"
else return $ TxOutDatumInline BabbageEraOnwardsConway sData
(Nothing, Nothing) -> return TxOutDatumNone
(_, _) ->
fail
"Should not be possible to create a tx output with either an inline datum hash or an inline datum"

mReferenceScript <- o .:? "referenceScript"

reconcileConway ConwayEraOnwardsConway alonzoTxOutInConway mInlineDatum mReferenceScript
ShelleyBasedEraDijkstra -> do
alonzoTxOutInConway <- alonzoTxOutParser AlonzoEraOnwardsDijkstra o

-- We check for the existence of inline datums
inlineDatumHash <- o .:? "inlineDatumhash"
inlineDatum <- o .:? "inlineDatum"
mInlineDatum <-
case (inlineDatum, inlineDatumHash) of
(Just dVal, Just h) ->
case scriptDataFromJson ScriptDataJsonDetailedSchema dVal of
Left err ->
fail $ "Error parsing TxOut JSON: " <> displayError err
Right sData ->
if hashScriptDataBytes sData /= h
then fail "Inline datum not equivalent to inline datum hash"
else return $ TxOutDatumInline BabbageEraOnwardsDijkstra sData
(Nothing, Nothing) -> return TxOutDatumNone
(_, _) ->
fail
"Should not be possible to create a tx output with either an inline datum hash or an inline datum"

mReferenceScript <- o .:? "referenceScript"

reconcileConway ConwayEraOnwardsDijkstra alonzoTxOutInConway mInlineDatum mReferenceScript
ShelleyBasedEraBabbage -> parseBabbageOnwardsTxOut BabbageEraOnwardsBabbage o
ShelleyBasedEraConway -> parseBabbageOnwardsTxOut BabbageEraOnwardsConway o
ShelleyBasedEraDijkstra -> parseBabbageOnwardsTxOut BabbageEraOnwardsDijkstra o
where
reconcileBabbage
:: TxOut CtxTx BabbageEra
-- \^ Alonzo era datum in Babbage era
-> TxOutDatum CtxTx BabbageEra
-- \^ Babbage inline datum
-> Maybe ScriptInAnyLang
-> Aeson.Parser (TxOut CtxTx BabbageEra)
reconcileBabbage top@(TxOut addr v dat r) babbageDatum mBabRefScript = do
-- We check for conflicting datums
finalDat <- case (dat, babbageDatum) of
(TxOutDatumNone, bDatum) -> return bDatum
(anyDat, TxOutDatumNone) -> return anyDat
(alonzoDat, babbageDat) ->
fail $
"Parsed an Alonzo era datum and a Babbage era datum "
<> "TxOut: "
<> show top
<> "Alonzo datum: "
<> show alonzoDat
<> "Babbage dat: "
<> show babbageDat
finalRefScript <- case mBabRefScript of
Nothing -> return r
Just anyScript ->
return $ ReferenceScript BabbageEraOnwardsBabbage anyScript
return $ TxOut addr v finalDat finalRefScript

reconcileConway
:: ConwayEraOnwards era
-- Parse TxOut for Babbage+ eras (Babbage, Conway, Dijkstra)
--
-- MOTIVATION: This unified helper eliminates ~100 lines of duplication that previously
-- existed across the three Babbage+ era cases.
--
-- DESIGN: Uses a two-phase parsing strategy:
-- 1. Parse Alonzo-style fields (datumhash/datum) via alonzoTxOutParser
-- 2. Parse Babbage-style fields (inlineDatumhash/inlineDatum) via parseInlineDatum
-- 3. Reconcile both via reconcileDatums, which validates no conflicting datums exist
--
-- This approach maintains backwards compatibility - old JSON with only Alonzo fields
-- still parses correctly, while new JSON can use inline datums.
--
-- ASSUMPTION: BabbageEraOnwards will always cover exactly these three eras. If a new
-- era is added, this code must be updated.
parseBabbageOnwardsTxOut
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key refactoring: eliminates ~100 lines of duplication

This unified helper consolidates what were previously three nearly-identical code blocks for Babbage, Conway, and Dijkstra eras.

The refactoring maintains full backwards compatibility by using a two-phase parsing strategy:

  1. Parse Alonzo-style fields first (for old JSON)
  2. Parse Babbage-style fields (for new JSON)
  3. Reconcile both (detecting conflicts)

Important assumption: BabbageEraOnwards will always cover exactly these three eras. If a new era is added to the GADT, this code must be updated.

:: BabbageEraOnwards era
-> Aeson.Object
-> Aeson.Parser (TxOut CtxTx era)
parseBabbageOnwardsTxOut w o = do
alonzoTxOut <- alonzoTxOutParser (convert w) o
inlineDatum <- parseInlineDatum w o
mReferenceScript <- o .:? "referenceScript"
reconcileDatums w alonzoTxOut inlineDatum mReferenceScript

-- Parse inline datum fields from JSON object
--
-- Handles both inlineDatumhash and inlineDatum fields, validating they match.
--
-- CRITICAL DISTINCTION: Babbage era uses scriptDataJsonToHashable (returns HashableScriptData)
-- while Conway+ uses scriptDataFromJson (returns ScriptData). This difference exists because
-- Babbage required preserving the original CBOR encoding for hash validation, while Conway+
-- can reconstruct it.
--
-- VALIDATION: When both hash and datum are present, we verify the datum hashes to the
-- provided hash. This catches malformed JSON where they don't match.
--
-- DESIGN: Uses caseBabbageOnlyOrConwayEraOnwards to distinguish between Babbage (first function)
-- and Conway/Dijkstra (second function). This provides exhaustiveness checking - if a new era
-- is added to BabbageEraOnwards, the compiler will ensure it's handled.
parseInlineDatum
:: BabbageEraOnwards era
-> Aeson.Object
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type-level constraint provides exhaustiveness checking

By taking BabbageEraOnwards era as a parameter instead of ShelleyBasedEra era, this function gains an important benefit: the compiler can verify exhaustiveness.

BabbageEraOnwards is a GADT with exactly 3 constructors (Babbage, Conway, Dijkstra). If a new era is added to this GADT in the future, any pattern match on it (like in eraName or parseInlineDatum) will fail to compile unless explicitly handled.

This prevents bugs where new eras are added but parsing logic is forgotten.

-> Aeson.Parser (TxOutDatum CtxTx era)
parseInlineDatum w o = do
inlineDatumHash <- o .:? "inlineDatumhash"
inlineDatum <- o .:? "inlineDatum"
case (inlineDatum, inlineDatumHash) of
(Just dVal, Just h) -> do
sData <-
caseBabbageOnlyOrConwayEraOnwards
-- Babbage case: use scriptDataJsonToHashable
( case scriptDataJsonToHashable ScriptDataJsonDetailedSchema dVal of
Left err -> fail $ "Error parsing TxOut JSON: " <> displayError err
Right hashableData -> return hashableData
)
-- Conway+ case: use scriptDataFromJson
( \_ -> case scriptDataFromJson ScriptDataJsonDetailedSchema dVal of
Left err -> fail $ "Error parsing TxOut JSON: " <> displayError err
Right scriptData -> return scriptData
)
w
if hashScriptDataBytes sData /= h
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hash validation prevents data corruption

When both inlineDatum and inlineDatumhash fields are present in the JSON, this validation ensures they match.

This check catches:

  • Transmission errors that corrupt the datum
  • Serialization bugs
  • Intentional tampering with datum data

If the hash does not match, parsing fails immediately with a clear error message, preventing invalid data from entering the system.

then fail "Inline datum not equivalent to inline datum hash"
else return $ TxOutDatumInline w sData
(Nothing, Nothing) -> return TxOutDatumNone
(_, _) ->
fail
"Should not be possible to create a tx output with either an inline datum hash or an inline datum"

-- Reconcile Alonzo-style and Babbage-style datums and reference scripts
--
-- This handles the two-phase parsing where both old and new style fields may be present.
--
-- BACKWARDS COMPATIBILITY: Accepts JSON with either:
-- - Only Alonzo fields (datumhash/datum) - common in older transactions
-- - Only Babbage fields (inlineDatumhash/inlineDatum) - modern format
-- - Neither (TxOutDatumNone) - simple payment outputs
--
-- ERROR HANDLING: If *both* Alonzo and Babbage style datums are present, this is a
-- malformed JSON and we fail with a detailed error message showing both datums.
-- This should never happen in correctly formed JSON but protects against corruption.
--
-- EXHAUSTIVENESS: The eraName helper now matches directly on BabbageEraOnwards GADT
-- constructors instead of converting to ShelleyBasedEra. This allows the compiler to
-- verify exhaustiveness - if a new era is added to BabbageEraOnwards, this will fail
-- to compile, forcing developers to update the code.
reconcileDatums
:: BabbageEraOnwards era
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backwards compatibility preserved: This reconciliation function accepts three valid JSON formats:

  1. Alonzo-only fields (datumhash/datum) - common in older transactions
  2. Babbage-only fields (inlineDatumhash/inlineDatum) - modern format
  3. No datum fields - simple payment outputs

This two-phase parsing strategy (parse Alonzo-style, then Babbage-style, then reconcile) ensures we can parse both old and new JSON formats without breaking existing tooling.

-> TxOut CtxTx era
-- \^ Alonzo era datum in Conway era
-- \^ TxOut with Alonzo-style datum
-> TxOutDatum CtxTx era
-- \^ Babbage inline datum
-- \^ Babbage-style inline datum
-> Maybe ScriptInAnyLang
-- \^ Optional reference script
-> Aeson.Parser (TxOut CtxTx era)
reconcileConway w top@(TxOut addr v dat r) babbageDatum mBabRefScript = do
-- We check for conflicting datums
finalDat <- case (dat, babbageDatum) of
reconcileDatums w top@(TxOut addr v dat r) inlineDatum mRefScript = do
-- Check for conflicting datums
finalDat <- case (dat, inlineDatum) of
(TxOutDatumNone, bDatum) -> return bDatum
(anyDat, TxOutDatumNone) -> return anyDat
(alonzoDat, babbageDat) ->
fail $
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error detection for malformed JSON: This case catches when both Alonzo-style datums (datumhash/datum) AND Babbage-style datums (inlineDatumhash/inlineDatum) are present in the same JSON.

This should never happen in correctly formed JSON, but the check protects against:

  • Corrupted data
  • Buggy serialization code
  • Manual JSON construction errors

The detailed error message includes both datums to help diagnose the source of the problem.

"Parsed an Alonzo era datum and a Conway era datum "
"Parsed an Alonzo era datum and a "
<> eraName
<> " era datum. "
<> "TxOut: "
<> show top
<> "Alonzo datum: "
<> " Alonzo datum: "
<> show alonzoDat
<> "Conway dat: "
<> " "
<> eraName
<> " datum: "
<> show babbageDat
finalRefScript <- case mBabRefScript of
finalRefScript <- case mRefScript of
Nothing -> return r
Just anyScript ->
return $ ReferenceScript (convert w) anyScript
Just anyScript -> return $ ReferenceScript w anyScript
return $ TxOut addr v finalDat finalRefScript
where
-- Pattern match directly on GADT instead of converting to ShelleyBasedEra.
-- This enables exhaustiveness checking - adding a new era to BabbageEraOnwards
-- will cause a compile error here, preventing bugs from incomplete updates.
eraName = case w of
BabbageEraOnwardsBabbage -> "Babbage"
BabbageEraOnwardsConway -> "Conway"
BabbageEraOnwardsDijkstra -> "Dijkstra"

alonzoTxOutParser
:: AlonzoEraOnwards era -> Aeson.Object -> Aeson.Parser (TxOut CtxTx era)
Expand Down Expand Up @@ -612,6 +615,19 @@ instance IsShelleyBasedEra era => FromJSON (TxOut CtxTx era) where
<*> return ReferenceScriptNone
(Just _dVal, Nothing) -> fail "Only datum JSON was found, this should not be possible."

-- NOTE: The CtxUTxO instance below still contains significant duplication in the
-- Babbage/Conway/Dijkstra cases (lines ~592-666). Each case has nearly identical
-- inline datum parsing logic that could be extracted into a helper similar to the
-- parseInlineDatum function in the CtxTx instance above.
--
-- POTENTIAL REFACTORING: The inline datum parsing at lines 596-611 (Babbage),
-- 621-636 (Conway), and 646-661 (Dijkstra) could be unified using a helper that
-- takes a BabbageEraOnwards witness, similar to how parseInlineDatum works in CtxTx.
-- This would eliminate ~60 more lines of duplication.
--
-- BLOCKER: The CtxUTxO alonzoTxOutParser differs from CtxTx - it doesn't handle
-- supplemental datums (only datum hash, no datum value). This difference would need
-- to be carefully preserved in any refactoring.
instance IsShelleyBasedEra era => FromJSON (TxOut CtxUTxO era) where
parseJSON = withObject "TxOut" $ \o -> do
case shelleyBasedEra :: ShelleyBasedEra era of
Expand Down
Loading
Loading