cardano-addresses-4.0.0: lib/Cardano/Address/Script/Parser.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK prune #-}
-- |
-- Copyright: 2020 Input Output (Hong Kong) Ltd., 2021-2022 Input Output Global Inc. (IOG), 2023-2025 Intersect
-- License: Apache-2.0
module Cardano.Address.Script.Parser
(
-- ** Script Parser
scriptFromString
, scriptToText
, scriptParser
-- Internal
, requireSignatureOfParser
, requireAllOfParser
, requireAnyOfParser
, requireAtLeastOfParser
, requireCosignerOfParser
) where
import Prelude
import Cardano.Address.KeyHash
( KeyHash, keyHashFromText, prettyErrKeyHashFromText )
import Cardano.Address.Script
( Cosigner (..), ErrValidateScript (..), Script (..) )
import Data.Char
( isDigit, isLetter )
import Data.Text
( Text )
import Data.Word
( Word8 )
import Numeric.Natural
( Natural )
import Text.ParserCombinators.ReadP
( ReadP, readP_to_S, (<++) )
import qualified Data.Text as T
import qualified Text.ParserCombinators.ReadP as P
-- | Run 'scriptParser' on string input.
--
-- @since 3.0.0
scriptFromString
:: ReadP (Script a)
-> String
-> Either ErrValidateScript (Script a)
scriptFromString parser str =
case readP_to_S (scriptParser parser) str of
[(script, "")] -> pure script
_ -> Left Malformed
-- | Defines canonical string output for script that is
-- consistent with 'scriptFromString'.
--
-- @since 3.10.0
scriptToText
:: Show a
=> Script a
-> Text
scriptToText (RequireSignatureOf object) = T.pack $ show object
scriptToText (RequireAllOf contents) =
"all [" <> T.intercalate "," (map scriptToText contents) <> "]"
scriptToText (RequireAnyOf contents) =
"any [" <> T.intercalate "," (map scriptToText contents) <> "]"
scriptToText (RequireSomeOf m contents) =
"at_least "<> T.pack (show m) <>
" [" <> T.intercalate "," (map scriptToText contents) <> "]"
scriptToText (ActiveFromSlot s) =
"active_from " <> T.pack (show s)
scriptToText (ActiveUntilSlot s) =
"active_until " <> T.pack (show s)
-- | The script embodies combination of signing keys that need to be met to make
-- it valid. We assume here that the script could
-- delivered from standard input. The examples below are self-explanatory:
--
-- 1. requiring signature
-- 3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3fe
--
-- 2. 'any' for signature required
-- any [3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3fe, 3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3f1]
--
-- 3. 'all' signatures required
-- all [3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3fe, 3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3f1]
--
-- 4. 'at_least' 1 signature required
-- at_least 1 [3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3fe, 3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3f1]
--
-- 5. Nested script are supported
-- at_least 1 [3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3fe, all [3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3f1, 3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3f1]]
-- 6. 1 signature required after slot number 120
-- all [3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3fe, active_from 120]
-- 7. 1 signature required until slot number 150
-- all [3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3fe, active_until 150]
-- 8. 1 signature required in slot interval <145, 150)
-- all [3c07030e36bfffe67e2e2ec09e5293d384637cd2f004356ef320f3fe, active_from 145, active_until 150]
--
-- Parser is insensitive to whitespaces.
--
-- @since 3.0.0
scriptParser :: ReadP (Script a) -> ReadP (Script a)
scriptParser parser =
requireAllOfParser parser <++
requireAnyOfParser parser <++
requireAtLeastOfParser parser <++
parser <++
activeFromSlotParser <++
activeUntilSlotParser
requireSignatureOfParser :: ReadP (Script KeyHash)
requireSignatureOfParser = do
P.skipSpaces
verKeyStr <- P.munch1 (\c -> isDigit c || isLetter c || c == '_')
case keyHashFromText (T.pack verKeyStr) of
Left e -> fail (prettyErrKeyHashFromText e)
Right h -> pure (RequireSignatureOf h)
requireCosignerOfParser :: ReadP (Script Cosigner)
requireCosignerOfParser = do
P.skipSpaces
_identifier <- P.string "cosigner#"
cosignerid <- fromInteger . read <$> P.munch1 isDigit
pure $ RequireSignatureOf $ Cosigner cosignerid
requireAllOfParser :: ReadP (Script a) -> ReadP (Script a)
requireAllOfParser parser = do
P.skipSpaces
_identifier <- P.string "all"
RequireAllOf <$> commonPart parser
requireAnyOfParser :: ReadP (Script a) -> ReadP (Script a)
requireAnyOfParser parser = do
P.skipSpaces
_identifier <- P.string "any"
RequireAnyOf <$> commonPart parser
requireAtLeastOfParser :: ReadP (Script a) -> ReadP (Script a)
requireAtLeastOfParser parser = do
P.skipSpaces
_identifier <- P.string "at_least"
RequireSomeOf <$> naturalParser <*> commonPart parser
activeFromSlotParser :: ReadP (Script a)
activeFromSlotParser = do
P.skipSpaces
_identifier <- P.string "active_from"
ActiveFromSlot <$> slotParser
activeUntilSlotParser :: ReadP (Script a)
activeUntilSlotParser = do
P.skipSpaces
_identifier <- P.string "active_until"
ActiveUntilSlot <$> slotParser
naturalParser :: ReadP Word8
naturalParser = do
P.skipSpaces
fromInteger . read <$> P.munch1 isDigit
slotParser :: ReadP Natural
slotParser = do
P.skipSpaces
fromInteger . read <$> P.munch1 isDigit
commonPart :: ReadP (Script a) -> ReadP [Script a]
commonPart parser = do
P.skipSpaces
_open <- P.string "["
P.skipSpaces
content <- P.sepBy (scriptParser parser) (P.string ",")
P.skipSpaces
_close <- P.string "]"
P.skipSpaces
return content