diff --git a/cardano-api/src/Cardano/Api/Typed.hs b/cardano-api/src/Cardano/Api/Typed.hs index b0bffe0314d..91966c3057f 100644 --- a/cardano-api/src/Cardano/Api/Typed.hs +++ b/cardano-api/src/Cardano/Api/Typed.hs @@ -202,7 +202,7 @@ module Cardano.Api.Typed ( -- ** Script addresses -- | Making addresses from scripts. - ScriptHash, + ScriptHash(..), scriptHash, -- ** Multi-signature scripts @@ -435,7 +435,7 @@ import qualified Cardano.Chain.Slotting as Byron -- -- Shelley imports -- -import Ouroboros.Consensus.Shelley.Eras (StandardAllegra, StandardShelley, StandardMary) +import Ouroboros.Consensus.Shelley.Eras (StandardAllegra, StandardMary, StandardShelley) import Ouroboros.Consensus.Shelley.Protocol.Crypto (StandardCrypto) import qualified Shelley.Spec.Ledger.Address as Shelley diff --git a/cardano-api/src/Cardano/Api/Value.hs b/cardano-api/src/Cardano/Api/Value.hs index f33e0ea8f6a..b12cc19073e 100644 --- a/cardano-api/src/Cardano/Api/Value.hs +++ b/cardano-api/src/Cardano/Api/Value.hs @@ -91,7 +91,8 @@ quantityToLovelace (Quantity x) = Lovelace x newtype PolicyId = PolicyId ScriptHash deriving stock (Show) - deriving newtype (Eq, Ord, IsString) + deriving (Eq, Ord) + newtype AssetName = AssetName ByteString deriving stock (Show) diff --git a/cardano-cli/cardano-cli.cabal b/cardano-cli/cardano-cli.cabal index 7746792efe0..3434563319c 100644 --- a/cardano-cli/cardano-cli.cabal +++ b/cardano-cli/cardano-cli.cabal @@ -59,6 +59,8 @@ library Cardano.CLI.Shelley.Run.TextView Cardano.CLI.Shelley.Run.Transaction + Cardano.CLI.Mary.Parser + Cardano.CLI.TopHandler other-modules: Paths_cardano_cli @@ -80,7 +82,6 @@ library , cardano-crypto-wrapper , cardano-ledger , cardano-ledger-shelley-ma - -- TODO: We shouldn't be forced to import "cardano-node". Fix this. , cardano-node , cardano-prelude , cardano-slotting @@ -103,6 +104,7 @@ library , ouroboros-consensus-cardano , ouroboros-consensus-shelley , ouroboros-network + , parsec , process , scientific , shelley-spec-ledger @@ -166,6 +168,8 @@ test-suite cardano-cli-test , base16-bytestring , cardano-api , cardano-cli + , cardano-crypto-class + , cardano-ledger-shelley-ma , cardano-node , cardano-prelude , containers @@ -176,7 +180,11 @@ test-suite cardano-cli-test , hedgehog-extras , lifted-base , optparse-applicative + , ouroboros-consensus-shelley + , parsec , process + , shelley-spec-ledger + , shelley-spec-ledger-test , temporary , text , time @@ -184,7 +192,9 @@ test-suite cardano-cli-test , unordered-containers other-modules: Test.Cli.FilePermissions + Test.Cli.Gen Test.Cli.ITN + Test.Cli.MultiAssetParsing Test.Cli.Pioneers.Exercise1 Test.Cli.Pioneers.Exercise2 Test.Cli.Pioneers.Exercise3 diff --git a/cardano-cli/src/Cardano/CLI/Mary/Parser.hs b/cardano-cli/src/Cardano/CLI/Mary/Parser.hs new file mode 100644 index 00000000000..6dfbd153de4 --- /dev/null +++ b/cardano-cli/src/Cardano/CLI/Mary/Parser.hs @@ -0,0 +1,270 @@ +module Cardano.CLI.Mary.Parser + ( Token (..) + , Tokens + , TParser + , addition + , applyAddSubtract + , calculateValue + , lexToken + , lexTokens + , preValueAddition + , preValueLovelace + , preValueMultiAsset + , preValueParser + , preValueSubtraction + , preValToValue + , stringToValue + , subtraction + , textToPolicyId + , tokenToValue + , valueTokenFullySpecified + , valueTokenPolicyIdAndAssetId + , valueTokenPolicyIdOnly + ) where + +import Prelude + +import Control.Applicative (many, (<|>)) +import Data.Text (Text) +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text +import Data.Word (Word64) +import Text.Parsec (ParseError, Parsec, SourcePos, anyChar, getPosition, manyTill, parse, + token, try, ()) +import Text.Parsec.Char (alphaNum, digit, hexDigit, letter, space, spaces, string) +import Text.Parsec.String (Parser) +import Text.ParserCombinators.Parsec.Combinator (eof, many1, notFollowedBy, sepBy1) + +import Cardano.Api.Typed (AssetId (..), AssetName (..), PolicyId (..), Quantity (..), + ScriptHash (..), Value, selectAsset, valueFromList) +import Cardano.Crypto.Hash (hashFromStringAsHex) +import qualified Shelley.Spec.Ledger.Scripts as Shelley + +{- HLINT ignore "Reduce duplication" -} + +stringToValue :: String -> Either ParseError Value +stringToValue input = calculateValue <$> fullParse input + where + fullParse :: String -> Either ParseError [PreValue] + fullParse str = parse lexTokens "" str >>= parse preValueParser "" + +calculateValue :: [PreValue] -> Value +calculateValue preVals = + let finalVal = mconcat . map preValToValue $ applyAddSubtract preVals + ada = selectAsset finalVal AdaAssetId + in if selectAsset finalVal AdaAssetId < 0 + then error $ "Negative lovelace values are not allowed: " <> show ada + else finalVal + +applyAddSubtract :: [PreValue] -> [PreValue] +applyAddSubtract [] = [] +applyAddSubtract [x] = [x] +applyAddSubtract (Subtraction : Lovelace w64 : rest) = + Lovelace w64 : applyAddSubtract rest +applyAddSubtract (Subtraction : MultiAsset pId aId minted : rest) = + MultiAsset pId aId (negate minted) : applyAddSubtract rest +applyAddSubtract (Addition : rest) = applyAddSubtract rest +applyAddSubtract (x : rest) = x : applyAddSubtract rest + +textToPolicyId :: Text -> PolicyId +textToPolicyId hashText = + case hashFromStringAsHex $ Text.unpack hashText of + Just h -> PolicyId . ScriptHash $ Shelley.ScriptHash h + Nothing -> error $ "PolicyId: " <> Text.unpack hashText <> " is not a hash." + +preValToValue :: PreValue -> Value +preValToValue Addition = valueFromList [] +preValToValue Subtraction = valueFromList [] +preValToValue (Lovelace w64) = + let quantity = Quantity w64 + in valueFromList [(AdaAssetId, quantity)] +preValToValue (MultiAsset pId aId minted) = + let polId = textToPolicyId pId + assetName = AssetName $ Text.encodeUtf8 aId + assetId = AssetId polId assetName + quantity = Quantity minted + in valueFromList [(assetId , quantity)] + +-- Parser + +type TParser a = Parsec Tokens () a + +data PreValue = Lovelace Integer + | MultiAsset + Text + -- ^ PolicyId + Text + -- ^ AssetId + Integer + -- ^ Amount minted + | Addition + | Subtraction + deriving Show + +preValueParser :: TParser [PreValue] +preValueParser = + many1 ( preValueLovelace + <|> preValueMultiAsset + <|> preValueAddition + <|> preValueSubtraction + ) + +tokenToTParser :: (Token -> Maybe a) -> TParser a +tokenToTParser f = + token + (show . snd) + fst + $ \(_,t) -> f t + +preValueLovelace :: TParser PreValue +preValueLovelace = + tokenToTParser (\t -> case t of + LovelaceT n -> Just $ Lovelace n + _ -> Nothing + ) + +preValueMultiAsset :: TParser PreValue +preValueMultiAsset = + tokenToTParser (\t -> case t of + MultiAssetT pId aId aM -> Just $ MultiAsset pId aId aM + _ -> Nothing + ) + +preValueAddition :: TParser PreValue +preValueAddition = + tokenToTParser (\t -> case t of + AdditionT -> Just Addition + _ -> Nothing + ) + + +preValueSubtraction :: TParser PreValue +preValueSubtraction = + tokenToTParser (\t -> case t of + SubtractionT -> Just Subtraction + _ -> Nothing + ) + +-- Lexer + +type Tokens = [(SourcePos, Token)] + +data Token = LovelaceT Integer + | MultiAssetT + Text + -- ^ ScriptHash + Text + -- ^ AssetId + Integer + -- ^ AmountMinted + | AdditionT + | PeriodT + | SubtractionT + deriving (Eq, Ord, Show) + +lexTokens :: Parser Tokens +lexTokens = spaces *> sepBy1 ((,) <$> getPosition <*> lexToken) spaces + +lexToken :: Parser Token +lexToken = + try (lovelaceToken "Expecting \"Word64 lovelace\"") + <|> (addition "Expecting \"+\"") + <|> (subtraction "Expecting \"-\"") + <|> (valueToken "Expecting \"INT hexidecimal.STRING\"") + <|> incorrectSyntax + +-- Primitive Token Lexers + +incorrectSyntax :: Parser Token +incorrectSyntax = do + _ <- spaces + incorrect <- many alphaNum + _ <- manyTill anyChar eof + fail $ "Incorrect syntax: " <> incorrect + <> "\nExpecting \"Word64 lovelace\",\"+\" or \"INT hexidecimal.STRING\"" + +period :: Parser Token +period = PeriodT <$ string "." + +word64 :: Parser Integer +word64 = do i <- integer + if i > fromIntegral (maxBound :: Word64) + then fail "Word64 max bound" + else return i + +integer :: Parser Integer +integer = do d <- many1 digit + notFollowedBy alphaNum + return $ read d + +lovelaceToken :: Parser Token +lovelaceToken = do + w64 <- word64 "Word64" + _ <- spaces + _ <- string "lovelace" + _ <- spaces + return $ LovelaceT w64 + +valueToken :: Parser Token +valueToken = + try valueTokenFullySpecified + <|> try valueTokenPolicyIdAndAssetId + <|> valueTokenPolicyIdOnly + <* spaces + +valueTokenFullySpecified :: Parser Token +valueTokenFullySpecified = do + i <- try integer "INT" + let minted = fromInteger i + _ <- spaces + pId <- scriptHash + _ <- period + assetId <- try $ many (letter <|> digit) + _ <- spaces + return $ MultiAssetT pId (Text.pack assetId) minted + +valueTokenPolicyIdAndAssetId :: Parser Token +valueTokenPolicyIdAndAssetId = do + pId <- scriptHash + _ <- period + notFollowedBy space + assetId <- many (letter <|> digit) + _ <- spaces <|> eof + notFollowedBy integer + return $ MultiAssetT pId (Text.pack assetId) 1 + +valueTokenPolicyIdOnly :: Parser Token +valueTokenPolicyIdOnly = do + i <- try integer "INT" + let minted = fromInteger i + _ <- spaces + pId <- scriptHash + notFollowedBy period + _ <- spaces + return $ MultiAssetT pId (Text.pack "") minted + +scriptHash :: Parser Text +scriptHash = Text.pack <$> many1 hexDigit + +addition :: Parser Token +addition = (AdditionT <$ string "+") <* spaces + +subtraction :: Parser Token +subtraction = (SubtractionT <$ string "-") <* spaces + +-- Helpers + +tokenToValue :: Token -> Value +tokenToValue AdditionT = valueFromList [] +tokenToValue SubtractionT = valueFromList [] +tokenToValue (LovelaceT w64) = + let quantity = Quantity w64 + in valueFromList [(AdaAssetId, quantity)] +tokenToValue (MultiAssetT pId aId minted) = + let polId = textToPolicyId pId + assetName = AssetName $ Text.encodeUtf8 aId + assetId = AssetId polId assetName + quantity = Quantity minted + in valueFromList [(assetId , quantity)] +tokenToValue PeriodT = valueFromList [] + diff --git a/cardano-cli/test/Test/Cli/Gen.hs b/cardano-cli/test/Test/Cli/Gen.hs new file mode 100644 index 00000000000..3c18c6232a8 --- /dev/null +++ b/cardano-cli/test/Test/Cli/Gen.hs @@ -0,0 +1,125 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Test.Cli.Gen where + +import Cardano.Prelude + +import qualified Data.Sequence.Strict as Strict + +import Cardano.Api.Typed +import Cardano.CLI.Helpers (textShow) +import Cardano.CLI.Mary.Parser (Token (..), tokenToValue) +import Cardano.Crypto.Hash (hashToTextAsHex) +import Cardano.Ledger.Mary (MaryEra) +import qualified Cardano.Ledger.ShelleyMA.Timelocks as Timelock + +import Ouroboros.Consensus.Shelley.Eras (StandardCrypto) +import qualified Shelley.Spec.Ledger.Scripts as Shelley + +import Hedgehog (Gen) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range + +-- Lexing Token Generators + +genVariableSpace :: Gen Text +genVariableSpace = Gen.text (Range.constant 1 10) $ return ' ' + +genLovelaceToken :: Gen (Text, Token) +genLovelaceToken = do + let mBound = fromIntegral (maxBound :: Word64) + w64 <- Gen.integral_ (Range.constant 1 mBound) + space1 <- genVariableSpace + space2 <- genVariableSpace + return (textShow w64 <> space1 <> "lovelace" <> space2, LovelaceT w64) + +genValueTokenFullySpecified :: Gen (Text, Token) +genValueTokenFullySpecified = do + sHash <- genScriptHashMaryText + assetId <- Gen.text (Range.constant 1 15) Gen.alphaNum + (mintedText, minted) <- genMintedText + variableSpace <- genVariableSpace + return ( mintedText <> variableSpace <> sHash <> "." <> assetId <> variableSpace + , MultiAssetT sHash assetId minted + ) + +genValueTokenPidAndAssetId :: Gen (Text, Token) +genValueTokenPidAndAssetId = do + sHash <- genScriptHashMaryText + assetId <- Gen.text (Range.constant 1 15) Gen.alphaNum + return ( sHash <> "." <> assetId + , MultiAssetT sHash assetId 1 + ) + +genValueTokenPidOnly :: Gen (Text, Token) +genValueTokenPidOnly = do + sHash <- genScriptHashMaryText + (mintedText, minted) <- genMintedText + variableSpace <- genVariableSpace + return ( mintedText <> variableSpace <> sHash + , MultiAssetT sHash "" minted + ) + +genValueTokens :: Gen [(Text, Token)] +genValueTokens = do + valsFulSpec <- Gen.list (Range.constant 1 10) genValueTokenFullySpecified + valsPidAssetId <- Gen.list (Range.constant 1 10) genValueTokenPidAndAssetId + valsPidOnly <- Gen.list (Range.constant 1 10) genValueTokenPidOnly + return $ valsFulSpec ++ valsPidAssetId ++ valsPidOnly + +genAdditionToken :: Gen (Text, Token) +genAdditionToken = do spaces1 <- genVariableSpace + return ("+" <> spaces1, AdditionT) + +genSubtractionToken :: Gen (Text, Token) +genSubtractionToken = do spaces1 <- genVariableSpace + return ("-" <> spaces1, SubtractionT) + +genTokens :: Gen (Text, [Token]) +genTokens = do lovelaces <- Gen.list (Range.constant 1 10) genLovelaceToken + vals <- genValueTokens + let total = lovelaces ++ vals + addOrSubtractTk <- Gen.choice [genAdditionToken, genSubtractionToken] + return . sequence $ intersperse addOrSubtractTk total + +genMintedText :: Gen (Text, Integer) +genMintedText = do + let mBound = fromIntegral (maxBound :: Word64) + minted <- Gen.integral_ (Range.constant 0 mBound) + return (textShow minted, minted) + +-- Parsing Token Generators + +genValues :: Gen (Text, Value) +genValues = do lovelaces <- Gen.list (Range.constant 1 10) genLovelaceValue + vals <- Gen.list (Range.constant 1 10) genMultiAssetValue + add <- genAdditionValue + let total = lovelaces ++ vals + return . mconcat $ intersperse add total + +genLovelaceValue :: Gen (Text, Value) +genLovelaceValue = do (input, lovelaceToken) <- genLovelaceToken + return (input, tokenToValue lovelaceToken) + +genMultiAssetValue :: Gen (Text, Value) +genMultiAssetValue = do vTkns <- genValueTokens + (input, maToken) <- Gen.element vTkns + return ( input + , tokenToValue maToken + ) + +genAdditionValue :: Gen (Text, Value) +genAdditionValue = do (input, AdditionT) <- genAdditionToken + return (input, valueFromList []) + +genSubtractionValue :: Gen (Text, Value) +genSubtractionValue = do (input, SubtractionT) <- genSubtractionToken + return (input, valueFromList []) + +genMaryScriptHash :: Gen (Shelley.ScriptHash (MaryEra StandardCrypto)) +genMaryScriptHash = return . Timelock.hashTimelockScript $ Timelock.RequireAllOf Strict.empty + +genScriptHashMaryText :: Gen Text +genScriptHashMaryText = do Shelley.ScriptHash h <- genMaryScriptHash + return $ hashToTextAsHex h + diff --git a/cardano-cli/test/Test/Cli/MultiAssetParsing.hs b/cardano-cli/test/Test/Cli/MultiAssetParsing.hs new file mode 100644 index 00000000000..b4c6571df63 --- /dev/null +++ b/cardano-cli/test/Test/Cli/MultiAssetParsing.hs @@ -0,0 +1,171 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Test.Cli.MultiAssetParsing where + +import Cardano.Prelude + +import Cardano.Api.Typed +import Cardano.CLI.Mary.Parser +import qualified Data.Text as Text +import Test.Cli.Gen +import Text.Parsec (ParseError) +import qualified Text.Parsec as Parsec (parse) +import Text.Parsec.String (Parser) + +import Hedgehog (Gen, Property, checkSequential, discover, evalEither, forAll, property, + (===)) +import Hedgehog.Internal.Property (failWith) + +-- Lexer +lex :: Parser a -> Text -> Either ParseError a +lex p = Parsec.parse p "" . Text.unpack + +-- Parser +parse :: TParser a -> Tokens -> Either ParseError a +parse p = Parsec.parse p "" + +-- Lexing + +prop_lexLovelace :: Property +prop_lexLovelace = + property $ do + (input, expectedOutput) <- forAll genLovelaceToken + case lex lexToken input of + Left pe -> failWith Nothing $ show pe + Right token -> token === expectedOutput + +prop_lexValue_fullySpecified :: Property +prop_lexValue_fullySpecified = + property $ do + (input, expectedOutput) <- forAll genValueTokenFullySpecified + case lex valueTokenFullySpecified input of + Left pe -> failWith Nothing $ show pe + Right token -> token === expectedOutput + + +prop_lexValue_pid_and_asset_id :: Property +prop_lexValue_pid_and_asset_id = + property $ do + (input, expectedOutput) <- forAll genValueTokenPidAndAssetId + case lex valueTokenPolicyIdAndAssetId input of + Left pe -> failWith Nothing $ show pe + Right token -> token === expectedOutput + +prop_lexValue_pid_only :: Property +prop_lexValue_pid_only = + property $ do + (input, expectedOutput) <- forAll genValueTokenPidOnly + case lex valueTokenPolicyIdOnly input of + Left pe -> failWith Nothing $ show pe + Right token -> token === expectedOutput + +prop_lexAddition :: Property +prop_lexAddition = + property $ do + (input, expectedOutput) <- forAll genAdditionToken + case lex addition input of + Left pe -> failWith Nothing $ show pe + Right token -> token === expectedOutput + +prop_lexSubtraction :: Property +prop_lexSubtraction = + property $ do + (input, expectedOutput) <- forAll genSubtractionToken + case lex subtraction input of + Left pe -> failWith Nothing $ show pe + Right token -> token === expectedOutput + +prop_lexTokens :: Property +prop_lexTokens = + property $ do + (input, expectedOutput) <- forAll genTokens + case lex lexTokens input of + Left pe -> failWith Nothing $ show pe + Right tokens -> + sort (foldl' (\n (_,tk) -> tk : n) [] tokens) === sort expectedOutput + +-- Parsing + +prop_parseLovelace :: Property +prop_parseLovelace = + property $ do + (input, expectedOutput) <- forAll genLovelaceValue + tkn <- evalEither $ lex lexTokens input + case parse preValueLovelace tkn of + Left pe -> failWith Nothing $ show pe + Right preVal -> preValToValue preVal === expectedOutput + +prop_parseMultiAsset :: Property +prop_parseMultiAsset = + property $ do + (input, expectedOutput) <- forAll genMultiAssetValue + tkn <- evalEither $ lex lexTokens input + case parse preValueMultiAsset tkn of + Left pe -> failWith Nothing $ show pe + Right preVal -> preValToValue preVal === expectedOutput + +prop_parseAddition :: Property +prop_parseAddition = + property $ do + (input, expectedOutput) <- forAll genAdditionValue + tkn <- evalEither $ lex lexTokens input + case parse preValueAddition tkn of + Left pe -> failWith Nothing $ show pe + Right preVal -> preValToValue preVal === expectedOutput + +prop_parseSubtraction :: Property +prop_parseSubtraction = + property $ do + (input, expectedOutput) <- forAll genSubtractionValue + tkn <- evalEither $ lex lexTokens input + case parse preValueSubtraction tkn of + Left pe -> failWith Nothing $ show pe + Right preVal -> preValToValue preVal === expectedOutput + +prop_parse :: Property +prop_parse = property $ do + (input, expectedOutput) <- forAll genValues + tkns <- evalEither $ lex lexTokens input + case parse preValueParser tkns of + Left pe -> failWith Nothing $ show pe + Right preVals -> calculateValue preVals === expectedOutput + +prop_addition_ada :: Property +prop_addition_ada = + testAdditionOperation genAdditionValue genLovelaceValue + +prop_addition_multi_asset :: Property +prop_addition_multi_asset = + testAdditionOperation genAdditionValue genMultiAssetValue + +prop_subtraction_multi_asset :: Property +prop_subtraction_multi_asset = + testSubtractionOperation genSubtractionValue genMultiAssetValue + +testAdditionOperation :: Gen (Text, Value) -> Gen (Text,Value) -> Property +testAdditionOperation binaryOperation value = property $ do + (input1, out1) <- forAll value + (input2, binaryOp) <- forAll binaryOperation + (input3, out2) <- forAll value + tkns <- evalEither $ lex lexTokens (input1 <> input2 <> input3) + case parse preValueParser tkns of + Left pe -> failWith Nothing $ show pe + Right preVals -> calculateValue preVals === (out1 <> binaryOp <> out2) + +testSubtractionOperation :: Gen (Text, Value) -> Gen (Text,Value) -> Property +testSubtractionOperation binaryOperation value = property $ do + (input1, out1) <- forAll value + (input2, binaryOp) <- forAll binaryOperation + (input3, out2) <- forAll value + tkns <- evalEither $ lex lexTokens (input1 <> input2 <> input3) + case parse preValueParser tkns of + Left pe -> failWith Nothing $ show pe + Right preVals -> calculateValue preVals === (out1 <> binaryOp <> negateValue out2) + +-- ----------------------------------------------------------------------------- + +tests :: IO Bool +tests = + checkSequential $$discover + diff --git a/cardano-cli/test/cardano-cli-test.hs b/cardano-cli/test/cardano-cli-test.hs index fa947e61591..62ada9a0059 100644 --- a/cardano-cli/test/cardano-cli-test.hs +++ b/cardano-cli/test/cardano-cli-test.hs @@ -4,6 +4,7 @@ import Hedgehog.Main (defaultMain) import qualified Test.Cli.FilePermissions import qualified Test.Cli.ITN +import qualified Test.Cli.MultiAssetParsing import qualified Test.Cli.Pioneers.Exercise1 import qualified Test.Cli.Pioneers.Exercise2 import qualified Test.Cli.Pioneers.Exercise3 @@ -14,6 +15,7 @@ main = defaultMain [ Test.Cli.FilePermissions.tests , Test.Cli.ITN.tests + , Test.Cli.MultiAssetParsing.tests , Test.Cli.Pioneers.Exercise1.tests , Test.Cli.Pioneers.Exercise2.tests , Test.Cli.Pioneers.Exercise3.tests