{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}

-- | [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
-- Hierarchical Deterministic Wallets, for bitcoin and other cryptocurrencies.
module BIP32
  ( -- * Base58 serialization
    encodeXPrv
  , encodeXPub
  , decodeXPrv
  , decodeXPub
  , decode

    -- * Raw serialization
  , encodeXPrvRaw
  , encodeXPubRaw
  , decodeXPrvRaw
  , decodeXPubRaw
  , decodeRaw

    -- * Private key
  , XPrv(..)
  , xprvToXPub
  , I.Prv
  , I.prv
  , I.unPrv
  , I.prvToPub

    -- * Public key
  , XPub(..)
  , I.Pub
  , I.pub
  , I.unPub

    -- * Chain code
  , Chain
  , chain
  , unChain

    -- * Derivation path
  , Index(..)
  , indexIsHardened

    -- * Subkeys
  , subXPubXPub
  , subXPrvXPrv
  , subXPrvXPub
    -- ** Bare
  , subPubPub
  , subPrvPrv
  , subPrvPub

    -- * Depth
  , Depth(..)

    -- * Fingerprint
  , Fingerprint(..)
  , fingerprint

    -- * Version
  , Version(..)
    -- ** Example versions
  , version_xprv
  , version_xpub
  , version_tprv
  , version_tpub
  , version_Ltpv
  , version_Ltub
  , version_ttpv
  , version_ttub
  ) where

import Control.Applicative
import Control.Monad
import Data.Bits
import qualified Data.Binary.Get as Bin
import qualified Data.ByteString as B
import qualified Data.ByteString.Base58 as B58
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import Data.Word


#ifdef ghcjs_HOST_OS
import qualified BIP32.GHCJS as I
#else
import qualified BIP32.GHC as I
#endif

--------------------------------------------------------------------------------

-- | Extended private key.
data XPrv = XPrv !Version !Depth !Fingerprint !Index !Chain !I.Prv
  deriving (Eq, Show)

-- | Obtain the 'XPub' corresponding to a particular 'XPrv', at a particular
-- 'Version'.
xprvToXPub :: Version -> XPrv -> XPub
xprvToXPub v (XPrv _ d f i c prv) = XPub v d f i c (I.prvToPub prv)

--------------------------------------------------------------------------------

-- | Extended private key.
data XPub = XPub !Version !Depth !Fingerprint !Index !Chain !I.Pub
  deriving (Eq, Show)

--------------------------------------------------------------------------------

-- | The 33-byte serialized contents of either 'I.Pub' or 'I.Prv'.
newtype Key = Key B.ByteString

keyPub :: I.Pub -> Key
{-# INLINE keyPub #-}
keyPub = Key . I.unPub

keyPrv :: I.Prv -> Key
{-# INLINE keyPrv #-}
keyPrv = Key . B.cons 0 . I.unPrv

--------------------------------------------------------------------------------

-- | Version bytes.
newtype Version = Version { unVersion :: Word32 }
  deriving (Eq, Show)

-- | @0x0488ade4@, “xprv”, Bitcoin mainnet private.
version_xprv :: Version
version_xprv = Version 0x0488ade4
{-# INLINE version_xprv #-}

-- | @0x0488b21e@, “xpub”, Bitcoin mainnet public.
version_xpub :: Version
version_xpub = Version 0x0488b21e
{-# INLINE version_xpub #-}

-- | @0x04358394@, “tprv”, Bitcoin testnet private.
version_tprv :: Version
version_tprv = Version 0x04358394
{-# INLINE version_tprv #-}

-- | @0x043587cf@, “tpub”, Bitcoin testnet public.
version_tpub :: Version
version_tpub = Version 0x043587cf
{-# INLINE version_tpub #-}

-- | @0x019d9cfe@, “Ltpv”, Litecoin mainnet private.
version_Ltpv :: Version
version_Ltpv = Version 0x019d9cfe
{-# INLINE version_Ltpv #-}

-- | @0x019da462@, “Ltub”, Litecoin mainnet public.
version_Ltub :: Version
version_Ltub = Version 0x019da462
{-# INLINE version_Ltub #-}

-- | @0x0436ef7d@, “ttpv”, Litecoin testnet private.
version_ttpv :: Version
version_ttpv = Version 0x0436ef7d
{-# INLINE version_ttpv #-}

-- | @0x0436f6e1@, “ttub”, Litecoin testnet public.
version_ttub :: Version
version_ttub = Version 0x0436f6e1
{-# INLINE version_ttub #-}

--------------------------------------------------------------------------------

-- | Derivation path depth.
--
-- * @0@ for master nodes — @m@
--
-- * @1@ for level-1 derived keys — @m\/0'@, or @m\/13@, or …
--
-- * @2@ for level-2 derived keys — @m\/0'\/28@, or @m\/44'/0'@, or …
--
-- * … up to @255@.
newtype Depth = Depth { unDepth :: Word8 }
  deriving (Eq, Show)

depthNext :: Depth -> Maybe Depth
{-# INLINE depthNext #-}
depthNext (Depth w) = do
  guard (w /= 0xFF)
  pure (Depth (w + 1))

--------------------------------------------------------------------------------

-- | 4-byte fingerprint of a 'I.Pub' key.
newtype Fingerprint = Fingerprint { unFingerprint :: Word32 }
  deriving (Eq, Show)

fingerprint :: I.Pub -> Fingerprint
{-# INLINE fingerprint #-}
fingerprint = fingerprint' . keyId

fingerprint' :: KeyId -> Fingerprint
{-# INLINE fingerprint' #-}
fingerprint' (KeyId x) =
  let b = B.take 4 x :: B.ByteString
  in Fingerprint $! unsafeShiftL (fromIntegral (B.index b 0)) 24 .|.
                    unsafeShiftL (fromIntegral (B.index b 1)) 16 .|.
                    unsafeShiftL (fromIntegral (B.index b 2))  8 .|.
                                  fromIntegral (B.index b 3)

--------------------------------------------------------------------------------

-- | 20-byte key identifier.
newtype KeyId = KeyId B.ByteString

keyId :: I.Pub -> KeyId
{-# INLINE keyId #-}
keyId = KeyId . I.ripemd160 . I.sha256 . I.unPub

--------------------------------------------------------------------------------

-- | Chain code.
--
-- Construct with 'chain'.
newtype Chain = Chain B.ByteString
  deriving (Eq)

instance Show Chain where
  showsPrec n (Chain b) = showParen (n > 10) $
    showString "Chain " .
    showsPrec 0 (BB.toLazyByteString (BB.byteStringHex b))

-- | Construct a 'Chain' code.
--
-- See Bitcoin's [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
-- for details.
--
-- 'Nothing' if the 'B.ByteString' length is not 32.
chain :: B.ByteString -> Maybe Chain
{-# INLINE chain #-}
chain x = do
  guard (B.length x == 32)
  Just (Chain x)

-- | Obtain the 32 raw bytes inside a 'Chain'.
unChain :: Chain -> B.ByteString
{-# INLINE unChain #-}
unChain (Chain x) = x

--------------------------------------------------------------------------------

-- | A derivation path 'Index'.
newtype Index = Index Word32
  deriving (Eq, Ord, Show)

-- | Whether a derivation path 'Index' is hardened. That is, \(2^{31}\) or
-- larger.
indexIsHardened :: Index -> Bool
{-# INLINE indexIsHardened #-}
indexIsHardened (Index w) = w >= 0x80000000

--------------------------------------------------------------------------------

-- | Derive a child 'XPrv' subkey from a parent a parent 'XPrv' key.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subXPrvXPrv :: XPrv -> Index -> Maybe XPrv
subXPrvXPrv (XPrv v d0 _ _ ch0 prv0) i1 = do
  d1 <- depthNext d0
  (ch1, prv1) <- subPrvPrv ch0 prv0 i1
  let f1 = fingerprint (I.prvToPub prv0)
  pure $ XPrv v d1 f1 i1 ch1 prv1

-- | Derive a child 'XPub' subkey from a parent a parent 'XPub' key.
--
-- The given 'Index' is expected to /not/ be hardened.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subXPubXPub :: XPub -> Index -> Maybe XPub
subXPubXPub (XPub v d0 _ _ ch0 pub0) i1 = do
  d1 <- depthNext d0
  (ch1, pub1) <- subPubPub ch0 pub0 i1
  let f1 = fingerprint pub0
  pure $ XPub v d1 f1 i1 ch1 pub1

-- | Derive a child 'XPub' subkey from a parent a parent 'XPrv' key.
--
-- Notice that while @'subXPubXPub' ('xprvToXPub' v) xprv i@ will fail with a
-- hardened 'Index', @'subXPrvXPub' v xprv i@ may succeed.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subXPrvXPub :: Version -> XPrv -> Index -> Maybe XPub
subXPrvXPub v xprv i1 = xprvToXPub v <$> subXPrvXPrv xprv i1

--------------------------------------------------------------------------------

-- | Derive a child 'I.Prv' subkey and 'Chain' code at a particular 'Index' from
-- a parent 'I.Prv' and 'Chain' code.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subPrvPrv :: Chain -> I.Prv -> Index -> Maybe (Chain, I.Prv)
subPrvPrv (Chain ch0) prv0 i1@(Index i1w) = do
  let pub0 = I.prvToPub prv0
      x = I.hmacSHA512 ch0 $ mconcat $ if indexIsHardened i1
            then [B.singleton 0, I.unPrv prv0, word32BE i1w]
            else [I.unPub pub0, word32BE i1w]
      (xl, xr) = B.splitAt 32 x
  prv1 <- I.addPrvTweak prv0 =<< I.tweak xl
  ch1 <- chain xr
  pure (ch1, prv1)

-- | Derive a child 'I.Pub' subkey and 'Chain' code at a particular 'Index' from
-- a parent 'I.Pub' and 'Chain' code.
--
-- The given 'Index' is expected to /not/ be hardened.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subPubPub :: Chain -> I.Pub -> Index -> Maybe (Chain, I.Pub)
subPubPub (Chain ch0) pub0 i1@(Index i1w) = do
  guard (not (indexIsHardened i1))
  let x = I.hmacSHA512 ch0 (I.unPub pub0 <> word32BE i1w)
      (xl, xr) = B.splitAt 32 x
  pub1 <- I.addPubTweak pub0 =<< I.tweak xl
  ch1 <- chain xr
  pure (ch1, pub1)

-- | Derive a child 'I.Pub' subkey and 'Chain' code at a particular 'Index' from
-- a parent 'I.Prv' and 'Chain' code.
--
-- Notice that while @'subPubPub' ('I.prvToPub' prv) i@ will fail with a
-- hardened 'Index', @'subPrvPub' prv i@ may succeed.
--
-- Returns 'Nothing' if the given inputs result in an invalid key.
subPrvPub :: Chain -> I.Prv -> Index -> Maybe (Chain, I.Pub)
subPrvPub ch0 prv0 i1 = fmap I.prvToPub <$> subPrvPrv ch0 prv0 i1

--------------------------------------------------------------------------------

-- | Obtain the Base58 representation for an 'XPub'.
--
-- It can be either 111 or 112 bytes in length, and when rendered as ASCII it
-- looks something like this:
--
-- @
-- xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8
-- @
encodeXPub :: XPub -> B.ByteString
{-# INLINE encodeXPub #-}
encodeXPub a = base58EncodeWithChecksum (encodeXPubRaw a)

-- | Obtain the Base58 representation for an 'XPrv'.
--
-- It can be either 111 or 112 bytes in length, and when rendered as ASCII it
-- looks something like this:
--
-- @
-- xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi
-- @
encodeXPrv :: XPrv -> B.ByteString
{-# INLINE encodeXPrv #-}
encodeXPrv a = base58EncodeWithChecksum (encodeXPrvRaw a)

base58EncodeWithChecksum :: B.ByteString -> B.ByteString
{-# INLINE base58EncodeWithChecksum #-}
base58EncodeWithChecksum a =
  B58.encodeBase58 B58.bitcoinAlphabet (a <> checksum32 a)

--------------------------------------------------------------------------------

-- | Encode the 78 raw 'XPub' bytes.
encodeXPubRaw :: XPub -> B.ByteString
{-# INLINE encodeXPubRaw #-}
encodeXPubRaw (XPub v d f i c p) = encodeRaw v d f i c (keyPub p)

-- | Encode the 78 raw 'XPrv' bytes.
encodeXPrvRaw :: XPrv -> B.ByteString
{-# INLINE encodeXPrvRaw #-}
encodeXPrvRaw (XPrv v d f i c p) = encodeRaw v d f i c (keyPrv p)

-- | Encode the 78 raw extended key bytes.
encodeRaw
  :: Version -> Depth -> Fingerprint -> Index -> Chain -> Key -> B.ByteString
encodeRaw (Version v) (Depth d) (Fingerprint f) (Index i) (Chain c) (Key k) =
  BL.toStrict $ BB.toLazyByteString $
    BB.word32BE v <>
    BB.word8 d <>
    BB.word32BE f <>
    BB.word32BE i <>
    BB.byteString c <>
    BB.byteString k

--------------------------------------------------------------------------------

-- | Decode the Base58-encoded 'XPub' representation. See 'encodeXPub'.
decodeXPub :: B.ByteString -> Maybe XPub
{-# INLINE decodeXPub #-}
decodeXPub = decodeXPubRaw <=< base58DecodeWithChecksum

-- | Decode the Base58-encoded 'XPrv' representation. See 'encodeXPrv'.
decodeXPrv :: B.ByteString -> Maybe XPrv
{-# INLINE decodeXPrv #-}
decodeXPrv = decodeXPrvRaw <=< base58DecodeWithChecksum

-- | Decode the Base58-encoded representation of either and 'XPub' or an 'XPub'.
decode :: B.ByteString -> Maybe (Either XPub XPrv)
{-# INLINE decode #-}
decode = decodeRaw <=< base58DecodeWithChecksum

base58DecodeWithChecksum :: B.ByteString -> Maybe B.ByteString
{-# INLINE base58DecodeWithChecksum #-}
base58DecodeWithChecksum = \a -> do
  b <- B58.decodeBase58 B58.bitcoinAlphabet a
  guard (B.length b == 78 + 4)
  let (raw, ch) = B.splitAt 78 b
  guard (ch == checksum32 raw)
  pure raw

--------------------------------------------------------------------------------

-- | Decode the 78 raw 'XPub' bytes.
decodeXPubRaw :: B.ByteString -> Maybe XPub
{-# INLINE decodeXPubRaw #-}
decodeXPubRaw = either Just (\_ -> Nothing) <=< decodeRaw

-- | Decode the 78 raw 'XPrv' bytes.
decodeXPrvRaw :: B.ByteString -> Maybe XPrv
{-# INLINE decodeXPrvRaw #-}
decodeXPrvRaw = either (\_ -> Nothing) Just <=< decodeRaw

-- | Encode the 78 raw 'XPub' or 'XPrv' bytes.
decodeRaw :: B.ByteString -> Maybe (Either XPub XPrv)
decodeRaw = \b -> do
  guard (B.length b == 78)
  case Bin.runGetOrFail getX (BL.fromStrict b) of
    Right (lo, 78, ex) | BL.null lo -> Just ex
    _ -> Nothing

getX :: Bin.Get (Either XPub XPrv)
getX = do
  v <- Version <$> Bin.getWord32be
  d <- Depth <$> Bin.getWord8
  f <- Fingerprint <$> Bin.getWord32be
  i <- Index <$> Bin.getWord32be
  c <- Chain <$> Bin.getByteString 32
  ek <- fmap Left getPub <|> fmap Right getPrv
  pure $ case ek of
    Left  k -> Left  (XPub v d f i c k)
    Right k -> Right (XPrv v d f i c k)

getPrv :: Bin.Get I.Prv
getPrv = do
  0 <- Bin.getWord8
  a <- Bin.getByteString 32
  case I.prv a of
    Just b -> pure b
    Nothing -> fail "Bad private key"

getPub :: Bin.Get I.Pub
getPub = do
  a <- Bin.getByteString 33
  case I.pub a of
    Just b -> pure b
    Nothing -> fail "Bad public key"

--------------------------------------------------------------------------------

word32BE :: Word32 -> B.ByteString -- ^ 4 bytes.
{-# INLINE word32BE #-}
word32BE = BL.toStrict . BB.toLazyByteString . BB.word32BE

checksum32 :: B.ByteString -> B.ByteString -- ^ 4 bytes.
{-# INLINE checksum32 #-}
checksum32 = B.take 4 . I.sha256 . I.sha256