{- |
Module      : Singular.Registry.Deployment.Attach
Description : Checking a manifest against a node and attaching to it
License     : Apache-2.0

One owner for the deployment family's node decisions: the compiled
release halves a manifest pins only by hash ('CageParts',
'cageConfigFor'), the registry token a seed
determines, a live carrier of each recorded script found by its hash
wherever it sits ("Singular.Registry.StateToken"; the recorded output
reference is not read), the registry's state output, and the two
operations built on them — 'verifyDeployment', which goes on
to read the live state datum and refuse a mismatched active policy or
process\/retract window, and 'attach', which returns the resolved
outputs without those additional checks. Neither operation loads or
compares a mirror; the caller that attaches does that afterwards.

This module is an internal owner behind the 'Singular.Registry.Deployment'
facade: callers import the facade, which re-exports the unchanged public
surface. It reads manifest values, 'parseOutRef', 'renderOutRef',
'hex' and 'die' from "Singular.Registry.Deployment.Manifest" and the
focused identity and lookup adapters from the builder's internal
family.
-}
module Singular.Registry.Deployment.Attach
    ( -- * The release halves the manifest pins only by hash
      CageParts (..)
    , cageConfigFor

      -- * Checking one against a node
    , verifyDeployment

      -- * Attaching a run to one
    , Attached (..)
    , attach
    ) where

import Control.Monad (unless, when)
import Data.ByteString.Base16 qualified as B16
import Data.ByteString.Char8 qualified as BC
import Data.ByteString.Short qualified as SBS
import Data.List (minimumBy)
import Data.Ord (comparing)
import Data.Text (Text)
import Data.Text qualified as T

import Cardano.Crypto.Hash.Class (hashFromBytes)
import Cardano.Ledger.Api.Tx.In (TxIn (..))
import Cardano.Ledger.Api.Tx.Out (TxOut)
import Cardano.Ledger.BaseTypes (Network (..))
import Cardano.Ledger.Hashes (ScriptHash (..))
import Data.Map.Strict qualified as Map

import Singular.Registry.AssetName (deriveAssetName)
import Singular.Registry.Config (CageConfig (..))
import Singular.Registry.Deployment.Manifest
    ( Deployment (..)
    , ReferenceScript (..)
    , die
    , hex
    , parseOutRef
    , renderOutRef
    )
import Singular.Registry.Evidence qualified as Cage
import Singular.Registry.Ledger
    ( AssetName (..)
    , Coin (..)
    , ConwayEra
    , TokenId (..)
    )
import Singular.Registry.LedgerProvider qualified as Cage
import Singular.Registry.SessionIO qualified as Cage
import Singular.Registry.StateToken
    ( ReferenceRefusal (..)
    , carriesReference
    , findReferences
    , parseRole
    , renderReferenceRefusal
    )
import Singular.Registry.TxBuilder.Internal.Identity
    ( cageAddrFromCfg
    , cagePolicyIdFromCfg
    , computeScriptHash
    , extractCageDatum
    , scriptHashBytes
    , txInToRef
    )
import Singular.Registry.TxBuilder.Internal.Lookup (findStateUtxo)
import Singular.Registry.Types
    ( CageDatum (..)
    , OnChainTokenState (..)
    , stateActivePolicyBytes
    )

-- ---------------------------------------------------------
-- The release halves the manifest pins only by hash
-- ---------------------------------------------------------

