content: {-# LANGUAGE ImportQualifiedPost #-}
  {-# LANGUAGE OverloadedStrings #-}
  {-# LANGUAGE NoFieldSelectors #-}
  
  -- |
  -- Module      : Haskoin.Address.Base58
  -- Copyright   : No rights reserved
  -- License     : MIT
  -- Maintainer  : jprupp@protonmail.ch
  -- Stability   : experimental
  -- Portability : POSIX
  --
  -- Support for legacy 'Base58' addresses. Superseded by Bech32 for Bitcoin SegWit
  -- (BTC) and CashAddr for Bitcoin Cash (BCH).
  module Haskoin.Address.Base58
    ( -- * Base58
      Base58,
      encodeBase58,
      decodeBase58,
      encodeBase58Check,
      decodeBase58Check,
    )
  where
  
  import Control.Monad (guard)
  import Data.Array (Array, assocs, listArray, (!), (//))
  import Data.ByteString (ByteString)
  import Data.ByteString qualified as B
  import Data.ByteString.Char8 qualified as C
  import Data.Bytes.Get ()
  import Data.Bytes.Put (runPutS)
  import Data.Bytes.Serial (Serial (serialize))
  import Data.Char (chr, ord)
  import Data.Maybe (fromMaybe, isJust, listToMaybe)
  import Data.String.Conversions (cs)
  import Data.Text (Text)
  import Data.Text qualified as T
  import Data.Word (Word8)
  import Haskoin.Crypto.Hash (checkSum32)
  import Haskoin.Util.Helpers (bsToInteger, integerToBS)
  import Numeric (readInt, showIntAtBase)
  
  -- | 'Base58' classic Bitcoin address format.
  type Base58 = Text
  
  -- | Symbols for Base58 encoding.
  b58Data :: ByteString
  b58Data = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
  
  b58Array :: Array Int Word8
  b58Array = listArray (0, 57) (B.unpack b58Data)
  
  b58InvArray :: Array Word8 (Maybe Int)
  b58InvArray =
    listArray
      (minBound, maxBound)
      (repeat Nothing)
      // map swap (assocs b58Array)
    where
      swap (i, c) = (c, Just i)
  
  -- | Convert a number less than or equal to provided integer into a 'Base58'
  -- character.
  b58 :: Int -> Word8
  b58 = (b58Array !)
  
  -- | Convert a 'Base58' character into the number it represents.
  b58' :: Word8 -> Maybe Int
  b58' = (b58InvArray !)
  
  -- | Encode an arbitrary-length 'Integer' into a 'Base58' string. Leading zeroes
  -- will not be part of the resulting string.
  encodeBase58I :: Integer -> Base58
  encodeBase58I i = cs $ showIntAtBase 58 (chr . fromIntegral . b58) i ""
  
  -- | Decode a 'Base58' string into an arbitrary-length 'Integer'.
  decodeBase58I :: Base58 -> Maybe Integer
  decodeBase58I s =
    case go of
      Just (r, []) -> Just r
      _ -> Nothing
    where
      p = isJust . b58' . fromIntegral . ord
      f = fromMaybe e . b58' . fromIntegral . ord
      go = listToMaybe $ readInt 58 p f (cs s)
      e = error "Could not decode base58"
  
  -- | Encode an arbitrary 'ByteString' into a its 'Base58' representation,
  -- preserving leading zeroes.
  encodeBase58 :: ByteString -> Base58
  encodeBase58 bs =
    l <> r
    where
      (z, b) = B.span (== 0) bs
      l = cs $ B.replicate (B.length z) (b58 0) -- preserve leading 0's
      r
        | B.null b = T.empty
        | otherwise = encodeBase58I $ bsToInteger b
  
  -- | Decode a 'Base58'-encoded 'Text' to a 'ByteString'.
  decodeBase58 :: Base58 -> Maybe ByteString
  decodeBase58 t =
    B.append prefix <$> r
    where
      (z, b) = B.span (== b58 0) (cs t)
      prefix = B.replicate (B.length z) 0 -- preserve leading 1's
      r
        | B.null b = Just B.empty
        | otherwise = integerToBS <$> decodeBase58I (cs b)
  
  -- | Computes a checksum for the input 'ByteString' and encodes the input and
  -- the checksum as 'Base58'.
  encodeBase58Check :: ByteString -> Base58
  encodeBase58Check bs =
    (encodeBase58 . B.append bs . runPutS . serialize . checkSum32) bs
  
  -- | Decode a 'Base58'-encoded string that contains a checksum. This function
  -- returns 'Nothing' if the input string contains invalid 'Base58' characters or
  -- if the checksum fails.
  decodeBase58Check :: Base58 -> Maybe ByteString
  decodeBase58Check bs = do
    rs <- decodeBase58 bs
    let (res, chk) = B.splitAt (B.length rs - 4) rs
    guard $ chk == (runPutS . serialize . checkSum32) res
    return res
  