Skip to content

Singletons

Every major component is a record of functions, polymorphic in the monad m. No typeclasses are used — dependencies are explicit and passed as values. This makes testing trivial: swap a record for a mock.

Provider

Blockchain queries (read-only). Queries a Cardano node via N2C LocalStateQuery.

Implementation: mkNodeClientProvider (real N2C, in Provider.NodeClient)

data Provider m = Provider
    { queryUTxOs
        :: Addr
        -> m [(TxIn, TxOut ConwayEra)]
    -- ^ Look up UTxOs at an address
    , queryProtocolParams
        :: m (PParams ConwayEra)
    -- ^ Fetch current protocol parameters
    , evaluateTx
        :: ByteString -> m ExUnits
    -- ^ Evaluate execution units for a serialised
    -- CBOR transaction (not yet implemented)
    }
graph LR
    PRV["Provider"]
    NODE["Cardano Node<br/>(LocalStateQuery)"]

    PRV -->|queryUTxOs| NODE
    PRV -->|queryProtocolParams| NODE

TrieManager

Manages a map of token identifiers to MPF tries. Each token has its own isolated trie, sharing the same RocksDB column families with per-token HexKey prefix scoping.

Transactional: mkUnifiedTrieManager — composes into the caller's Transaction (used by CageFollower for atomic block processing) IO: mkPersistentTrieManager — auto-commits + IORef caches (used by TxBuilder and speculative sessions) Test: mkPureTrieManager — in-memory (in Trie.PureManager)

data TrieManager m = TrieManager
    { withTrie
        :: forall a
         . TokenId
        -> (Trie m -> m a)
        -> m a
    -- ^ Run an action with access to a token's trie
    , createTrie :: TokenId -> m ()
    -- ^ Create a new empty trie for a token
    , deleteTrie :: TokenId -> m ()
    -- ^ Delete a token's trie
    }

data Trie m = Trie
    { insert
        :: ByteString -> ByteString -> m Root
    , delete :: ByteString -> m Root
    , lookup :: ByteString -> m (Maybe ByteString)
    , getRoot :: m Root
    , getProof :: ByteString -> m (Maybe Proof)
    }

State

Token and request state tracking. Three sub-records for tokens, requests, and chain sync checkpoints.

Transactional: mkTransactionalState — composes into the caller's Transaction (used by CageFollower) IO: mkPersistentState — auto-commits via hoistState (in Indexer.Persistent) Test: mkMockState — in-memory (in Mock.State)

data State m = State
    { tokens :: Tokens m
    , requests :: Requests m
    , checkpoints :: Checkpoints m
    }

data Tokens m = Tokens
    { getToken :: TokenId -> m (Maybe TokenState)
    , putToken :: TokenId -> TokenState -> m ()
    , removeToken :: TokenId -> m ()
    , listTokens :: m [TokenId]
    }

data Requests m = Requests
    { getRequest :: TxIn -> m (Maybe Request)
    , putRequest :: TxIn -> Request -> m ()
    , removeRequest :: TxIn -> m ()
    , requestsByToken :: TokenId -> m [Request]
    }

data Checkpoints m = Checkpoints
    { getCheckpoint :: m (Maybe (SlotNo, BlockId))
    , putCheckpoint :: SlotNo -> BlockId -> m ()
    }

Indexer

Chain sync follower with lifecycle control.

Real: CageFollower — processes each block in a single atomic transaction covering UTxO, cage state, tries, and rollback Mock: mkSkeletonIndexer (lifecycle-only skeleton, in Mock.Skeleton)

data ChainTip = ChainTip
    { tipSlot :: SlotNo
    , tipBlockId :: BlockId
    }

data Indexer m = Indexer
    { start :: m ()
    , stop :: m ()
    , pause :: m ()
    , resume :: m ()
    , getTip :: m ChainTip
    }

Submitter

Transaction submission via N2C LocalTxSubmission. Takes a full ledger Tx ConwayEra and returns a SubmitResult.

Implementation: mkN2CSubmitter (real N2C, in Submitter.N2C)

data SubmitResult
    = Submitted TxId
    | Rejected ByteString

newtype Submitter m = Submitter
    { submitTx :: Tx ConwayEra -> m SubmitResult
    }

TxBuilder

Constructs transactions for all MPFS protocol operations. Returns full ledger Tx values ready for signing.

Mock: mkMockTxBuilder (in Mock.TxBuilder) Real: mkRealTxBuilder (in TxBuilder.Real)

data TxBuilder m = TxBuilder
    { bootToken
        :: Addr -> m (Tx ConwayEra)
    -- ^ Create a new MPFS token
    , requestInsert
        :: TokenId -> ByteString -> ByteString
        -> Addr -> m (Tx ConwayEra)
    -- ^ Request inserting a key-value pair
    , requestDelete
        :: TokenId -> ByteString
        -> Addr -> m (Tx ConwayEra)
    -- ^ Request deleting a key
    , updateToken
        :: TokenId -> Addr -> m (Tx ConwayEra)
    -- ^ Process pending requests for a token
    , retractRequest
        :: TxIn -> Addr -> m (Tx ConwayEra)
    -- ^ Cancel a pending request
    , endToken
        :: TokenId -> Addr -> m (Tx ConwayEra)
    -- ^ Retire an MPFS token
    }

Balance

Pure transaction balancing function (not a singleton record), in Core.Balance. Adds a fee-paying UTxO and change output, finding the fee via a fixpoint loop over estimateMinFeeTx.

balanceTx
    :: PParams ConwayEra
    -> (TxIn, TxOut ConwayEra)   -- fee-paying UTxO
    -> Addr                       -- change address
    -> Tx ConwayEra               -- unbalanced tx
    -> Either BalanceError (Tx ConwayEra)

The fee estimation iterates until stable (max 10 rounds, crashes if not converged). One key witness is assumed for the fee input.


Context

Facade record that bundles all singletons into a single environment.

data Context m = Context
    { provider :: Provider m
    , trieManager :: TrieManager m
    , state :: State m
    , indexer :: Indexer m
    , submitter :: Submitter m
    , txBuilder :: TxBuilder m
    , utxoExists :: TxIn -> m Bool
    }
graph TD
    CTX["Context"]
    PRV["Provider"]
    TM["TrieManager"]
    ST["State"]
    IDX["Indexer"]
    SUB["Submitter"]
    TXB["TxBuilder"]

    CTX --> PRV
    CTX --> TM
    CTX --> ST
    CTX --> IDX
    CTX --> SUB
    CTX --> TXB

HTTP Server

mkApp from HTTP.Server wraps a Context IO into a WAI Application with Swagger UI at /swagger-ui. Each HTTP handler extracts the relevant interface from Context and delegates to it.