bech32-1.1.2: app/Main.hs
{-# LANGUAGE NamedFieldPuns #-}
import Prelude
import Codec.Binary.Bech32
( HumanReadablePart
, dataPartFromBytes
, dataPartToBytes
, humanReadablePartFromText
)
import Control.Arrow
( left, right )
import Control.Monad
( guard, void )
import Data.ByteArray.Encoding
( convertFromBase, convertToBase )
import Data.ByteString.Base58
( bitcoinAlphabet, decodeBase58, unAlphabet )
import Data.Char
( isHexDigit, isLetter, isLower, isUpper, toLower )
import Data.Either.Extra
( maybeToEither )
import Data.Maybe
( fromJust )
import Data.Version
( showVersion )
import Options.Applicative
( Parser
, ParserInfo
, argument
, customExecParser
, eitherReader
, flag
, footerDoc
, help
, helpDoc
, helper
, hidden
, info
, long
, metavar
, optional
, prefs
, progDesc
, short
, showHelpOnEmpty
, (<|>)
)
import Options.Applicative.Help.Pretty
( bold, hsep, indent, text, underline, vsep )
import Paths_bech32
( version )
import System.IO
( BufferMode (..), Handle, hSetBuffering, stderr, stdin, stdout )
import qualified Codec.Binary.Bech32 as Bech32
import qualified Codec.Binary.Bech32.Internal as Bech32
import qualified Data.ByteArray.Encoding as BA
import qualified Data.ByteString.Char8 as B8
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
main :: IO ()
main = setup >> parse >>= run
data Cmd
= RunCmd
{ prefix :: Maybe HumanReadablePart
}
| VersionCmd
deriving (Show)
-- | Enable ANSI colors on Windows and correct output buffering
setup :: IO ()
setup =
mapM_ hSetup [stderr, stdout]
where
hSetup :: Handle -> IO ()
hSetup h = hSetBuffering h NoBuffering
-- | Parse command line options and arguments
parse :: IO Cmd
parse = customExecParser (prefs showHelpOnEmpty) parser
where
parser :: ParserInfo Cmd
parser = info (helper <*> cmd) $ mconcat
[ progDesc $ unwords
[ "Convert to and from bech32 strings."
, "Data are read from standard input."
]
, footerDoc $ Just $ vsep
[ hsep
[ text "Supported encoding formats:"
, indent 0 $ text "Base16, Bech32 & Base58."
]
, text ""
, text "Examples:"
, indent 2 $ hsep [underline $ text "To", text "Bech32:"]
, indent 4 $ bold $ text "$ bech32 base16_ <<< 706174617465"
, indent 4 $ text "base16_1wpshgct5v5r5mxh0"
, text ""
, indent 4 $ bold $ text "$ bech32 base58_ <<< Ae2tdPwUPEYy"
, indent 4 $ text "base58_1p58rejhd9592uusa8pzj2"
, text ""
, indent 4 $ bold $ text "$ bech32 new_prefix <<< old_prefix1wpshgcg2s33x3"
, indent 4 $ text "new_prefix1wpshgcgeak9mv"
, text ""
, indent 2 $ hsep [underline $ text "From", text "Bech32:"]
, indent 4 $ bold $ text "$ bech32 <<< base16_1wpshgct5v5r5mxh0"
, indent 4 $ text "706174617465"
]
]
cmd :: Parser Cmd
cmd = (RunCmd <$> optional hrpArgument) <|> (VersionCmd <$ versionFlag)
versionFlag :: Parser ()
versionFlag = void . flag False True $ mconcat
[ long "version"
, short 'v'
, help "output version information and exit"
, hidden
]
-- | Parse a 'HumanReadablePart' as an argument.
hrpArgument :: Parser HumanReadablePart
hrpArgument = argument (eitherReader reader) $ mconcat
[ metavar "PREFIX"
, helpDoc $ Just $ vsep
[ text "An optional human-readable prefix (e.g. 'addr')."
, indent 2 $ text
"- When provided, the input text is decoded from various encoding \
\formats and re-encoded to bech32 using the given prefix."
, indent 2 $ text
"- When omitted, the input text is decoded from bech32 to base16."
]
]
where
reader :: String -> Either String HumanReadablePart
reader = left show . humanReadablePartFromText . T.pack
-- | Run a Command in IO
run :: Cmd -> IO ()
run cmd = case cmd of
VersionCmd -> putStrLn $ showVersion version
RunCmd {prefix} -> do
source <- T.strip . T.decodeUtf8 <$> B8.hGetContents stdin
case prefix of
Nothing -> runDecode source
Just hrp -> runEncode hrp source
where
runDecode source =
case Bech32.decodeLenient source of
Left err ->
fail (show err)
Right (_, dataPart) -> do
let base16 = convertToBase BA.Base16
B8.putStrLn $ base16 $ fromJust $ dataPartToBytes dataPart
runEncode hrp source = do
datapart <- either fail pure $
case detectEncoding (T.unpack source) of
Just Base16 -> do
let fromBase16 = convertFromBase BA.Base16 . T.encodeUtf8
dataPartFromBytes <$> fromBase16 source
Just Bech32 ->
right snd $ left show $ Bech32.decodeLenient source
Just Base58 -> do
let err = "Invalid Base58-encoded string."
let fromBase58 = decodeBase58 bitcoinAlphabet . T.encodeUtf8
dataPartFromBytes <$> maybeToEither err (fromBase58 source)
Nothing ->
Left "Unable to detect input encoding. Neither Base16, \
\Bech32 nor Base58."
B8.putStrLn $ T.encodeUtf8 $ Bech32.encodeLenient hrp datapart
data Encoding = Base16 | Bech32 | Base58 deriving (Show, Eq)
-- | Try detecting the encoding of a given 'String'
detectEncoding :: String -> Maybe Encoding
detectEncoding str
| length str < minimalSizeForDetection = Nothing
| otherwise = resembleBase16 <|> resembleBech32 <|> resembleBase58
where
resembleBase16 = do
guard (all isHexDigit (toLower <$> str))
guard (even (length str))
pure Base16
resembleBech32 = do
guard (not (null humanpart))
guard (all Bech32.humanReadableCharIsValid humanpart)
guard (length datapart >= Bech32.checksumLength)
guard (all (`elem` Bech32.dataCharList) datapart)
guard (all isUpper alpha || all isLower alpha)
guard (Bech32.separatorChar `elem` str)
pure Bech32
where
datapart = reverse . takeWhile (/= Bech32.separatorChar) . reverse $ str
humanpart = takeWhile (/= Bech32.separatorChar) str
alpha = filter isLetter str
resembleBase58 = do
guard (all isBase58Digit str)
pure Base58
where
isBase58Digit :: Char -> Bool
isBase58Digit =
(`elem` T.unpack (T.decodeUtf8 $ unAlphabet bitcoinAlphabet))
-- NOTE For small string, it can be tricky to tell whether a string is hex
-- or bech32 encoded. Both could potentially be valid. As the length
-- increases, the probability for a string to satisfy all three encoding
-- rules gets smaller and smaller.
--
-- For example, let's consider the probability for the alphabet to match
-- between base16 and base58 (which will be bigger than the actual probability
-- of both encoding to be valid, since there are additional rules on top of
-- the alphabet):
--
-- P_1 = 16/58
--
-- Now, the probability that a base58 string of 8 characters will contain
-- only hexadecimal characters is
--
-- P_8 = P_1 ^ 8 ~ 0.00003
--
-- Which can be considered small enough to not happened too frequently. The
-- probability gets worse with Bech32 which has quite a lot of rules.
minimalSizeForDetection :: Int
minimalSizeForDetection = 8