cardano-addresses-4.0.0: lib/Options/Applicative/Derivation.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_HADDOCK hide #-}
-- |
-- Copyright: 2020 Input Output (Hong Kong) Ltd., 2021-2022 Input Output Global Inc. (IOG), 2023-2025 Intersect
-- License: Apache-2.0
module Options.Applicative.Derivation
(
-- * Derivation Path
-- ** Type
DerivationPath
, castDerivationPath
, derivationPathToString
, derivationPathFromString
-- ** Applicative Parser
, derivationPathArg
-- * Derivation Index
, DerivationIndex
, mkDerivationIndex
, firstHardened
, indexToInteger
, derivationIndexToString
, derivationIndexFromString
-- * XPub / Pub / XPrv / KeyHash
, xpubReader
, xpubOpt
, xpubArg
, keyhashReader
, pubReader
-- * Internal
, bech32Reader
) where
import Prelude
import Cardano.Address.Derivation
( DerivationType (..)
, Index
, Pub
, XPub
, pubFromBytes
, wholeDomainIndex
, xpubFromBytes
)
import Cardano.Address.KeyHash
( KeyHash (..), KeyRole (..), keyHashFromBytes )
import Codec.Binary.Bech32
( HumanReadablePart, humanReadablePartToText )
import Codec.Binary.Encoding
( fromBech32 )
import Control.Arrow
( left )
import Control.Monad
( unless )
import Data.ByteString
( ByteString )
import Data.List
( intercalate, isSuffixOf )
import Data.Word
( Word32 )
import Options.Applicative
( Parser
, argument
, completer
, eitherReader
, help
, listCompleter
, long
, metavar
, option
)
import Safe
( readEitherSafe )
import System.IO.Extra
( markCharsRedAtIndices )
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
--
-- Derivation Path
--
-- | Represent a user-provided 'DerivationPath'.
newtype DerivationPath = DerivationPath [DerivationIndex]
deriving (Show, Eq)
derivationPathFromString :: String -> Either String DerivationPath
derivationPathFromString str =
DerivationPath
<$> mapM (derivationIndexFromString . T.unpack) (T.splitOn "/" txt)
where
txt = T.pack str
derivationPathToString :: DerivationPath -> String
derivationPathToString (DerivationPath xs) =
intercalate "/" $ map derivationIndexToString xs
castDerivationPath :: DerivationPath -> [Index 'WholeDomain depth]
castDerivationPath (DerivationPath xs) = map (wholeDomainIndex . getDerivationIndex) xs
derivationPathArg :: Parser DerivationPath
derivationPathArg = argument (eitherReader derivationPathFromString) $ mempty
<> metavar "DERIVATION-PATH"
<> help
"Slash-separated derivation path. Hardened indexes are marked with a \
\'H' (e.g. 1852H/1815H/0H/0)."
<> completer (listCompleter ["1852H/1815H/0H/", "44H/1815H/0H/"])
--
-- Derivation Index
--
newtype DerivationIndex = DerivationIndex { getDerivationIndex :: Word32 }
deriving stock (Show, Eq)
deriving newtype (Bounded, Ord)
-- | Safely cast a 'DerivationIndex' to an 'Integer'.
indexToInteger :: DerivationIndex -> Integer
indexToInteger (DerivationIndex ix) = fromIntegral ix
-- | Get the first 'DerivationIndex' considered /hardened/.
firstHardened :: DerivationIndex
firstHardened = DerivationIndex 0x80000000
-- | Smart-constructor for a 'DerivationIndex'
mkDerivationIndex :: Integer -> Either String DerivationIndex
mkDerivationIndex ix
| ix > fromIntegral (maxBound @Word32) =
Left $ show ix <> " is too high to be a derivation index."
| otherwise =
pure $ DerivationIndex $ fromIntegral ix
-- | Convert a string to a derivation index. String must be followed by a
-- capital /H/ to mark hardened index. For example @0@ refers to the first soft
-- index, whereas @0H@ refers to the first hardened index.
derivationIndexFromString :: String -> Either String DerivationIndex
derivationIndexFromString "" = Left "An empty string is not a derivation index!"
derivationIndexFromString str
| "H" `isSuffixOf` str = do
parseHardenedIndex (init str)
| otherwise = do
parseSoftIndex str
where
parseHardenedIndex txt = do
ix <- left (const msg) $ readEitherSafe txt
mkDerivationIndex $ ix + indexToInteger firstHardened
where
msg = mconcat
[ "Unable to parse hardened index. Hardened indexes are integer "
, "values, between "
, show (indexToInteger (minBound @DerivationIndex))
, " and "
, show (indexToInteger firstHardened)
, " ending with a capital 'H'. For example: \"42H\","
]
parseSoftIndex txt = do
ix <- left (const msg) $ readEitherSafe txt
guardSoftIndex ix
mkDerivationIndex ix
where
guardSoftIndex ix
| ix >= indexToInteger firstHardened =
Left $ mconcat
[ show ix
, " is too high to be a soft derivation index. "
, "Did you mean \""
, show (ix - indexToInteger firstHardened)
, "H\"?"
]
| otherwise =
pure ()
msg = mconcat
[ "Unable to parse soft index. Soft indexes are integer "
, "values, between "
, show (indexToInteger (minBound @DerivationIndex))
, " and "
, show (indexToInteger firstHardened)
, ". For example: \"14\"."
]
-- | Convert a 'DerivationIndex' back to string.
derivationIndexToString :: DerivationIndex -> String
derivationIndexToString ix_@(DerivationIndex ix)
| ix_ >= firstHardened = show ix' ++ "H"
| otherwise = show ix
where
ix' = fromIntegral ix - indexToInteger firstHardened
--
-- XPub / Pub / XPrv
--
xpubReader :: [HumanReadablePart] -> String -> Either String XPub
xpubReader allowedPrefixes str = do
(_hrp, bytes) <- bech32Reader allowedPrefixes str
case xpubFromBytes bytes of
Just xpub -> pure xpub
Nothing -> Left
"Failed to convert bytes into a valid extended public key."
pubReader :: [HumanReadablePart] -> String -> Either String Pub
pubReader allowedPrefixes str = do
(_hrp, bytes) <- bech32Reader allowedPrefixes str
case pubFromBytes bytes of
Just pub -> pure pub
Nothing -> Left
"Failed to convert bytes into a valid non-extended public key."
xpubOpt :: [HumanReadablePart] -> String -> String -> Parser XPub
xpubOpt allowedPrefixes name helpDoc =
option (eitherReader (xpubReader allowedPrefixes)) $ mempty
<> long name
<> metavar "XPUB"
<> help helpDoc
xpubArg :: [HumanReadablePart] -> String -> Parser XPub
xpubArg allowedPrefixes helpDoc =
argument (eitherReader (xpubReader allowedPrefixes)) $ mempty
<> metavar "XPUB"
<> help helpDoc
keyhashReader :: (KeyRole, [HumanReadablePart]) -> String -> Either String KeyHash
keyhashReader (keyrole, allowedPrefixes) str = do
(_hrp, bytes) <- bech32Reader allowedPrefixes str
case keyHashFromBytes (keyrole, bytes) of
Just keyhash -> pure keyhash
Nothing -> Left
"Failed to convert bytes into a valid public key hash."
--
-- Internal
--
bech32Reader
:: [HumanReadablePart]
-> String
-> Either String (HumanReadablePart, ByteString)
bech32Reader allowedPrefixes str = do
(hrp, bytes) <- fromBech32 markCharsRedAtIndices (toBytes str)
unless (hrp `elem` allowedPrefixes) $ Left
$ "Invalid human-readable prefix. Prefix ought to be one of: "
<> show (showHrp <$> allowedPrefixes)
pure (hrp, bytes)
where
showHrp :: HumanReadablePart -> String
showHrp = T.unpack . humanReadablePartToText
toBytes :: String -> ByteString
toBytes = T.encodeUtf8 . T.pack