|
| 1 | +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} |
| 2 | + |
| 3 | +-- | Indexer events, i.e. indexer outputs. |
| 4 | +module Cardano.CEM.Indexing.Event where |
| 5 | + |
| 6 | +import Cardano.Api qualified as C |
| 7 | +import Cardano.Api.ScriptData qualified as C |
| 8 | +import Cardano.Api.SerialiseRaw qualified as SerialiseRaw |
| 9 | +import Cardano.CEM (CEMScript, CEMScriptDatum, State, Transition, transitionStage) |
| 10 | +import Cardano.CEM.Address qualified as Address |
| 11 | +import Cardano.CEM.Indexing.Tx |
| 12 | +import Cardano.CEM.OnChain (CEMScriptCompiled, CEMScriptIsData) |
| 13 | +import Cardano.Ledger.BaseTypes qualified as Ledger |
| 14 | +import Control.Lens (view, (^.)) |
| 15 | +import Data.Bifunctor (first) |
| 16 | +import Data.ByteString.Base16 qualified as B16 |
| 17 | +import Data.Data (Proxy (Proxy)) |
| 18 | +import Data.Either.Extra (eitherToMaybe) |
| 19 | +import Data.Function ((&)) |
| 20 | +import Data.List (find) |
| 21 | +import Data.Map.Strict qualified as Map |
| 22 | +import Data.Maybe (fromJust) |
| 23 | +import Data.Spine (Spine, getSpine) |
| 24 | +import Data.Text.Encoding (encodeUtf8) |
| 25 | +import Data.Tuple (swap) |
| 26 | +import PlutusLedgerApi.V1 (FromData) |
| 27 | +import PlutusLedgerApi.V1 qualified |
| 28 | +import Prelude |
| 29 | + |
| 30 | +-- --- |
| 31 | + |
| 32 | +{- | Indexer events. |
| 33 | + We extract events from transactions, where we can encounter three situations: |
| 34 | +
|
| 35 | + (1) For the very first transition there is only target datum and no redeemer. |
| 36 | + In that case we can only restore the name of the transition, |
| 37 | + i.e. 'Spine Transition' |
| 38 | +
|
| 39 | + (2) For intermidiate transitions we have both datums that identify them and |
| 40 | + additionally redeemer, that contains the whole transition. In that case |
| 41 | + we can restore the whole transition. |
| 42 | +
|
| 43 | + (3) For the final transition the situation is like (2) except the target |
| 44 | + datum is missing, which doesn't matter. |
| 45 | +
|
| 46 | + TODO: How we can improve this in the future: |
| 47 | + * API is probably bad, as we always have some transition like Init state - |
| 48 | + which you can decode, as you have State. If one changes data |
| 49 | + `CEMAction script = MkCEMAction (Params script) (Transition script)` to |
| 50 | + `... = Init (Params script) (State script) |
| 51 | + | Transition (Params script) (Transition script)` |
| 52 | + one could reuse this datatype in all situations. |
| 53 | +-} |
| 54 | +data IndexerEvent script |
| 55 | + = Initial (Spine (Transition script)) |
| 56 | + | -- | TODO: Migrate from (Spine (Transition script)) to (Transition script) |
| 57 | + -- once we have this done: https://github.com/utxorpc/spec/issues/132 |
| 58 | + Following (Spine (Transition script)) -- (Transition script) |
| 59 | + |
| 60 | +deriving stock instance |
| 61 | + (Show (Spine (Transition script))) => |
| 62 | + (Show (IndexerEvent script)) |
| 63 | +deriving stock instance |
| 64 | + (Eq (Spine (Transition script))) => |
| 65 | + (Eq (IndexerEvent script)) |
| 66 | + |
| 67 | +{- | The core function, that extracts an Event out of a Oura transaction. |
| 68 | +It might be a pure function, IO here was used mostly to simplify debugging |
| 69 | +during its development. |
| 70 | +-} |
| 71 | +extractEvent :: |
| 72 | + forall script. |
| 73 | + ( CEMScript script |
| 74 | + , CEMScriptIsData script |
| 75 | + , CEMScriptCompiled script |
| 76 | + ) => |
| 77 | + Ledger.Network -> |
| 78 | + Tx -> |
| 79 | + IO (Maybe (IndexerEvent script)) |
| 80 | +extractEvent network tx = do |
| 81 | + -- Script payemnt credential based predicate |
| 82 | + let ~(Right scriptAddr) = Address.scriptCardanoAddress (Proxy @script) network |
| 83 | + let cPred = hasAddr scriptAddr |
| 84 | + |
| 85 | + -- Source state |
| 86 | + let mOwnInput :: Maybe TxInput = find (cPred . view as_output) (tx ^. inputs) |
| 87 | + let mSourceState :: Maybe (State script) = (extractState . view as_output) =<< mOwnInput |
| 88 | + let mSourceSpine :: Maybe (Spine (State script)) = getSpine <$> mSourceState |
| 89 | + |
| 90 | + -- Target state |
| 91 | + let mOwnOutput :: Maybe TxOutput = find cPred $ tx ^. outputs |
| 92 | + let mTargetState :: Maybe (State script) = extractState =<< mOwnOutput |
| 93 | + let mTargetSpine :: Maybe (Spine (State script)) = getSpine <$> mTargetState |
| 94 | + |
| 95 | + -- Look up the transition |
| 96 | + let transitions = |
| 97 | + first |
| 98 | + (\(_, b, c) -> (b, c)) |
| 99 | + . swap |
| 100 | + <$> Map.toList (transitionStage $ Proxy @script) |
| 101 | + let transSpine = lookup (mSourceSpine, mTargetSpine) transitions |
| 102 | + |
| 103 | + -- Return |
| 104 | + case mOwnInput of |
| 105 | + Nothing -> pure $ Initial <$> transSpine |
| 106 | + Just _ownInput -> do |
| 107 | + -- TODO: fix once Oura has rawCbor for redeemer |
| 108 | + -- rdm <- ownInput ^. redeemer |
| 109 | + -- pure $ Following $ undefined (rdm ^. redeemerPayload) |
| 110 | + pure $ Following <$> transSpine |
| 111 | + |
| 112 | +extractState :: |
| 113 | + forall script. |
| 114 | + (FromData (CEMScriptDatum script)) => |
| 115 | + TxOutput -> |
| 116 | + Maybe (State script) |
| 117 | +extractState MkTxOutput {_datum = mDtm} = |
| 118 | + case mDtm of |
| 119 | + Nothing -> Nothing |
| 120 | + Just dtm -> do |
| 121 | + let MkDatum _ _ cbor = dtm |
| 122 | + let datumAsData :: PlutusLedgerApi.V1.Data = |
| 123 | + cbor |
| 124 | + & C.toPlutusData |
| 125 | + . C.getScriptData |
| 126 | + . fromJust |
| 127 | + . eitherToMaybe |
| 128 | + . C.deserialiseFromCBOR C.AsHashableScriptData |
| 129 | + . B16.decodeBase16Lenient -- use base64 |
| 130 | + . encodeUtf8 |
| 131 | + let ~(Just (_, _, state)) = PlutusLedgerApi.V1.fromData @(CEMScriptDatum script) datumAsData |
| 132 | + pure state |
| 133 | + |
| 134 | +hasAddr :: C.Address C.ShelleyAddr -> TxOutput -> Bool |
| 135 | +hasAddr addr' output = |
| 136 | + let addr = output ^. address |
| 137 | + in fromOuraAddress addr == addr' |
| 138 | + |
| 139 | +fromOuraAddress :: Address -> C.Address C.ShelleyAddr |
| 140 | +fromOuraAddress (MkAddressAsBase64 addr) = |
| 141 | + addr |
| 142 | + & fromJust |
| 143 | + . eitherToMaybe |
| 144 | + . SerialiseRaw.deserialiseFromRawBytes (C.AsAddress C.AsShelleyAddr) |
| 145 | + . B16.decodeBase16Lenient -- use base64 |
| 146 | + . encodeUtf8 |
0 commit comments