packages feed

cardano-transactions-1.0.0: app/Main.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}

module Main where

import Prelude

import Control.Applicative
    ( (<|>) )
import Control.Exception
    ( bracket )
import Control.Monad
    ( guard, void )
import Data.ByteArray.Encoding
    ( Base (..), convertFromBase, convertToBase )
import Data.ByteString
    ( ByteString )
import Data.Maybe
    ( isNothing )
import Data.Text
    ( Text )
import Data.UTxO.Transaction
    ( MkPayment (..) )
import Data.UTxO.Transaction.Cardano.Byron
    ( Byron
    , decodeCoinSel
    , decodeTx
    , encodeCoinSel
    , encodeTx
    , fromBase16
    , fromBase58
    , mkInit
    , mkInput
    , mkOutput
    , mkSignKey
    )
import Data.Word
    ( Word32 )
import Numeric.Natural
    ( Natural )
import Options.Applicative
    ( ArgumentFields
    , CommandFields
    , Mod
    , Parser
    , ParserInfo
    , argument
    , auto
    , command
    , customExecParser
    , flag
    , flag'
    , footerDoc
    , header
    , headerDoc
    , helper
    , info
    , long
    , maybeReader
    , metavar
    , prefs
    , progDesc
    , showHelpOnEmpty
    , subparser
    )
import Options.Applicative.Help.Pretty
    ( hardline, indent, string, vsep )
import System.Console.ANSI
    ( Color (..)
    , ColorIntensity (..)
    , ConsoleLayer (..)
    , SGR (..)
    , hSetSGR
    , hSupportsANSIWithoutEmulation
    )
import System.Exit
    ( exitFailure )
import System.IO
    ( Handle, hIsTerminalDevice, stderr, stdin, stdout )

import qualified Codec.CBOR.Decoding as CBOR
import qualified Codec.CBOR.Encoding as CBOR
import qualified Codec.CBOR.Read as CBOR
import qualified Codec.CBOR.Write as CBOR
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.IO as TIO
import qualified Data.UTxO.Transaction as Tx


