web3-polkadot-1.0.0.0: src/Network/Polkadot/Account.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
-- |
-- Module : Network.Polkadot.Account
-- Copyright : Aleksandr Krupenkin 2016-2021
-- License : Apache-2.0
--
-- Maintainer : mail@akru.me
-- Stability : experimental
-- Portability : portable
--
-- Polkadot account types.
--
module Network.Polkadot.Account (Ss58Codec(..), IdentifyAccount(..)) where
import Codec.Scale (decode, encode)
import Control.Monad ((<=<))
import Data.BigNum (h256)
import Data.Bits (bit, shiftL, shiftR, (.&.), (.|.))
import Data.ByteArray (cons, convert)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS (drop, length, pack, take)
import Data.ByteString.Base58 (bitcoinAlphabet, decodeBase58,
encodeBase58)
import Data.Digest.Blake2 (blake2_256, blake2_512)
import Data.Maybe (fromJust)
import Data.Word (Word16)
import Network.Polkadot.Primitives (MultiSigner (..))
import qualified Network.Polkadot.Primitives as P (AccountId)
-- | Some type that is able to be collapsed into an account ID.
--
-- It is not possible to recreate the original value from the account ID.
class IdentifyAccount a where
-- | The account ID that this can be transformed into.
type AccountId a
-- | Transform into an account.
into_account :: a -> AccountId a
instance IdentifyAccount MultiSigner where
type AccountId MultiSigner = P.AccountId
into_account (Ed25519Signer pub) = fromJust (h256 pub)
into_account (EcdsaSigner px pub) = fromJust (h256 $ blake2_256 $ cons px $ convert pub)
into_account Sr25519Signer = error "Sr25519 has no support yet"
instance Show MultiSigner where
show = show . to_ss58check . into_account
-- | Key that can be encoded to/from SS58.
--
-- See <https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58)#address-type>
-- for information on the codec.
class Ss58Codec a where
-- | Some if the string is a properly encoded SS58Check address (default prefix).
from_ss58check :: ByteString -> Either String a
from_ss58check = from_ss58check_with_version 42
-- | Return the ss58-check string for this key (default prefix).
to_ss58check :: a -> ByteString
to_ss58check = to_ss58check_with_version 42
-- | Return the ss58-check string for this key.
to_ss58check_with_version :: Word16 -> a -> ByteString
-- | Some if the string is a properly encoded SS58Check address (default prefix).
from_ss58check_with_version :: Word16 -> ByteString -> Either String a
instance Ss58Codec P.AccountId where
from_ss58check_with_version v = decode <=< from_ss58check_with_version' v <=< from_base58
to_ss58check_with_version v = to_base58 . to_ss58check_with_version' v . encode
to_base58 :: ByteString -> ByteString
to_base58 = encodeBase58 bitcoinAlphabet
from_base58 :: ByteString -> Either String ByteString
from_base58 = maybe (Left "Bad encoding") Right . decodeBase58 bitcoinAlphabet
to_ss58check_with_version' :: Word16 -> ByteString -> ByteString
to_ss58check_with_version' v input = out <> ss58hash out
where
out = encode_version v <> input
from_ss58check_with_version' :: Word16 -> ByteString -> Either String ByteString
from_ss58check_with_version' v input = versionGuard >> ss58hashGuard
where
checksumLen = 2
versionLen | v < 64 = 1
| otherwise = 2
inputLen = BS.length input - checksumLen - versionLen
input' = BS.take inputLen (BS.drop versionLen input)
versionGuard
| encode_version v == BS.take versionLen input = return ()
| otherwise = Left "Bad version"
ss58hashGuard
| ss58hash (BS.take (versionLen + inputLen) input)
== BS.drop (versionLen + inputLen) input = return input'
| otherwise = Left "Bad checksum"
ss58hash :: ByteString -> ByteString
ss58hash = BS.take 2 . blake2_512 . ("SS58PRE" <>)
encode_version :: Word16 -> ByteString
encode_version v
| v < 64 = BS.pack [fromIntegral v]
| otherwise = let first = bit 6 .|. ((v `shiftR` 2) .&. 63)
second = (v `shiftR` 8) .|. ((v .&. 3) `shiftL` 6)
in BS.pack (fromIntegral <$> [first, second])