{- | The compiled bytes a release ships. The manifest records their
hashes; the bytes themselves come from the blueprints in hand, so a
manifest can never be used to smuggle in a different validator.
-}
data CageParts = CageParts
    { CageParts -> ShortByteString
partsStateBytes :: SBS.ShortByteString
    , CageParts -> ShortByteString
partsRequestBytes :: SBS.ShortByteString
    , CageParts -> ShortByteString
partsApplicationPolicy :: SBS.ShortByteString
    {- ^ The application policy the registry pins (#157 genesis-policy-pins): the
    naming application script's applied hash for this registry
    identity, read from the partitions' `script-identity.json`.
    -}
    , CageParts -> ShortByteString
partsActivePolicy :: SBS.ShortByteString
    -- ^ The active-token policy: `witness(1, registry)` applied.
    , CageParts -> ShortByteString
partsAbsentPolicy :: SBS.ShortByteString
    -- ^ The absent-token policy: `witness(0, registry)` applied.
    , CageParts -> ShortByteString
partsTerminalPolicy :: SBS.ShortByteString
    -- ^ The terminal-token policy: `witness(2, registry)` applied.
    , CageParts -> ShortByteString
partsConsumerScript :: SBS.ShortByteString
    }

{- | The cage configuration a manifest describes: this release's
compiled scripts, with the seed and the economics the deployment was
booted with.

Refuses when the release in hand compiles the state validator to a
different hash than the deployment was made with — the manifest then
belongs to another release and attaching to it would spend against a
registry whose validator this code does not implement.
-}
cageConfigFor :: Deployment -> CageParts -> Either String CageConfig
cageConfigFor :: Deployment -> CageParts -> Either String CageConfig
cageConfigFor Deployment
dep CageParts
parts = do
    seedIn <- Text -> Either String TxIn
parseOutRef (Deployment -> Text
depSeedOutRef Deployment
dep)
    -- The manifest keeps its own vocabulary for the pin it recorded; the
    -- field it names is the one #157 state-datum-fields renamed the active policy.
    when
        ( T.pack (hex (SBS.fromShort (partsActivePolicy parts)))
            /= depRepresentativePolicy dep
        )
        $ Left
            "this release's registry-bound active policy differs from the deployment"
    let stateHash = ShortByteString -> ScriptHash
computeScriptHash (CageParts -> ShortByteString
partsStateBytes CageParts
parts)
        stateHex = String -> Text
T.pack (ByteString -> String
hex (ScriptHash -> ByteString
scriptHashBytes ScriptHash
stateHash))
    if stateHex /= depStatePolicy dep
        then
            Left
                ( "this release compiles the state validator to 0x"
                    <> T.unpack stateHex
                    <> " but the deployment was made with 0x"
                    <> T.unpack (depStatePolicy dep)
                    <> ": the manifest belongs to another release"
                )
        else
            Right
                CageConfig
                    { cageScriptBytes = partsStateBytes parts
                    , requestScriptBytes = partsRequestBytes parts
                    , cfgScriptHash = stateHash
                    , cageSeed = txInToRef seedIn
                    , defaultProcessTime = depProcessTime dep
                    , defaultRetractTime = depRetractTime dep
                    , defaultTip = Coin (depTip dep)
                    , cfgApplicationPolicy = partsApplicationPolicy parts
                    , cfgActivePolicy = partsActivePolicy parts
                    , cfgAbsentPolicy = partsAbsentPolicy parts
                    , cfgTerminalPolicy = partsTerminalPolicy parts
                    , cfgConsumerScript = partsConsumerScript parts
                    , network = Testnet
                    }

{- | The registry token a seed determines. Derived, never trusted from
the file: the manifest's own claim is checked against it.
-}
tokenFor :: Deployment -> Either String TokenId
tokenFor :: Deployment -> Either String TokenId
tokenFor Deployment
dep = do
    seedIn <- Text -> Either String TxIn
parseOutRef (Deployment -> Text
depSeedOutRef Deployment
dep)
    let name = OnChainTxOutRef -> ByteString
deriveAssetName (TxIn -> OnChainTxOutRef
txInToRef TxIn
seedIn)
        nameHex = String -> Text
T.pack (ByteString -> String
hex ByteString
name)
    if nameHex /= depCageToken dep
        then
            Left
                ( "the seed "
                    <> T.unpack (depSeedOutRef dep)
                    <> " determines registry token 0x"
                    <> T.unpack nameHex
                    <> " but the manifest records 0x"
                    <> T.unpack (depCageToken dep)
                )
        else Right (TokenId (AssetName (SBS.toShort name)))

-- ---------------------------------------------------------
-- Checking a manifest against a node
-- ---------------------------------------------------------

{- | Ask a node whether it still agrees with a manifest, and return what
it answered, one line per claim.

Every claim is about the chain or about the compiled release, never
about the file restating itself: the seed determines the recorded
token, this release compiles to the recorded state hash, every recorded
reference output is live at its recorded address carrying the hash the
manifest pins, and the registry's state output carries the recorded
token under the recorded policy. The first claim that fails is raised
by name.
-}
verifyDeployment
    :: Cage.Session Cage.NoWitness IO
    -> Deployment
    -> CageParts
    -> IO [String]
verifyDeployment :: Session NoWitness IO -> Deployment -> CageParts -> IO [String]
verifyDeployment Session NoWitness IO
view Deployment
dep CageParts
parts = do
    cfg <- (String -> IO CageConfig)
-> (CageConfig -> IO CageConfig)
-> Either String CageConfig
-> IO CageConfig
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either String -> IO CageConfig
forall a. String -> IO a
die CageConfig -> IO CageConfig
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Deployment -> CageParts -> Either String CageConfig
cageConfigFor Deployment
dep CageParts
parts)
    tok <- either die pure (tokenFor dep)
    refs <- referencesByHash view dep
    (stateIn, stateOut) <- resolveStateUtxo view cfg tok
    stateLive <- case extractCageDatum stateOut of
        Just (StateDatum OnChainTokenState
st)
            | OnChainTokenState -> ByteString
