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