main :: IO ()
main = setup >> parseCmd >>= \case
    CmdEmpty Empty{protocolMagic} -> do
        let state0 = Tx.empty protocolMagic
        hPutState stdout (encodeCoinSel state0)

    CmdAddInput AddInput{inputIx,inputTxId} -> do
        state <- hGetState stdin decodeCoinSel
        case mkInput inputIx inputTxId of
            Nothing -> failWith "Invalid input index or transaction id."
            Just input -> do
                let state' = Tx.addInput input state
                hPutState stdout (encodeCoinSel state')

    CmdAddOutput AddOutput{coin,address} -> do
        state <- hGetState stdin decodeCoinSel
        case mkOutput coin address of
            Nothing -> failWith "Invalid output value or address."
            Just output -> do
                let state' = Tx.addOutput output state
                hPutState stdout (encodeCoinSel state')

    CmdLock -> do
        state <- hGetState stdin decodeCoinSel
        let state' = Tx.lock state
        hPutState stdout (encodeTx state')

    CmdSignWith SignWith{prvKey} -> do
        state <- hGetState stdin decodeTx
        case mkSignKey prvKey of
            Nothing -> failWith "Invalid signing key."
            Just signKey -> do
                let state' = Tx.signWith signKey state
                hPutState stdout (encodeTx state')

    CmdSerialize Serialize{base} -> do
        state <- hGetState stdin decodeTx
        case Tx.serialize state of
            Left e -> failWith (show e)
            Right bytes -> TIO.putStr $ T.decodeUtf8 $ convertToBase base bytes
  where
    setup :: IO ()
    setup = do
        -- Enable ANSI colors on Windows.
        void $ hSupportsANSIWithoutEmulation stderr

parseCmd :: IO Cmd
parseCmd = customExecParser (prefs showHelpOnEmpty) cmd

data Cmd
    = CmdEmpty Empty
    | CmdAddInput AddInput
    | CmdAddOutput AddOutput
    | CmdLock
    | CmdSignWith SignWith
    | CmdSerialize Serialize
    deriving (Show)

cmd :: ParserInfo Cmd
cmd = info (helper <*> cmds) $ progDesc "cardano-tx"
    <> headerDoc (Just $ vsep
        [ string "Construct and sign transactions according to the following state-machine:"
        , hardline
        , string "                           empty                    "
        , string "                             |                      "
        , string "      *------------------*   |   *-----------------*"
        , string "      |                  |   |   |                 |"
        , string "      |                  v   v   v                 |"
        , string "      *--- add-output ---=========--- add-input ---*"
        , string "                             |                      "
        , string "                             |                      "
        , string "                           lock   *----------------*"
        , string "                             |    |                |"
        , string "                             |    v                |"
        , string "                         =========--- sign-with ---*"
        , string "                             |                      "
        , string "                             |                      "
        , string "                         serialize                  "
        , string "                             |                      "
        , string "                             |                      "
        , string "                             v                      "
        , hardline
        , string "/!\\ Except 'serialize', every command outputs an intermediate \
            \state that is of little use and shouldn't be tempered with."
        , string "Redirect the output to a file, or use Unix pipes as shown below."
        ])
    <> footerDoc (Just $ vsep
        [ "Example:"
        , indent 2 $ string "cardano-tx empty 764824073 \\"
        , indent 2 $ string "  | cardano-tx add-input 0 \\"
        , indent 2 $ string "      3b40265111d8bb3c3c608d95b3a0bf83461ace32d79336579a1939b3aad1c0b7 \\"
        , indent 2 $ string "  | cardano-tx add-output 42 \\"
        , indent 2 $ string "      Ae2tdPwUPEZETXfbQxKMkMJQY1MoHCBS7bkw6TmhLjRvi9LZh1uDnXy319f \\"
        , indent 2 $ string "  | cardano-tx lock \\"
        , indent 2 $ string "  | cardano-tx sign-with \\"
        , indent 2 $ string "      e0860dab46f13e74ab834142e8877b80bf22044cae8ebab7a21ed1b8dc00c155 \\"
        , indent 2 $ string "      f6b78eee2a5bbd453ce7e7711b2964abb6a36837e475271f18ff36ae5fc8af73 \\"
        , indent 2 $ string "      e25db39fb78e74d4b53fb51776d0f5eb360e62d09b853f3a87ac25bf834ee1fb \\"
        , indent 2 $ string "  | cardano-tx serialize"
        ])
  where
    cmds = subparser $ mconcat
        [ cmdEmpty
        , cmdAddInput
        , cmdAddOutput
        , cmdLock
        , cmdSignWith
        , cmdSerialize
        ]

--                       _
--                      | |
--   ___ _ __ ___  _ __ | |_ _   _
--  / _ \ '_ ` _ \| '_ \| __| | | |
-- |  __/ | | | | | |_) | |_| |_| |
--  \___|_| |_| |_| .__/ \__|\__, |
--                | |         __/ |
--                |_|        |___/

newtype Empty = Empty
    { protocolMagic :: Init Byron
    } deriving (Show)

cmdEmpty :: Mod CommandFields Cmd
cmdEmpty = command "empty" $
    info (helper <*> fmap CmdEmpty subCmd) mempty
  where
    subCmd = Empty <$> protocolMagicArg

protocolMagicArg :: Parser (Init Byron)
protocolMagicArg = fmap mkInit $ argument auto $
    metavar "PROTOCOL MAGIC"

--            _     _        _                   _
--           | |   | |      (_)                 | |
--   __ _  __| | __| |______ _ _ __  _ __  _   _| |_
--  / _` |/ _` |/ _` |______| | '_ \| '_ \| | | | __|
-- | (_| | (_| | (_| |      | | | | | |_) | |_| | |_
--  \__,_|\__,_|\__,_|      |_|_| |_| .__/ \__,_|\__|
--                                  | |
--                                  |_|

data AddInput = AddInput
    { inputIx :: Word32
    , inputTxId :: ByteString
    } deriving (Show)

cmdAddInput :: Mod CommandFields Cmd
cmdAddInput = command "add-input" $
    info (helper <*> fmap CmdAddInput subCmd) $ mconcat
        [ progDesc "Add a new input to the transaction."
        , headerDoc (Just $ vsep
            [ string "An input is made of:"
            , indent 2 $ string "- A input index."
            , indent 2 $ string "- An transaction id, 32 bytes, base16-encoded."
            ])
        , footerDoc (Just $ vsep
            [ string "Example:"
            , indent 2 $ string "add-input 0 3b40265...aad1c0b7"
            , indent 2 $ string "            <--- 64 CHARS --->"
            ])
        ]
  where
    subCmd = AddInput <$> inputIxArg <*> inputTxIdArg

inputIxArg :: Parser Word32
inputIxArg = argument auto $
    metavar "INDEX"

inputTxIdArg :: Parser ByteString
inputTxIdArg = bytesArgument (Just 32) fromBase16 $
    metavar "TXID"

--            _     _                   _               _
--           | |   | |                 | |             | |
--   __ _  __| | __| |______ ___  _   _| |_ _ __  _   _| |_
--  / _` |/ _` |/ _` |______/ _ \| | | | __| '_ \| | | | __|
-- | (_| | (_| | (_| |     | (_) | |_| | |_| |_) | |_| | |_
--  \__,_|\__,_|\__,_|      \___/ \__,_|\__| .__/ \__,_|\__|
--                                         | |
--                                         |_|

data AddOutput = AddOutput
    { coin :: Natural
    , address :: ByteString
    } deriving (Show)

cmdAddOutput :: Mod CommandFields Cmd
cmdAddOutput = command "add-output" $
    info (helper <*> fmap CmdAddOutput subCmd) $ mconcat
        [ progDesc "Add a new output to the transaction."
        , headerDoc (Just $ vsep
            [ string "An output is made of:"
            , indent 2 $ string "- A coin value in Lovelace (1 Ada = 1e6 Lovelace)."
            , indent 2 $ string "- A target address, base58-encoded."
            ])
        , footerDoc (Just $ vsep
            [ string "Example:"
            , indent 2 $ string "add-output 1000000 2cWKMJemo...ZTBMqHTPTkv"
            ])
        ]
  where
    subCmd = AddOutput <$> coinArg <*> addressArg

coinArg :: Parser Natural
coinArg = argument auto $
    metavar "LOVELACE"

addressArg :: Parser ByteString
addressArg = bytesArgument Nothing fromBase58 $
    metavar "ADDRESS"

--  _            _
-- | |          | |
-- | | ___   ___| | __
-- | |/ _ \ / __| |/ /
-- | | (_) | (__|   <
-- |_|\___/ \___|_|\_\

cmdLock :: Mod CommandFields Cmd
cmdLock = command "lock" $
    info (helper <*> pure CmdLock) $ mconcat
        [ progDesc "Lock the transaction and start signing inputs."
        , header
            "Once locked, it is no longer possible to add inputs or outputs to \
            \a transaction. This is a necessary step to be able to sign the \
            \transaction with private keys corresponding to inputs."
        ]

--      _             _    _ _ _   _
--     (_)           | |  | (_) | | |
--  ___ _  __ _ _ __ | |  | |_| |_| |__
-- / __| |/ _` | '_ \| |/\| | | __| '_ \
-- \__ \ | (_| | | | \  /\  / | |_| | | |
-- |___/_|\__, |_| |_|\/  \/|_|\__|_| |_|
--         __/ |
--        |___/

newtype SignWith = SignWith
    { prvKey :: ByteString
    } deriving (Show)

cmdSignWith :: Mod CommandFields Cmd
cmdSignWith = command "sign-with" $
    info (helper <*> fmap CmdSignWith subCmd) $ mconcat
        [ progDesc "Add a signature."
        ]
  where
    subCmd = SignWith <$> prvKeyArg

prvKeyArg :: Parser ByteString
prvKeyArg = bytesArgument (Just 96) fromBase16 $
    metavar "XPRV"

--                _       _ _
--               (_)     | (_)
--  ___  ___ _ __ _  __ _| |_ _______
-- / __|/ _ \ '__| |/ _` | | |_  / _ \
-- \__ \  __/ |  | | (_| | | |/ /  __/
-- |___/\___|_|  |_|\__,_|_|_/___\___|
--

newtype Serialize = Serialize
    { base :: Base
    } deriving Show

cmdSerialize :: Mod CommandFields Cmd
cmdSerialize = command "serialize" $
    info (helper <*> fmap CmdSerialize subCmd) $ mconcat
        [ progDesc "Serialize the signed transaction to binary."
        ]
  where
    subCmd = serializeArg

serializeArg :: Parser Serialize
serializeArg = base16Flag <|> base64Flag
  where
    base16Flag = flag' (Serialize Base16) (long "base16")
    base64Flag = flag (Serialize Base64) (Serialize Base64) (long "base64")

--  _   _      _
-- | | | |    | |
-- | |_| | ___| |_ __   ___ _ __ ___
-- |  _  |/ _ \ | '_ \ / _ \ '__/ __|
-- | | | |  __/ | |_) |  __/ |  \__ \
-- \_| |_/\___|_| .__/ \___|_|  |___/
--              | |
--              |_|

-- | Parse a encoded 'Bytestring' argument.
bytesArgument
    :: Maybe Int -- ^ Number of bytes that are expected, if known.
    -> (Text -> Maybe ByteString) -- ^ A base conversion
    -> Mod ArgumentFields ByteString
    -> Parser ByteString
bytesArgument len fromBase =
    argument (maybeReader readerT)
  where
    readerT :: String -> Maybe ByteString
    readerT str = do
        bytes <- fromBase $ T.pack str
        guard (isNothing len || Just (BS.length bytes) == len)
        pure bytes

-- | Convert a 'ByteString' to 'Base64'
base64 :: ByteString -> Text
base64 = T.decodeUtf8 . convertToBase Base64

-- | Convert a /Base64/ 'Text' into a 'ByteString'
fromBase64 :: Text -> Maybe ByteString
fromBase64 = either (const Nothing) Just . convertFromBase Base64 . T.encodeUtf8

-- | Deserialize data from a /Base64/ CBOR 'ByteString', or fail.
hGetState :: Handle -> (forall s. CBOR.Decoder s a) -> IO a
hGetState h decoder = do
    bytes <- stripPEM . T.decodeUtf8 <$> BS.hGetContents h
    case fromBase64 bytes of
        Nothing -> failWith
            "Unable to decode intermediate buffer. Did you manually crafted one?"
        Just cbor -> case CBOR.deserialiseFromBytes decoder (BL.fromStrict cbor) of
            Left e -> failWith (show e)
            Right (_, a) -> pure a
  where
    stripPEM =
        T.replace "\n" "" . T.unlines . dropFromEnd 1 . drop 2 . T.lines
    dropFromEnd n =
        reverse . drop n . reverse

-- | Helper to output a given state to the console
hPutState :: Handle -> CBOR.Encoding -> IO ()
hPutState h =
    TIO.hPutStr h . encodePEM . base64 . CBOR.toStrictByteString
  where
    encodePEM :: Text -> Text
    encodePEM body = T.unlines
        [ "-----BEGIN CARDANO TX-----"
        , "version: 1.0.0"
        , T.intercalate "\n" (mkGroupsOf 64 body)
        , "-----END CARDANO TX-----"
        ]

    mkGroupsOf :: Int -> Text -> [Text]
    mkGroupsOf n xs
        | T.null xs = []
        | otherwise = (T.take n xs) : mkGroupsOf n (T.drop n xs)


-- | Fail with a colored red error message.
failWith :: String -> IO a
failWith msg = do
    withSGR stderr (SetColor Foreground Vivid Red) $ do
        TIO.hPutStrLn stderr (T.pack msg)
    exitFailure

-- | Bracket-style constructor for applying ANSI Select Graphic Rendition to an
-- action and revert back to normal after.
--
-- This does nothing if the device isn't an ANSI terminal.
withSGR :: Handle -> SGR -> IO a -> IO a
withSGR h sgr action = hIsTerminalDevice h >>= \case
    True  -> bracket aFirst aLast aBetween
    False -> action
  where
    aFirst = ([] <$ hSetSGR h [sgr])
    aLast = hSetSGR h
    aBetween = const action