stateActivePolicyBytes OnChainTokenState
st ByteString -> ByteString -> Bool
forall a. Eq a => a -> a -> Bool
== ShortByteString -> ByteString
SBS.fromShort (CageParts -> ShortByteString
partsActivePolicy CageParts
parts) ->
                OnChainTokenState -> IO OnChainTokenState
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure OnChainTokenState
st
        Maybe CageDatum
_ ->
            String -> IO OnChainTokenState
forall a. String -> IO a
die
                String
"the live registry state does not configure this registry-bound representative policy"
    unless
        ( stateProcessTime stateLive == depProcessTime dep
            && stateRetractTime stateLive == depRetractTime dep
        )
        $ die
            ( "the live registry state carries process/retract windows "
                <> show (stateProcessTime stateLive, stateRetractTime stateLive)
                <> " but the manifest records "
                <> show (depProcessTime dep, depRetractTime dep)
            )
    pure
        ( [ "release "
                <> T.unpack (depRelease dep)
                <> " compiles the state validator to the recorded 0x"
                <> T.unpack (depStatePolicy dep)
          , "seed "
                <> T.unpack (depSeedOutRef dep)
                <> " determines the recorded registry token 0x"
                <> T.unpack (depCageToken dep)
          , "compiled representative policy agrees with the manifest and live registry configuration: 0x"
                <> T.unpack (depRepresentativePolicy dep)
          , "registry state carries the recorded request windows: process "
                <> show (depProcessTime dep)
                <> " ms, retract "
                <> show (depRetractTime dep)
                <> " ms"
          ]
            <> [ "reference script "
                    <> T.unpack (refRole r)
                    <> " live at "
                    <> T.unpack (renderOutRef carrier)
                    <> " carrying 0x"
                    <> T.unpack (refHash r)
               | (r, (carrier, _)) <- refs
               ]
            <> [ "registry state output "
                    <> T.unpack (renderOutRef stateIn)
                    <> " carries the recorded token"
               ]
        )

{- | A live carrier of each script the manifest records, found by its hash
wherever it sits ("Singular.Registry.StateToken"): the recorded output
reference is not read. The registry roles go through the reference
search; the deployment family's custody script, which no registry
transaction runs, is found by the same rule on its own. Each is
returned beside its manifest entry, in manifest order.
-}
referencesByHash
    :: Cage.Session Cage.NoWitness IO
    -> Deployment
    -> IO [(ReferenceScript, (TxIn, TxOut ConwayEra))]
referencesByHash :: Session NoWitness IO
-> Deployment -> IO [(ReferenceScript, (TxIn, TxOut ConwayEra))]
referencesByHash Session NoWitness IO
view Deployment
dep = do
    recorded <- (ReferenceScript
 -> IO (ReferenceScript, Maybe ReferenceRole, ScriptHash))
