packages feed

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

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

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

import Data.Aeson (FromJSON(..), ToJSON(..))
import Data.Constraint (Dict(..), (\\))
import Data.Constraint.Extras (has)
import Data.Constraint.Extras.TH (deriveArgDict)
import Data.GADT.Compare.TH (deriveGCompare, deriveGEq)
import Data.Singletons (SingI(..), demote)
import Data.Text qualified as T
import Fmt (Buildable(..), Doc, pretty)
import Options.Applicative qualified as Opt

import Data.Char qualified as Char
import Morley.Tezos.Address
import Morley.Tezos.Address.Kinds
import Morley.Util.CLI (HasCLReader(..))
import Morley.Util.Constrained
import Morley.Util.Interpolate (itu)
import Morley.Util.Sing
import Morley.Util.TH

-- | @octez-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)

deriveGADTNFData ''Alias
deriveGEq ''Alias
deriveGCompare ''Alias
deriveArgDict ''Alias

-- | 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'.
type SomeAlias = Constrained NullConstraint Alias

pattern SomeAlias :: Alias a -> SomeAlias
pattern SomeAlias x = Constrained x
{-# COMPLETE SomeAlias #-}

-- | 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 x = has @AliasKindSanityHelper x Dict

class (L1AddressKind kind, SingI kind) => AliasKindSanityHelper kind
instance (L1AddressKind kind, SingI kind) => AliasKindSanityHelper kind

{- | This type is meant to be used to parse CLI options where either an address
or an alias of an implicit account or a contract can be accepted.

This can be later converted to an address using @Morley.Client.resolveAddress@
or an alias using @Morley.Client.getAlias@.

This polymorphic type can be instantiated with 'AddressKindImplicit' or
'AddressKindContract' (see 'ImplicitAddressOrAlias' and
'ContractAddressOrAlias'), but not 'AddressKindSmartRollup'. There is no
@octez-client@ command to list smart rollup aliases, unlike @octez-client list
known addresses/contracts@, therefore:

1. It wouldn't be possible to implement @Morley.Client.resolveAddress@ for
   @AddressAlias _ :: AddressOrAlias 'AddressKindTxRollup@.
2. It wouldn't be possible to implement @Morley.Client.getAlias@ for
   @AddressResolved _ :: AddressOrAlias 'AddressKindTxRollup@.

This should be revisited if/when @octez-client@ adds support for smart rollup
aliases.
-}
data AddressOrAlias kind where
  AddressResolved :: L1AddressKind kind => KindedAddress kind -> AddressOrAlias kind
  -- ^ Address itself, can be used as is.
  AddressAlias :: Alias kind -> AddressOrAlias kind
  -- ^ Address alias, should be resolved by @octez-client@.

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

instance (SingI kind, L1AddressKind kind) => HasCLReader (AddressOrAlias kind) where
  getReader =
    getReader @SomeAddressOrAlias >>= \case
      SAOAKindSpecified aoa ->
        case castSing @_ @kind aoa \\ addressOrAliasKindSanity aoa of
          Just aoa' -> pure aoa'
          Nothing -> Opt.readerError
            let expectedKind = demote @kind in
            [itu|Unexpected address kind: expected #{expectedKind} address or alias, but got: '#{aoa}'|]
      SAOAKindUnspecified aliasText ->
        pure $ AddressAlias $ mkAlias aliasText
  getMetavar = (fmap Char.toUpper $ pretty $ demote @kind) <> " ADDRESS OR ALIAS"

instance Buildable (AddressOrAlias kind) where
  build = \case
    AddressResolved addr -> build addr
    AddressAlias alias -> aliasPrefix @kind <> ":" <> build alias \\ aliasKindSanity alias

{- This type is meant to be used to parse CLI options where either an address or an alias
of an implicit account can be accepted.

Example inputs:
  * "tz1hZ7o4bhFTo6AXpWZsXzbnddEK3dSCv1S8": an address belonging to an implicit account.
  * "implicit:some-alias" or "some-alias": an alias that is expected to be associated with an implicit account.
      * If it's associated with a contract, @Morley.Client.resolveAddress@ will fail.
      * If it's associated with __both__ a contract and an implicit account,
        @Morley.Client.resolveAddress@ will return the implicit account address.

Parsing will fail on these inputs:
  * "KT1STb2aG7NpoBBNRggvummqsxNQZmuAVFvG"
  * "contract:some-alias"

Refer to the 'HasCLReader' and @Morley.Client.Resolve@ instances for implementation details.
-}
type ImplicitAddressOrAlias = AddressOrAlias 'AddressKindImplicit

{- This type is meant to be used to parse CLI options where either an address or an alias
of a contract can be accepted.

Example inputs:
  * "KT1STb2aG7NpoBBNRggvummqsxNQZmuAVFvG": an address belonging to a contract.
  * "contract:some-alias" or "some-alias": an alias that is expected to be associated with a contract.
      * If it's associated with an implicit account, @Morley.Client.resolveAddress@ will fail.
      * If it's associated with __both__ a contract and an implicit account,
        @Morley.Client.resolveAddress@ will return the contract address.

Parsing will fail on these inputs:
  * "tz1hZ7o4bhFTo6AXpWZsXzbnddEK3dSCv1S8"
  * "implicit:some-alias"

Refer to the 'HasCLReader' and @Morley.Client.Resolve@ instances for implementation details.
-}
type ContractAddressOrAlias = AddressOrAlias 'AddressKindContract

-- | Given an 'AddressOrAlias', prove it's @kind@ is well-defined (i.e. it has a 'SingI'
-- instance and satisfies 'L1AddressKind' constraint)
addressOrAliasKindSanity :: forall kind. AddressOrAlias kind -> Dict (L1AddressKind kind, SingI kind)
addressOrAliasKindSanity = \case
  AddressResolved addr -> Dict \\ addressKindSanity addr
  AddressAlias alias -> aliasKindSanity alias

{- This type is meant to be used to parse CLI options where either an address or an alias
of either a contract or an implicit account can be accepted.

Example inputs:
  * "KT1STb2aG7NpoBBNRggvummqsxNQZmuAVFvG": an address belonging to a contract
  * "tz1hZ7o4bhFTo6AXpWZsXzbnddEK3dSCv1S8": an address belonging to an implicit account
  * "contract:some-alias": an alias that is expected to be associated with a contract.
      * If it's associated with an implicit account, @Morley.Client.resolveAddress@ will fail.
  * "implicit:some-alias": an alias that is expected to be associated with an implicit account.
      * If it's associated with a contract, @Morley.Client.resolveAddress@ will fail.
  * "some-alias": an alias that is expected to be associated with either a contract or an implicit account.
      * If it's associated with __both__ a contract and an implicit account,
        @Morley.Client.resolveAddress@ will fail.

Refer to the 'HasCLReader' and @Morley.Client.Resolve@ instances for implementation details.
-}
data SomeAddressOrAlias where
  SAOAKindSpecified :: AddressOrAlias kind -> SomeAddressOrAlias
  SAOAKindUnspecified :: Text -> SomeAddressOrAlias

deriving stock instance Show SomeAddressOrAlias

-- | The output of 'build' should be parseable by the 'HasCLReader' instance.
instance Buildable SomeAddressOrAlias where
  build = \case
    SAOAKindUnspecified alias -> build alias
    SAOAKindSpecified aoa -> build aoa

instance HasCLReader SomeAddressOrAlias where
  getMetavar = "CONTRACT OR IMPLICIT ADDRESS OR ALIAS"
  getReader =
    Opt.str >>= \str ->
      case parseAddress str of
        Right (Constrained addr) ->
          case addr of
            ImplicitAddress{} -> pure $ SAOAKindSpecified $ AddressResolved addr
            ContractAddress{} -> pure $ SAOAKindSpecified $ AddressResolved addr
            SmartRollupAddress{} -> Opt.readerError $ "Unexpected smart rollup address: " <> pretty addr
        Left _ ->
          pure $
            parseAliasWithPrefix @'AddressKindImplicit str <|> parseAliasWithPrefix @'AddressKindContract str
              & fromMaybe (SAOAKindUnspecified str)
      where
        -- Try parsing an alias with an explicit kind prefix.
        parseAliasWithPrefix
          :: forall kind. (L1AddressKind kind, SingI kind)
          => Text
          -> Maybe SomeAddressOrAlias
        parseAliasWithPrefix str =
          T.stripPrefix (pretty $ aliasPrefix @kind <> ":") str <&> \alias ->
            SAOAKindSpecified $ AddressAlias $ mkAlias @kind alias

-- | The prefix used to specify whether an alias belongs to a contract or an implicit account.
aliasPrefix :: forall addressKind. (L1AddressKind addressKind, SingI addressKind) => Doc
aliasPrefix =
  usingImplicitOrContractKind @addressKind $
    build $ demote @addressKind

contractPrefix, implicitPrefix :: Doc
contractPrefix = aliasPrefix @'AddressKindContract
implicitPrefix = aliasPrefix @'AddressKindImplicit