{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilyDependencies #-}
module Data.UTxO.Transaction
(
-- * Overview
-- $overview
-- * Abstraction
MkPayment (..)
, ErrMkPayment (..)
-- * Note about type-level constraints
-- $note
) where
import Prelude
import Data.ByteString
( ByteString )
-- $overview
--
-- This module offers an abstraction over the construction of simple UTxO
-- transactions. Such a transaction is called a /Payment/. Payments are
-- constructed by selecting inputs and outputs, and signing each input
-- independently.
--
-- This can be represented as a state machine, such as:
--
-- @
-- empty
-- |
-- *-----------------* | *----------------*
-- | | | | |
-- | v v v |
-- *--- addOutput ---=========--- addInput ---*
-- |
-- |
-- lock *----------------*
-- | | |
-- | v |
-- =========--- signWith ---*
-- |
-- |
-- serialize
-- |
-- |
-- v
-- ...
-- @
--
-- Functions from the 'MkPayment' interfaces are designed to be easy to chain
-- like pipes. To give a concrete example, let's see how to construct a
-- transaction with /one input/ and /two outputs/:
--
-- @
-- import Data.Function
-- ( (&) )
-- import qualified Data.UTxO.Transaction as Tx
--
-- bytes = Tx.empty
-- & Tx.addInput input0
-- & Tx.addOutput output0
-- & Tx.addOutput output1
-- & Tx.lock
-- & Tx.signWith key0
-- & Tx.serialize
-- @
--
-- The reason why the 'lock' step is required is because, in order to create
-- witnesses, one must produce signatures of the whole transaction body (inputs
-- + outputs). Hence, signatures can only be added once all inputs and all
-- outputs have already been addded.
--
-- This is correct-by-construction thanks to the 'lock' step and the distinction
-- between 'CoinSel' and 'Tx'.
-- | Abstract interface for constructing a /Payment/.
class MkPayment dom where
-- | Some initial extra data needed to construct and or serialize the
-- transaction. This is a free-type which is implementation-specific.
--
-- For example, Byron transactions requires the underlying protocol magic in
-- order to produce witnesses. This is an appropriate place for.
type Init dom :: *
-- | Type representation of transaction 'Input'
type Input dom = inp | inp -> dom
-- | Type representation of transaction 'Output'
type Output dom = out | out -> dom
-- | Type representation of transaction 'SignKey'
type SignKey dom = key | key -> dom
-- | An intermediate type for representing a transaction under construction.
-- 'Input' and 'Output' can still be added to a 'CoinSel' and must be added
-- before trying to sign any of the inputs.
type CoinSel dom = sel | sel -> dom
-- | A transaction is a locked 'CoinSel' to which it is no longer possible
-- to add new 'Input' and or 'Output'. Only 'Tx' can be serialized.
type Tx dom = tx | tx -> dom
-- * Transaction construction
empty :: Init dom -> CoinSel dom
addInput :: Input dom -> CoinSel dom -> CoinSel dom
addOutput :: Output dom -> CoinSel dom -> CoinSel dom
-- * Transaction signature
lock :: CoinSel dom -> Tx dom
signWith :: SignKey dom -> Tx dom -> Tx dom
-- * Transaction serialization
serialize :: Tx dom -> Either ErrMkPayment ByteString
-- | Possible errors occuring when constructing a /Payment/. This can in
-- practice be safely ignored if the transaction is known to be well-formed.
data ErrMkPayment
= MissingInput
-- ^ Payments must have at least one input.
| MissingOutput
-- ^ Payments must have at least one output
| MissingSignature
-- ^ Payments must have a signature for each input.
deriving (Show, Read, Eq)
-- $note
--
-- It is relatively easy to tweak the interface below to get it fully type-safe
-- by using an inductive type, such as:
--
-- @
-- data N = Zero | Some N
--
-- class MkPayment dom where
-- type Init dom :: *
--
-- type Input dom = inp | inp -> dom
-- type Output dom = out | out -> dom
-- type SignKey dom = key | key -> dom
--
-- type CoinSel dom = (sel :: N -> N -> *) | sel -> dom
-- type Tx dom = (tx :: N -> *) | tx -> dom
--
-- empty :: Init dom -> CoinSel dom Zero Zero
--
-- addInput :: Input dom -> CoinSel dom n m -> CoinSel dom (Some n) m
-- addOutput :: Output dom -> CoinSel dom n m -> CoinSel dom n (Some m)
--
-- lock :: CoinSel dom (Some n) (Some m) -> Tx dom (Some n)
--
-- signWith :: SignKey dom -> Tx dom (Some n) -> Tx dom n
--
-- serialize :: Tx dom Zero -> ByteString
-- @
--
-- Such approach is great for ensuring that the transaction can't be ill-formed
-- and works well when manually constructing the transaction as shown above.
--
-- It gets however quite complex when one wants to start folding or mapping on a
-- list of inputs or outputs where their numbers isn't known at compile-time!
-- Indeed, each call to 'addInput' does actually return a new type!
--
-- Let's say we want to write the following function:
--
-- @
-- addMany :: [Input dom] -> CoinSel dom n m -> CoinSel dom ? m
-- @
--
-- What should be the type of `?`. This can't possibly known at compile-time.