-> [ReferenceScript]
-> IO [(ReferenceScript, Maybe ReferenceRole, ScriptHash)]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
(a -> m b) -> t a -> m (t b)
forall (m :: * -> *) a b. Monad m => (a -> m b) -> [a] -> m [b]
mapM ReferenceScript
-> IO (ReferenceScript, Maybe ReferenceRole, ScriptHash)
roleOf (Deployment -> [ReferenceScript]
depReferenceScripts Deployment
dep)
    let expected = [(ReferenceRole, ScriptHash)] -> Map ReferenceRole ScriptHash
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [(ReferenceRole
role, ScriptHash
hash) | (ReferenceScript
_, Just ReferenceRole
role, ScriptHash
hash) <- [(ReferenceScript, Maybe ReferenceRole, ScriptHash)]
recorded]
    found <-
        findReferences view Nothing expected (Map.keysSet expected)
            >>= either (die . T.unpack . renderReferenceRefusal) pure
    let carrier (a
r, Just ReferenceRole
role, ScriptHash
_) = (a, (TxIn, BabbageTxOut ConwayEra))
-> IO (a, (TxIn, BabbageTxOut ConwayEra))
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (a
r, Map ReferenceRole (TxIn, BabbageTxOut ConwayEra)
found Map ReferenceRole (TxIn, BabbageTxOut ConwayEra)
-> ReferenceRole -> (TxIn, BabbageTxOut ConwayEra)
forall k a. Ord k => Map k a -> k -> a
Map.! ReferenceRole
role)
        carrier (a
r, Maybe ReferenceRole
Nothing, ScriptHash
hash) = do
            carried <- Session NoWitness IO -> ScriptHash -> IO (TxIn, TxOut ConwayEra)
custodyCarrier Session NoWitness IO
view ScriptHash
hash
            pure (r, carried)
    mapM carrier recorded
  where
    {- The manifest's role vocabulary. A registry role goes to the
    reference search, spelled as receipts spell it; the custody script
    the deployment publishes beside them is looked up by its hash alone.
    Anything else the manifest records is unknown, refused by name: the
    closed set is what keeps the producer's vocabulary and this
    consumer's one set.
    -}
    roleOf :: ReferenceScript
-> IO (ReferenceScript, Maybe ReferenceRole, ScriptHash)
roleOf ReferenceScript
r = do
        role <-
            if ReferenceScript -> Text
refRole ReferenceScript
r Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
custodyRole
                then Maybe ReferenceRole -> IO (Maybe ReferenceRole)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe ReferenceRole
forall a. Maybe a
Nothing
                else case Text -> Maybe ReferenceRole
parseRole (ReferenceScript -> Text
refRole ReferenceScript
r) of
                    Just ReferenceRole
registryRole -> Maybe ReferenceRole -> IO (Maybe ReferenceRole)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (ReferenceRole -> Maybe ReferenceRole
forall a. a -> Maybe a
Just ReferenceRole
registryRole)
                    Maybe ReferenceRole
Nothing ->
                        String -> IO (Maybe ReferenceRole)
forall a. String -> IO a
die
                            ( String
"the deployment records an unknown reference role "
                                String -> String -> String
forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack (ReferenceScript -> Text
refRole ReferenceScript
r)
                            )
        hash <-
            maybe
                ( die
                    ( "the deployment's "
                        <> T.unpack (refRole r)
                        <> " hash is not a script hash"
                    )
                )
                pure
                (decodeScriptHash (refHash r))
        pure (r, role, hash)

-- | The manifest's own name for the custody script it publishes.
custodyRole :: Text
custodyRole :: Text
custodyRole = Text
"custody"

{- | A live carrier of the custody script, by its hash wherever it sits:
the rule the reference search applies to registry roles, with no wallet
to fall back on — a run attached to a deployment spends the publication
its manifest names.
-}
custodyCarrier
    :: Cage.Session Cage.NoWitness IO
    -> ScriptHash
    -> IO (TxIn, TxOut ConwayEra)
custodyCarrier :: Session NoWitness IO -> ScriptHash -> IO (TxIn, TxOut ConwayEra)
custodyCarrier Session NoWitness IO
view ScriptHash
hash = do
    answer <-
        Session NoWitness IO
