packages feed

morley-1.18.0: src/Morley/Tezos/Address/Alias.hs

-- SPDX-FileCopyrightText: 2022 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

module Morley.Tezos.Address.Alias
  ( AddressOrAlias(..)
  , Alias(..)
  , SomeAlias(..)
  , SomeAddressOrAlias(..)
  , ImplicitAlias
  , ContractAlias
  , ImplicitAddressOrAlias
  , ContractAddressOrAlias
  , unAlias
  , mkAlias
  , aliasKindSanity
  )
  where

import Data.Aeson (FromJSON(..), ToJSON(..))
import Data.Constraint (Dict(..), (\\))
import Data.Singletons (SingI(..), demote)
import Data.Type.Equality ((:~:)(..))
import Fmt (Buildable(..), nameF, pretty, (+|), (|+))
import Options.Applicative qualified as Opt

import Morley.Tezos.Address
import Morley.Tezos.Address.Kinds
import Morley.Util.CLI (HasCLReader(..))
import Morley.Util.Sing

-- | @tezos-client@ can associate addresses with textual aliases.
-- This type denotes such an alias.
data Alias (kind :: AddressKind) where
  ImplicitAlias :: Text -> Alias 'AddressKindImplicit
  ContractAlias :: Text -> Alias 'AddressKindContract

-- | A type only allowing v'ImplicitAlias' values.
type ImplicitAlias = Alias 'AddressKindImplicit

-- | A type only allowing v'ContractAlias' values.
type ContractAlias = Alias 'AddressKindContract

deriving stock instance Show (Alias kind)
deriving stock instance Eq (Alias kind)
deriving stock instance Ord (Alias kind)

-- | Get raw alias text from 'Alias'
unAlias :: Alias kind -> Text
unAlias = \case
  ImplicitAlias x -> x
  ContractAlias x -> x

-- | Construct an 'Alias' from alias 'Text'.
mkAlias :: forall kind. (SingI kind, L1AddressKind kind) => Text -> Alias kind
mkAlias = usingImplicitOrContractKind @kind $ case sing @kind of
  SAddressKindImplicit -> ImplicitAlias
  SAddressKindContract -> ContractAlias

instance Buildable (Alias kind) where
  build = build . unAlias

instance ToJSON (Alias kind) where
  toJSON = toJSON . unAlias

instance (SingI kind, L1AddressKind kind) => FromJSON (Alias kind) where
  parseJSON = fmap mkAlias . parseJSON

-- | Existential wrapper over 'Alias'.
data SomeAlias = forall kind. SomeAlias (Alias kind)

deriving stock instance Show SomeAlias

instance Buildable SomeAlias where
  build (SomeAlias x) = build x

-- | Given an 'Alias', prove it's @kind@ is well-defined (i.e. it has a 'SingI'
-- instance and satisfies 'L1AddressKind' constraint)
aliasKindSanity :: Alias kind -> Dict (L1AddressKind kind, SingI kind)
aliasKindSanity = \case
  ImplicitAlias{} -> Dict
  ContractAlias{} -> Dict

-- | Representation of an address that @tezos-client@ uses. It can be
-- an address itself or a textual alias.
data AddressOrAlias kind
  = AddressResolved (KindedAddress kind)
  -- ^ Address itself, can be used as is.
  | AddressAlias (Alias kind)
  -- ^ Address alias, should be resolved by @tezos-client@.
  deriving stock (Show, Eq, Ord)

instance (SingI kind, L1AddressKind kind) => HasCLReader (AddressOrAlias kind) where
  getReader =
    Opt.str >>= \addrOrAlias ->
      case parseAddress addrOrAlias of
        Right (MkAddress (addr :: KindedAddress kind')) ->
          case eqI @kind @kind' \\ addressKindSanity addr of
            Just Refl -> pure $ AddressResolved addr
            Nothing -> Opt.readerError $ pretty $ nameF "Unexpected address kind" $
              "expected " +| demote @kind |+ " address, but got " +| addr |+ ""
        Left _ -> pure $ AddressAlias (mkAlias addrOrAlias)
  getMetavar = "ADDRESS OR ALIAS"

instance Buildable (AddressOrAlias kind) where
  build = \case
    AddressResolved addr -> build addr
    AddressAlias alias -> build alias

-- | Convenience type synonym.
type ImplicitAddressOrAlias = AddressOrAlias 'AddressKindImplicit

-- | Convenience type synonym.
type ContractAddressOrAlias = AddressOrAlias 'AddressKindContract

-- | Existential over 'AddressOrAlias'.
data SomeAddressOrAlias = forall kind. SomeAddressOrAlias (AddressOrAlias kind)

instance Buildable SomeAddressOrAlias where
  build (SomeAddressOrAlias x) = build x

deriving stock instance Show SomeAddressOrAlias