-
Notifications
You must be signed in to change notification settings - Fork 25
Refactor TxOut Aeson instances
#1002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
cfdbb6a
3e3eed1
197e56f
8d8ec01
66982f6
7730a6a
cba7b3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| :: 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 | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type-level constraint provides exhaustiveness checking By taking
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 | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hash validation prevents data corruption When both This check catches:
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 | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Backwards compatibility preserved: This reconciliation function accepts three valid JSON formats:
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 $ | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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) | ||
|
|
@@ -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 | ||
newhoggy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| parseJSON = withObject "TxOut" $ \o -> do | ||
| case shelleyBasedEra :: ShelleyBasedEra era of | ||
|
|
||
There was a problem hiding this comment.
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:
Important assumption:
BabbageEraOnwardswill always cover exactly these three eras. If a new era is added to the GADT, this code must be updated.