-> OutputQuery
-> IO (Either ReadFailure (Evidenced NoWitness Outputs))
forall w (m :: * -> *).
Session w m
-> OutputQuery -> m (Either ReadFailure (Evidenced w Outputs))
Cage.outputs Session NoWitness IO
view (ScriptHash -> OutputQuery
Cage.CarryingReferenceScript ScriptHash
hash)
            IO
  (Either
     ReadFailure (Evidenced NoWitness [(TxIn, BabbageTxOut ConwayEra)]))
-> (Either
      ReadFailure (Evidenced NoWitness [(TxIn, BabbageTxOut ConwayEra)])
    -> IO [(TxIn, BabbageTxOut ConwayEra)])
-> IO [(TxIn, BabbageTxOut ConwayEra)]
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= (ReadFailure -> IO [(TxIn, BabbageTxOut ConwayEra)])
-> (Evidenced NoWitness [(TxIn, BabbageTxOut ConwayEra)]
    -> IO [(TxIn, BabbageTxOut ConwayEra)])
-> Either
     ReadFailure (Evidenced NoWitness [(TxIn, BabbageTxOut ConwayEra)])
-> IO [(TxIn, BabbageTxOut ConwayEra)]
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either
                (String -> IO [(TxIn, BabbageTxOut ConwayEra)]
forall a. String -> IO a
die (String -> IO [(TxIn, BabbageTxOut ConwayEra)])
-> (ReadFailure -> String)
-> ReadFailure
-> IO [(TxIn, BabbageTxOut ConwayEra)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> String
T.unpack (Text -> String) -> (ReadFailure -> Text) -> ReadFailure -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ReferenceRefusal -> Text
renderReferenceRefusal (ReferenceRefusal -> Text)
-> (ReadFailure -> ReferenceRefusal) -> ReadFailure -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ReadFailure -> ReferenceRefusal
ReferenceUnreadable)
                ([(TxIn, BabbageTxOut ConwayEra)]
-> IO [(TxIn, BabbageTxOut ConwayEra)]
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([(TxIn, BabbageTxOut ConwayEra)]
 -> IO [(TxIn, BabbageTxOut ConwayEra)])
-> (Evidenced NoWitness [(TxIn, BabbageTxOut ConwayEra)]
    -> [(TxIn, BabbageTxOut ConwayEra)])
-> Evidenced NoWitness [(TxIn, BabbageTxOut ConwayEra)]
-> IO [(TxIn, BabbageTxOut ConwayEra)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Evidenced NoWitness [(TxIn, BabbageTxOut ConwayEra)]
-> [(TxIn, BabbageTxOut ConwayEra)]
forall w a. Evidenced w a -> a
Cage.value)
    case filter (carriesReference hash . snd) answer of
        [] ->
            String -> IO (TxIn, BabbageTxOut ConwayEra)
forall a. String -> IO a
die
                ( String
"the deployment's custody script 0x"
                    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ByteString -> String
hex (ScriptHash -> ByteString
scriptHashBytes ScriptHash
hash)
                    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
" has no live carrier this provider reports"
                )
        [(TxIn, BabbageTxOut ConwayEra)]
carriers -> (TxIn, BabbageTxOut ConwayEra) -> IO (TxIn, BabbageTxOut ConwayEra)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (((TxIn, BabbageTxOut ConwayEra)
 -> (TxIn, BabbageTxOut ConwayEra) -> Ordering)
-> [(TxIn, BabbageTxOut ConwayEra)]
-> (TxIn, BabbageTxOut ConwayEra)
forall (t :: * -> *) a.
Foldable t =>
(a -> a -> Ordering) -> t a -> a
minimumBy (((TxIn, BabbageTxOut ConwayEra) -> TxIn)
-> (TxIn, BabbageTxOut ConwayEra)
-> (TxIn, BabbageTxOut ConwayEra)
-> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing (TxIn, BabbageTxOut ConwayEra) -> TxIn
forall a b. (a, b) -> a
fst) [(TxIn, BabbageTxOut ConwayEra)]
carriers)

-- | The registry's state output, by the token it must carry.
resolveStateUtxo
    :: Cage.Session Cage.NoWitness IO
    -> CageConfig
    -> TokenId
    -> IO (TxIn, TxOut ConwayEra)
resolveStateUtxo :: Session NoWitness IO
-> CageConfig -> TokenId -> IO (TxIn, TxOut ConwayEra)
resolveStateUtxo Session NoWitness IO
view CageConfig
cfg TokenId
tok = do
    let stateAddr :: Addr
stateAddr = CageConfig -> Network -> Addr
cageAddrFromCfg CageConfig
cfg Network
Testnet
    utxos <- Session NoWitness IO -> Addr -> IO Outputs
forall w. Session w IO -> Addr -> IO Outputs
Cage.outputsAt Session NoWitness IO
view Addr
stateAddr
    case findStateUtxo (cagePolicyIdFromCfg cfg) tok utxos of
        Just (TxIn, TxOut ConwayEra)
u -> (TxIn, BabbageTxOut ConwayEra) -> IO (TxIn, BabbageTxOut ConwayEra)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (TxIn, TxOut ConwayEra)
(TxIn, BabbageTxOut ConwayEra)
u
        Maybe (TxIn, TxOut ConwayEra)
Nothing ->
            String -> IO (TxIn, BabbageTxOut ConwayEra)
forall a. String -> IO a
die
                String
"no output at the registry address carries the recorded \
                \token; the node does not know this deployment (wrong \
                \network, or the registry was never booted here)"

-- ---------------------------------------------------------
-- Attaching
-- ---------------------------------------------------------

-- | What a run gets instead of booting and publishing.
data Attached = Attached
    { Attached -> CageConfig
attCfg :: CageConfig
    -- ^ The deployed registry's configuration
    , Attached -> TokenId
attToken :: TokenId
    -- ^ The deployed registry's token
    , Attached -> Outputs
attRefUtxos :: [(TxIn, TxOut ConwayEra)]
    -- ^ The published reference outputs, in manifest order
    , Attached -> (TxIn, TxOut ConwayEra)
attStateUtxo :: (TxIn, TxOut ConwayEra)
    -- ^ The registry's current state output
    }

{- | Attach a run to a recorded deployment: the release, token and
reference\/state resolution 'verifyDeployment' performs, plus the
resolved outputs a runner needs in hand. The live state's active policy
and windows are not checked here; verification is the operation that
reads them.
-}
attach
    :: Cage.Session Cage.NoWitness IO
    -> Deployment
    -> CageParts
    -> IO Attached
attach :: Session NoWitness IO -> Deployment -> CageParts -> IO Attached
attach Session NoWitness IO
view Deployment
dep CageParts
parts = do
    cfg <- (String -> IO CageConfig)
-> (CageConfig -> IO CageConfig)
-> Either String CageConfig
-> IO CageConfig
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either String -> IO CageConfig
forall a. String -> IO a
die CageConfig -> IO CageConfig
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Deployment -> CageParts -> Either String CageConfig
cageConfigFor Deployment
dep CageParts
parts)
    tok <- either die pure (tokenFor dep)
    refs <- referencesByHash view dep
    state <- resolveStateUtxo view cfg tok
    pure
        Attached
            { attCfg = cfg
            , attToken = tok
            , attRefUtxos = map snd refs
            , attStateUtxo = state
            }

-- ---------------------------------------------------------
-- Small helpers
-- ---------------------------------------------------------

-- | A recorded script hash, from its hex.
decodeScriptHash :: Text -> Maybe ScriptHash
decodeScriptHash :: Text -> Maybe ScriptHash
decodeScriptHash Text
t = case ByteString -> Either String ByteString
B16.decode (String -> ByteString
BC.pack (Text -> String
T.unpack Text
t)) of
    Right ByteString
raw -> Hash ADDRHASH EraIndependentScript -> ScriptHash
ScriptHash (Hash ADDRHASH EraIndependentScript -> ScriptHash)
-> Maybe (Hash ADDRHASH EraIndependentScript) -> Maybe ScriptHash
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ByteString -> Maybe (Hash ADDRHASH EraIndependentScript)
forall h a. HashAlgorithm h => ByteString -> Maybe (Hash h a)
hashFromBytes ByteString
raw
    Left String
_ -> Maybe ScriptHash
forall a. Maybe a
Nothing