diff --git a/.changes/20261023_account_balance_intervals.yml b/.changes/20261023_account_balance_intervals.yml new file mode 100644 index 0000000000..9d23fa7278 --- /dev/null +++ b/.changes/20261023_account_balance_intervals.yml @@ -0,0 +1,5 @@ +project: cardano-cli +pr: 1483 +kind: + - feature +description: Add CIP-159 `--account-balance-interval` and `--starting-account-balance-interval` to Dijkstra `transaction build`, `build-raw`, and `build-estimate`; `sub-transaction build-raw` supports only the former. Values use `STAKE_ADDRESS{=N|>=L|=L,)) @@ -198,7 +200,63 @@ dijkstraBodyPairs dijkstraBodyPairs era body = case era of Exp.ConwayEra -> [] Exp.DijkstraEra -> - ["direct deposits" .= friendlyAccountAmounts (L.unDirectDeposits $ body ^. L.directDepositsTxBodyL)] + [ "direct deposits" .= friendlyAccountAmounts (L.unDirectDeposits $ body ^. L.directDepositsTxBodyL) + , "account balance intervals" + .= friendlyAccountBalanceIntervals (body ^. L.accountBalanceIntervalsTxBodyL) + , "starting account balance intervals" + .= friendlyAccountBalanceIntervals (body ^. L.startingAccountBalanceIntervalsTxBodyL) + , "sub-transactions" .= case toList (body ^. L.subTransactionsTxBodyL) of + [] -> Null + subTransactions -> array $ map renderSubTransaction subTransactions + ] + +renderSubTransaction + :: L.Tx C.SubTx (Exp.LedgerEra Exp.DijkstraEra) + -> Aeson.Value +renderSubTransaction subTx = + object + [ "id" .= serialiseToRawBytesHexText (Exp.getSignedSubTxId (Exp.SignedSubTx subTx)) + , "account balance intervals" + .= friendlyAccountBalanceIntervals (subTx ^. L.bodyTxL . L.accountBalanceIntervalsTxBodyL) + ] + +friendlyAccountBalanceIntervals :: L.AccountBalanceIntervals era -> Aeson.Value +friendlyAccountBalanceIntervals (L.AccountBalanceIntervals intervals) + | Map.null intervals = Null + | otherwise = + array + [ object $ + [ "address" .= serialiseAddress addr + , "interval" .= friendlyAccountBalanceInterval interval + ] + <> friendlyStakeAddress addr + | (account, interval) <- Map.toList intervals + , let addr = fromShelleyStakeAddr account + ] + +friendlyAccountBalanceInterval :: L.AccountBalanceInterval era -> Aeson.Value +friendlyAccountBalanceInterval = \case + L.AccountBalanceLowerBound (Inclusive lower) -> + object + [ "kind" .= String "lowerBound" + , "lower" .= friendlyLovelace lower + ] + L.AccountBalanceUpperBound (Exclusive upper) -> + object + [ "kind" .= String "upperBound" + , "upper" .= friendlyLovelace upper + ] + L.AccountBalanceBothBounds (Inclusive lower) (Exclusive upper) -> + object + [ "kind" .= String "bothBounds" + , "lower" .= friendlyLovelace lower + , "upper" .= friendlyLovelace upper + ] + L.AccountBalanceExact coin -> + object + [ "kind" .= String "exact" + , "coin" .= friendlyLovelace coin + ] renderCollateralInputs :: L.AlonzoEraTxBody (Exp.LedgerEra era) diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Common/Option.hs b/cardano-cli/src/Cardano/CLI/EraBased/Common/Option.hs index 38b39b97b5..d60b67b56e 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Common/Option.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Common/Option.hs @@ -30,7 +30,11 @@ import Cardano.CLI.EraBased.Script.Type import Cardano.CLI.EraBased.Script.Type qualified as ScriptT import Cardano.CLI.EraBased.Script.Vote.Type qualified as Voting import Cardano.CLI.EraBased.Script.Withdrawal.Type qualified as Withdrawal -import Cardano.CLI.EraBased.Transaction.Command (IncludeCurrentTreasuryValue (..)) +import Cardano.CLI.EraBased.Transaction.Command + ( IncludeCurrentTreasuryValue (..) + , accountBalanceIntervalOptionName + , startingAccountBalanceIntervalOptionName + ) import Cardano.CLI.Option.Flag import Cardano.CLI.Option.Flag.Type qualified as Z import Cardano.CLI.Orphan () @@ -40,7 +44,8 @@ import Cardano.CLI.Type.Common import Cardano.CLI.Type.Governance import Cardano.CLI.Type.Key import Cardano.CLI.Type.Key.VerificationKey -import Cardano.Ledger.BaseTypes (NonZero, nonZero) +import Cardano.Ledger.BaseTypes (Exclusive (..), Inclusive (..), NonZero, nonZero) +import Cardano.Ledger.Dijkstra.Scripts qualified as L import Cardano.Ledger.Plutus.Language qualified as L import Control.Monad (void, when) @@ -1516,6 +1521,66 @@ pDirectDeposits era = case era of <> Opt.help "Deposit ADA into a registered account. Amount in lovelace; each account may occur once." +-- | Comparison syntax deliberately follows the ledger's inclusive lower and +-- exclusive upper bounds. Exact assertions retain their distinct constructor. +parseAccountBalanceInterval :: P.Parser (StakeAddress, L.AccountBalanceInterval era) +parseAccountBalanceInterval = do + address <- parseAddressAny + interval <- + (L.AccountBalanceExact <$> parseExact) + <|> (L.AccountBalanceUpperBound . Exclusive <$> parseUpperBound) + <|> do + lower <- parseLowerBound + upper <- optional (P.char ',' *> parseUpperBound) + case upper of + Nothing -> pure $ L.AccountBalanceLowerBound (Inclusive lower) + Just upperBound + | lower < upperBound -> + pure $ L.AccountBalanceBothBounds (Inclusive lower) (Exclusive upperBound) + | otherwise -> fail "The lower bound must be strictly less than the upper bound" + pure (address, interval) + where + parseExact = P.char '=' *> parseLovelace + parseLowerBound = P.string ">=" *> parseLovelace + parseUpperBound = P.char '<' *> parseLovelace + +pAccountBalanceIntervals + :: Exp.Era era + -> Parser [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] +pAccountBalanceIntervals era = + pBalanceIntervals + era + accountBalanceIntervalOptionName + "Require the account balance to satisfy this interval before applying this transaction's withdrawals, certificates, or direct deposits. Top-level transactions check the balance after all sub-transactions; each sub-transaction checks the balance after any preceding sub-transactions." + +pStartingAccountBalanceIntervals + :: Exp.Era era + -> Parser [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] +pStartingAccountBalanceIntervals era = + pBalanceIntervals + era + startingAccountBalanceIntervalOptionName + "Check the account balance before the entire transaction, including all sub-transactions. Top-level only." + +pBalanceIntervals + :: Exp.Era era + -> String + -> String + -> Parser [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] +pBalanceIntervals era optionName timing = case era of + Exp.ConwayEra -> pure [] + Exp.DijkstraEra -> + many $ + Opt.option (readerFromParsecParser parseAccountBalanceInterval) $ + Opt.long optionName + <> Opt.metavar "STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER, Opt.help + ( timing + <> " Amounts in lovelace; lower bounds inclusive, upper bounds exclusive." + <> " Quote the argument so the shell treats < and > as literal characters, e.g. 'stake_test1...>=100,<200'." + <> " Each account may occur once per option." + ) + pWithdrawalForEra :: Exp.Era era -> BalanceTxExecUnits diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Command.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Command.hs index 3978fcb57c..0d1f31ac5e 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Command.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Command.hs @@ -25,6 +25,8 @@ module Cardano.CLI.EraBased.Transaction.Command , TransactionWitnessCmdArgs (..) , TxCborFormat (..) , IncludeCurrentTreasuryValue (..) + , accountBalanceIntervalOptionName + , startingAccountBalanceIntervalOptionName , renderTransactionCmds ) where @@ -37,10 +39,17 @@ import Cardano.CLI.EraBased.Script.Type import Cardano.CLI.Orphan () import Cardano.CLI.Type.Common import Cardano.CLI.Type.Governance +import Cardano.Ledger.Dijkstra.Scripts qualified as L import Data.Universe (Some) import Vary (Vary) +accountBalanceIntervalOptionName :: String +accountBalanceIntervalOptionName = "account-balance-interval" + +startingAccountBalanceIntervalOptionName :: String +startingAccountBalanceIntervalOptionName = "starting-account-balance-interval" + data TransactionCmds era = TransactionBuildRawCmd !(TransactionBuildRawCmdArgs era) | TransactionBuildCmd !(TransactionBuildCmdArgs era) @@ -86,6 +95,8 @@ data TransactionBuildRawCmdArgs era = TransactionBuildRawCmdArgs , withdrawals :: ![(StakeAddress, Coin, Maybe AnyNonAssetScript)] , directDeposits :: ![(StakeAddress, Coin)] -- ^ Direct ADA deposits as recipient stake addresses and amounts in lovelace. + , accountBalanceIntervals :: ![(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] + , startingAccountBalanceIntervals :: ![(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] , metadataSchema :: !TxMetadataJsonSchema , scriptFiles :: ![ScriptFile] -- ^ Auxiliary scripts @@ -154,6 +165,8 @@ data TransactionBuildCmdArgs era = TransactionBuildCmdArgs -- ^ Withdrawals with potential script witness , directDeposits :: ![(StakeAddress, Coin)] -- ^ Direct ADA deposits as recipient stake addresses and amounts in lovelace. + , accountBalanceIntervals :: ![(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] + , startingAccountBalanceIntervals :: ![(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] , metadataSchema :: !TxMetadataJsonSchema , scriptFiles :: ![ScriptFile] -- ^ Auxiliary scripts @@ -203,6 +216,8 @@ data TransactionBuildEstimateCmdArgs era = TransactionBuildEstimateCmdArgs -- ^ Withdrawals with potential script witness , directDeposits :: ![(StakeAddress, Coin)] -- ^ Direct ADA deposits as recipient stake addresses and amounts in lovelace. + , accountBalanceIntervals :: ![(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] + , startingAccountBalanceIntervals :: ![(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] , plutusCollateral :: !(Maybe Coin) -- ^ Total collateral , totalReferenceScriptSize :: !(Maybe ReferenceScriptSize) diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Internal/Common.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Internal/Common.hs index ce559700ab..da0ae7e411 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Internal/Common.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Internal/Common.hs @@ -10,6 +10,8 @@ module Cardano.CLI.EraBased.Transaction.Internal.Common , createTxMintValue , convertWithdrawals , convertDirectDeposits + , convertAccountBalanceIntervals + , findDuplicates , convertVotingProcedures , scriptInEraToSimpleScript , partitionSomeWitnesses @@ -29,6 +31,7 @@ import Cardano.CLI.Read import Cardano.CLI.Type.Common import Cardano.CLI.Type.Error.TxCmdError import Cardano.Ledger.Address qualified as L +import Cardano.Ledger.Dijkstra.Scripts qualified as L import Cardano.Ledger.Hashes (DataHash) import RIO hiding (toList) @@ -181,12 +184,30 @@ partitionSomeWitnesses = reversePartitionedWits . Foldable.foldl' go mempty AShelleyKeyWitness shelleyKeyWit -> (byronAcc, shelleyKeyWit : shelleyKeyAcc) +-- | Return repeated values in ascending order, with each value included only once. +findDuplicates :: Ord a => [a] -> [a] +findDuplicates values = [value | value : _ : _ <- List.group (List.sort values)] + -- | Reject duplicate recipients before converting deposits to the ledger map. convertDirectDeposits :: [(StakeAddress, Lovelace)] -> Either TxCmdError L.DirectDeposits convertDirectDeposits deposits = - case [address | address : _ : _ <- List.group (List.sort $ map fst deposits)] of + case findDuplicates (map fst deposits) of [] -> Right $ L.DirectDeposits $ Map.fromList [(toShelleyStakeAddr address, amount) | (address, amount) <- deposits] duplicates -> Left $ TxCmdDuplicateDirectDeposits duplicates + +-- | An account may occur once in each interval field. Never silently discard +-- assertions by constructing a map before checking for duplicate addresses. +convertAccountBalanceIntervals + :: String + -> [(StakeAddress, L.AccountBalanceInterval era)] + -> Either TxCmdError (L.AccountBalanceIntervals era) +convertAccountBalanceIntervals option intervals = + case findDuplicates (map fst intervals) of + [] -> + Right $ + L.AccountBalanceIntervals $ + Map.fromList [(toShelleyStakeAddr address, interval) | (address, interval) <- intervals] + duplicates -> Left $ TxCmdDuplicateAccountBalanceIntervals option duplicates diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Option.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Option.hs index 1c4455b7a1..7d6109f18c 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Option.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Option.hs @@ -216,6 +216,8 @@ pTransactionBuildCmd envCli = do <*> many (pCertificateFileForEra (Exp.useEra @era) AutoBalance) <*> many (pWithdrawalForEra (Exp.useEra @era) AutoBalance) <*> pDirectDeposits (Exp.useEra @era) + <*> pAccountBalanceIntervals (Exp.useEra @era) + <*> pStartingAccountBalanceIntervals (Exp.useEra @era) <*> pTxMetadataJsonSchema <*> many ( pScriptFor @@ -279,6 +281,8 @@ pTransactionBuildEstimateCmd _envCli = do <*> many (pCertificateFileForEra (Exp.useEra @era) ManualBalance) <*> many (pWithdrawalForEra (Exp.useEra @era) ManualBalance) <*> pDirectDeposits (Exp.useEra @era) + <*> pAccountBalanceIntervals (Exp.useEra @era) + <*> pStartingAccountBalanceIntervals (Exp.useEra @era) <*> optional pTotalCollateral <*> optional pReferenceScriptSize <*> pTxMetadataJsonSchema @@ -325,6 +329,8 @@ pTransactionBuildRaw = <*> many (pCertificateFileForEra (Exp.useEra @era) ManualBalance) <*> many (pWithdrawalForEra (Exp.useEra @era) ManualBalance) <*> pDirectDeposits (Exp.useEra @era) + <*> pAccountBalanceIntervals (Exp.useEra @era) + <*> pStartingAccountBalanceIntervals (Exp.useEra @era) <*> pTxMetadataJsonSchema <*> many (pScriptFor "auxiliary-script-file" Nothing "Filepath of auxiliary script(s)") <*> many pMetadataFile diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Run.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Run.hs index f3680ef4fc..92623a6a92 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Run.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Run.hs @@ -74,6 +74,7 @@ import Cardano.CLI.Type.Error.TxCmdError import Cardano.CLI.Type.Error.TxValidationError import Cardano.CLI.Type.Output (renderScriptCostsWithScriptHashesMap) import Cardano.Ledger.Api (allInputsTxBodyF, bodyTxL) +import Cardano.Ledger.Dijkstra.Scripts qualified as L import Cardano.Ledger.Hashes (DataHash) import Cardano.Prelude (putLByteString) @@ -149,6 +150,8 @@ runTransactionBuildCmd , certificates , withdrawals , directDeposits + , accountBalanceIntervals + , startingAccountBalanceIntervals , metadataSchema , scriptFiles , metadataFiles @@ -159,8 +162,16 @@ runTransactionBuildCmd , isCborOutCanonical , buildOutputOptions } = do - -- Reject invalid deposits before any node queries, including the network check. + -- Reject duplicate deposits and interval assertions before any node queries. void $ fromEitherCli $ convertDirectDeposits directDeposits + void $ + fromEitherCli $ + convertAccountBalanceIntervals accountBalanceIntervalOptionName accountBalanceIntervals + void $ + fromEitherCli $ + convertAccountBalanceIntervals + startingAccountBalanceIntervalOptionName + startingAccountBalanceIntervals checkNodeNetworkId nodeConnInfo let eon = convert currentEra @@ -312,6 +323,8 @@ runTransactionBuildCmd certsAndMaybeScriptWits withdrawalsAndMaybeScriptWits directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals requiredSigners txAuxScripts txMetadata @@ -398,6 +411,8 @@ runTransactionBuildEstimateCmd -- TODO change type , certificates , withdrawals , directDeposits + , accountBalanceIntervals + , startingAccountBalanceIntervals , metadataSchema , scriptFiles , metadataFiles @@ -491,6 +506,8 @@ runTransactionBuildEstimateCmd -- TODO change type certsAndMaybeScriptWits withdrawalsAndMaybeScriptWits directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals requiredSigners 0 txAuxScripts @@ -606,6 +623,8 @@ runTransactionBuildRawCmd , certificates , withdrawals , directDeposits + , accountBalanceIntervals + , startingAccountBalanceIntervals , metadataSchema , scriptFiles , metadataFiles @@ -696,6 +715,8 @@ runTransactionBuildRawCmd certsAndMaybeScriptWits withdrawalsAndMaybeScriptWits directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals requiredSigners txAuxScripts txMetadata @@ -743,6 +764,8 @@ runTxBuildRaw -> [(StakeAddress, Lovelace, Exp.AnyWitness (Exp.LedgerEra era))] -> [(StakeAddress, Lovelace)] -- ^ Direct ADA deposits as recipient stake addresses and amounts in lovelace. + -> [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] + -> [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] -> [Hash PaymentKey] -- ^ Required signers -> TxAuxScripts era @@ -771,6 +794,8 @@ runTxBuildRaw certsAndMaybeSriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners txAuxScripts txMetadata @@ -797,6 +822,8 @@ runTxBuildRaw certsAndMaybeSriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners fee txAuxScripts @@ -840,6 +867,8 @@ constructTxBodyContent -- ^ Withdrawals -> [(StakeAddress, Lovelace)] -- ^ Direct ADA deposits as recipient stake addresses and amounts in lovelace. + -> [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] + -> [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] -> [Hash PaymentKey] -- ^ Required signers -> Lovelace @@ -872,6 +901,8 @@ constructTxBodyContent certsAndMaybeScriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners fee txAuxScripts @@ -920,6 +951,12 @@ constructTxBodyContent let validatedCurrentTreasuryValue = unTxCurrentTreasuryValue <$> mCurrentTreasury validatedTreasuryDonation = unTxTreasuryDonation <$> mTreasuryDonation validatedDirectDeposits <- convertDirectDeposits directDeposits + validatedIntervals <- + convertAccountBalanceIntervals accountBalanceIntervalOptionName accountBalanceIntervals + validatedStartingIntervals <- + convertAccountBalanceIntervals + startingAccountBalanceIntervalOptionName + startingAccountBalanceIntervals let validatedWithdrawals = convertWithdrawals withdrawals return ( Exp.defaultTxBodyContent @@ -936,6 +973,8 @@ constructTxBodyContent & Exp.setTxAuxScripts auxScripts & Exp.setTxWithdrawals validatedWithdrawals & Exp.setTxDirectDeposits validatedDirectDeposits + & Exp.setTxAccountBalanceIntervals validatedIntervals + & Exp.setTxStartingAccountBalanceIntervals validatedStartingIntervals & Exp.setTxExtraKeyWits (Exp.TxExtraKeyWitnesses reqSigners) & maybe id (Exp.setTxProtocolParams . Exp.obtainCommonConstraints (Exp.useEra @era)) mPparams & Exp.setTxCertificates @@ -991,6 +1030,8 @@ runTxBuild -> [(StakeAddress, Lovelace, Exp.AnyWitness (Exp.LedgerEra era))] -> [(StakeAddress, Lovelace)] -- ^ Direct ADA deposits as recipient stake addresses and amounts in lovelace. + -> [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] + -> [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] -> [Hash PaymentKey] -- ^ Required signers -> TxAuxScripts era @@ -1021,6 +1062,8 @@ runTxBuild certsAndMaybeScriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners txAuxScripts txMetadata @@ -1090,6 +1133,8 @@ runTxBuild certsAndMaybeScriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners 0 txAuxScripts diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Command.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Command.hs index c8cc7721d4..415e51bf26 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Command.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Command.hs @@ -15,12 +15,14 @@ module Cardano.CLI.EraBased.Transaction.SubTransaction.Command where import Cardano.Api +import Cardano.Api.Experimental qualified as Exp import Cardano.Api.Ledger qualified as L import Cardano.CLI.EraBased.Script.Type import Cardano.CLI.Type.Common import Cardano.CLI.Type.Governance import Cardano.CLI.Type.Key (VerificationKeyOrHashOrFile) +import Cardano.Ledger.Dijkstra.Scripts qualified as L import Vary (Vary) @@ -52,6 +54,8 @@ data SubTransactionBuildRawCmdArgs = SubTransactionBuildRawCmdArgs , withdrawals :: ![(StakeAddress, Coin, Maybe AnyNonAssetScript)] , directDeposits :: ![(StakeAddress, Coin)] -- ^ Direct ADA deposits as recipient stake addresses and amounts in lovelace. + , accountBalanceIntervals + :: ![(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra Exp.DijkstraEra))] , metadataSchema :: !TxMetadataJsonSchema , scriptFiles :: ![ScriptFile] -- ^ Auxiliary scripts diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Option.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Option.hs index 349aecff64..64f7c3486e 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Option.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Option.hs @@ -113,6 +113,7 @@ pSubTransactionBuildRaw = ManualBalance ) <*> pDirectDeposits Exp.DijkstraEra + <*> pAccountBalanceIntervals Exp.DijkstraEra <*> pTxMetadataJsonSchema <*> many (pScriptFor "auxiliary-script-file" Nothing "Filepath of auxiliary script(s)") <*> many pMetadataFile diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Run.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Run.hs index 9a64e22ab1..f3957c1575 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Run.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Run.hs @@ -38,6 +38,7 @@ import Cardano.CLI.EraBased.Script.Read.Common (readScriptDataOrFile) import Cardano.CLI.EraBased.Script.Spend.Read import Cardano.CLI.EraBased.Script.Vote.Read import Cardano.CLI.EraBased.Script.Withdrawal.Read +import Cardano.CLI.EraBased.Transaction.Command (accountBalanceIntervalOptionName) import Cardano.CLI.EraBased.Transaction.Internal.Common import Cardano.CLI.EraBased.Transaction.SubTransaction.Command qualified as Cmd import Cardano.CLI.Json.Encode qualified as Json @@ -58,7 +59,6 @@ import RIO hiding (toList) import Data.ByteString.Char8 qualified as BS import Data.ByteString.Lazy.Char8 qualified as LBS import Data.Foldable qualified as Foldable -import Data.List qualified as List import Data.Map.Strict qualified as Map import Data.OSet.Strict (OSet) import Data.OSet.Strict qualified as OSet @@ -81,7 +81,7 @@ readSignedSubTransactions subTransactionFiles = do signedSubTxs <- forM subTransactionFiles $ \(File subTxPath) -> do subTxFile <- liftIO $ fileOrPipe subTxPath fromEitherIOCli $ first TxCmdTextEnvError <$> readFileSignedSubTx subTxFile - case [i | (i : _ : _) <- List.group (List.sort (map Exp.getSignedSubTxId signedSubTxs))] of + case findDuplicates (map Exp.getSignedSubTxId signedSubTxs) of duplicateId : _ -> throwCliError $ TxCmdDuplicateSubTransaction duplicateId [] -> pure signedSubTxs @@ -99,6 +99,7 @@ runSubTransactionBuildRawCmd , certificates , withdrawals , directDeposits + , accountBalanceIntervals , metadataSchema , scriptFiles , metadataFiles @@ -156,14 +157,11 @@ runSubTransactionBuildRawCmd guardWitnesses <- mapM readGuard guards let guardCredentials = [credential | (credential, _, _) <- guardWitnesses] - duplicates = [credential | credential : _ : _ <- List.group (List.sort guardCredentials)] + duplicates = findDuplicates guardCredentials unless (null duplicates) $ throwCliError $ TxCmdDuplicateGuards duplicates topLevelGuardEntries <- mapM readTopLevelGuard topLevelGuards - let duplicateTopLevelGuards = - [ credential - | credential : _ : _ <- List.group (List.sort (map fst topLevelGuardEntries)) - ] + let duplicateTopLevelGuards = findDuplicates (map fst topLevelGuardEntries) unless (null duplicateTopLevelGuards) $ throwCliError $ TxCmdDuplicateTopLevelGuards duplicateTopLevelGuards @@ -182,6 +180,7 @@ runSubTransactionBuildRawCmd certsAndMaybeScriptWits withdrawalsAndMaybeScriptWits directDeposits + accountBalanceIntervals txAuxScripts txMetadata votingProceduresAndMaybeScriptWits @@ -353,6 +352,7 @@ constructSubTx -- ^ Withdrawals -> [(StakeAddress, Lovelace)] -- ^ Direct ADA deposits as recipient stake addresses and amounts in lovelace. + -> [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] -> TxAuxScripts era -> TxMetadataInEra era -> [(VotingProcedures era, Exp.AnyWitness (Exp.LedgerEra era))] @@ -377,6 +377,7 @@ constructSubTx certsAndMaybeScriptWits withdrawals directDeposits + accountBalanceIntervals txAuxScripts txMetadata votingProcedures @@ -423,6 +424,8 @@ constructSubTx TxMetadataInEra _ mDat -> mDat validatedDirectDeposits <- convertDirectDeposits directDeposits + validatedIntervals <- + convertAccountBalanceIntervals accountBalanceIntervalOptionName accountBalanceIntervals validatedGuards <- first TxCmdGuardsError $ obtainCommonConstraints (Exp.useEra @era) $ @@ -443,6 +446,7 @@ constructSubTx & Exp.setTxAuxScripts auxScripts & Exp.setTxWithdrawals (convertWithdrawals withdrawals) & Exp.setTxDirectDeposits validatedDirectDeposits + & Exp.setTxAccountBalanceIntervals validatedIntervals & maybe id (Exp.setTxProtocolParams . Exp.obtainCommonConstraints (Exp.useEra @era)) mPparams & Exp.setTxCertificates (Exp.mkTxCertificates Exp.useEra certsAndMaybeScriptWits) diff --git a/cardano-cli/src/Cardano/CLI/Type/Error/TxCmdError.hs b/cardano-cli/src/Cardano/CLI/Type/Error/TxCmdError.hs index e808e5efb6..f94c493a25 100644 --- a/cardano-cli/src/Cardano/CLI/Type/Error/TxCmdError.hs +++ b/cardano-cli/src/Cardano/CLI/Type/Error/TxCmdError.hs @@ -87,6 +87,7 @@ data TxCmdError | TxCmdSubTxUnsupportedPlutusLanguage !L.Language | TxCmdSubTxByronWitnessUnsupported | TxCmdDuplicateDirectDeposits ![StakeAddress] + | TxCmdDuplicateAccountBalanceIntervals !String ![StakeAddress] | TxCmdDuplicateGuards ![L.Credential L.Guard] | TxCmdGuardsError !Exp.TxGuardsError | TxCmdPlutusGuardMissingRedeemer !(L.Credential L.Guard) @@ -221,6 +222,11 @@ renderTxCmdError = \case "Sub-transactions require Plutus V4 for script execution; found " <> pshow language <> "." TxCmdSubTxByronWitnessUnsupported -> "Byron (bootstrap) witnesses are not supported for sub-transactions." + TxCmdDuplicateAccountBalanceIntervals option addresses -> + "Duplicate accounts in --" + <> pretty option + <> ": " + <> hsep (map (pretty . serialiseAddress) addresses) TxCmdDuplicateDirectDeposits addresses -> "Duplicate direct-deposit accounts: " <> hsep (map (pretty . serialiseAddress) addresses) diff --git a/cardano-cli/test/cardano-cli-golden/Test/Golden/Debug/TransactionView.hs b/cardano-cli/test/cardano-cli-golden/Test/Golden/Debug/TransactionView.hs index f486b09abc..f4225ba3a7 100644 --- a/cardano-cli/test/cardano-cli-golden/Test/Golden/Debug/TransactionView.hs +++ b/cardano-cli/test/cardano-cli-golden/Test/Golden/Debug/TransactionView.hs @@ -2,7 +2,7 @@ module Test.Golden.Debug.TransactionView where -import Control.Monad (void) +import Control.Monad (void, when) import System.FilePath (()) import Test.Cardano.CLI.Util (execCardanoCLI, watchdogProp) @@ -92,3 +92,76 @@ goldenDijkstraDirectDeposits format = , "--output-" ++ format ] H.diffVsGoldenFile result $ goldenDir "dijkstra/transaction-view-direct-deposits." ++ format + +hprop_golden_debug_transaction_view_dijkstra_intervals_json :: Property +hprop_golden_debug_transaction_view_dijkstra_intervals_json = goldenDijkstraIntervals "json" False + +hprop_golden_debug_transaction_view_dijkstra_intervals_yaml :: Property +hprop_golden_debug_transaction_view_dijkstra_intervals_yaml = goldenDijkstraIntervals "yaml" False + +hprop_golden_debug_transaction_view_dijkstra_signed_intervals_json :: Property +hprop_golden_debug_transaction_view_dijkstra_signed_intervals_json = goldenDijkstraIntervals "json" True + +hprop_golden_debug_transaction_view_dijkstra_signed_intervals_yaml :: Property +hprop_golden_debug_transaction_view_dijkstra_signed_intervals_yaml = goldenDijkstraIntervals "yaml" True + +-- Include every interval constructor, the same account at different levels, +-- and a second sub-transaction without intervals to exercise ordering and nulls. +goldenDijkstraIntervals :: String -> Bool -> Property +goldenDijkstraIntervals format signed = + watchdogProp . propertyOnce . H.moduleWorkspace "tmp" $ \tempDir -> do + bodyFile <- H.noteTempFile tempDir "tx.body" + signedFile <- H.noteTempFile tempDir "tx.signed" + let account = "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99" + otherAccount = "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl" + input ix = "63e6a9a8e58e48cc025cae04daaed9d36fc7b70bc292721d9f5057ae37b24981#" ++ ix + output = "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg+5000000" + key = inputDir "dijkstra/keys/utxo_keys/signing_key" + buildSub ix args = do + body <- H.noteTempFile tempDir $ "sub-" ++ ix ++ ".body" + out <- H.noteTempFile tempDir $ "sub-" ++ ix ++ ".signed" + void $ + execCardanoCLI $ + concat + [ ["dijkstra", "sub-transaction", "build-raw"] + , ["--tx-in", input ix, "--tx-out", output, "--out-file", body] + , args + ] + void $ + execCardanoCLI $ + concat + [ ["dijkstra", "sub-transaction", "sign"] + , ["--sub-tx-file", body, "--signing-key-file", key, "--out-file", out] + ] + pure out + firstSub <- + buildSub "1" $ + concat + [ ["--account-balance-interval", account ++ ">=100,<200"] + , ["--account-balance-interval", otherAccount ++ "=42"] + ] + secondSub <- buildSub "2" [] + void $ + execCardanoCLI $ + concat + [ ["dijkstra", "transaction", "build-raw"] + , ["--tx-in", input "0", "--tx-out", output, "--fee", "200000"] + , ["--account-balance-interval", account ++ ">=100"] + , ["--account-balance-interval", otherAccount ++ "<200"] + , ["--starting-account-balance-interval", account ++ "=300"] + , ["--starting-account-balance-interval", otherAccount ++ ">=0,<400"] + , ["--signed-sub-tx-file", firstSub, "--signed-sub-tx-file", secondSub] + , ["--out-file", bodyFile] + ] + when signed $ + void $ + execCardanoCLI $ + concat + [ ["dijkstra", "transaction", "sign"] + , ["--tx-body-file", bodyFile, "--signing-key-file", key, "--out-file", signedFile] + ] + let (fileFlag, file, suffix) = + if signed then ("--tx-file", signedFile, "-signed") else ("--tx-body-file", bodyFile, "") + result <- execCardanoCLI ["debug", "transaction", "view", fileFlag, file, "--output-" ++ format] + H.diffVsGoldenFile result $ + goldenDir "dijkstra/transaction-view-intervals" ++ suffix ++ "." ++ format diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-direct-deposits.json b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-direct-deposits.json index 3e8bfcc102..6dcc708064 100644 --- a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-direct-deposits.json +++ b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-direct-deposits.json @@ -1,4 +1,5 @@ { + "account balance intervals": null, "auxiliary scripts": null, "certificates": null, "collateral inputs": [], @@ -45,6 +46,8 @@ "required signers (payment key hashes needed for scripts)": null, "return collateral": null, "scripts": [], + "starting account balance intervals": null, + "sub-transactions": null, "total collateral": null, "treasuryDonation": 0, "validity range": { diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-direct-deposits.yaml b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-direct-deposits.yaml index af8357846f..7565027b4d 100644 --- a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-direct-deposits.yaml +++ b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-direct-deposits.yaml @@ -1,3 +1,4 @@ +account balance intervals: null auxiliary scripts: null certificates: null collateral inputs: [] @@ -34,6 +35,8 @@ reference inputs: [] required signers (payment key hashes needed for scripts): null return collateral: null scripts: [] +starting account balance intervals: null +sub-transactions: null total collateral: null treasuryDonation: 0 validity range: diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals-signed.json b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals-signed.json new file mode 100644 index 0000000000..23b91c28e2 --- /dev/null +++ b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals-signed.json @@ -0,0 +1,120 @@ +{ + "account balance intervals": [ + { + "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", + "interval": { + "kind": "lowerBound", + "lower": "100 Lovelace" + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "kind": "upperBound", + "upper": "200 Lovelace" + }, + "network": "Testnet", + "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" + } + ], + "auxiliary scripts": null, + "certificates": null, + "collateral inputs": [], + "currentTreasuryValue": null, + "datums": [], + "direct deposits": null, + "era": "Dijkstra", + "fee": "200000 Lovelace", + "governance actions": [], + "inputs": [ + "63e6a9a8e58e48cc025cae04daaed9d36fc7b70bc292721d9f5057ae37b24981#0" + ], + "metadata": null, + "mint": null, + "outputs": [ + { + "address": "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg", + "address era": "Shelley", + "amount": { + "lovelace": 5000000 + }, + "datum": null, + "network": "Testnet", + "payment credential key hash": "5ebab53d2d9785718cf4105fe7a9e5d6e271e6c4aa0ad934082c1938", + "reference script": null, + "stake reference": null + } + ], + "redeemers": [], + "reference inputs": [], + "required signers (payment key hashes needed for scripts)": null, + "return collateral": null, + "scripts": [], + "starting account balance intervals": [ + { + "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", + "interval": { + "coin": "300 Lovelace", + "kind": "exact" + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "kind": "bothBounds", + "lower": "0 Lovelace", + "upper": "400 Lovelace" + }, + "network": "Testnet", + "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" + } + ], + "sub-transactions": [ + { + "account balance intervals": [ + { + "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", + "interval": { + "kind": "bothBounds", + "lower": "100 Lovelace", + "upper": "200 Lovelace" + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "coin": "42 Lovelace", + "kind": "exact" + }, + "network": "Testnet", + "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" + } + ], + "id": "61ba2d8821e417661702f29f7ce1d3aea71fabd53d3594be3e87ff9ddb647188" + }, + { + "account balance intervals": null, + "id": "c45b108f5a31036036618ef8213be0df21e556c7fd53e35d0254ba8fe959d261" + } + ], + "total collateral": null, + "treasuryDonation": 0, + "validity range": { + "lower bound": null, + "upper bound": null + }, + "voters": {}, + "withdrawals": null, + "witnesses": [ + { + "key": "VKey (VerKeyEd25519DSIGN \"48d968a47923d491cbba6548e61cc3e347e658d1818d7bea90cf7d04ca13dbd1\")", + "signature": "SignedDSIGN (SigEd25519DSIGN \"81e5b2b75f6370e1255ea705f5a1ff7f41e35e7716b0afbfff54b545028b4a4607babfcc4e2272f3fe53ba86234d14715f3bb5a168f4314dd51dd88e5564b807\")" + } + ] +} \ No newline at end of file diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals-signed.yaml b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals-signed.yaml new file mode 100644 index 0000000000..da11633789 --- /dev/null +++ b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals-signed.yaml @@ -0,0 +1,83 @@ +account balance intervals: +- address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 + interval: + kind: lowerBound + lower: 100 Lovelace + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 +- address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + kind: upperBound + upper: 200 Lovelace + network: Testnet + stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 +auxiliary scripts: null +certificates: null +collateral inputs: [] +currentTreasuryValue: null +datums: [] +direct deposits: null +era: Dijkstra +fee: 200000 Lovelace +governance actions: [] +inputs: +- 63e6a9a8e58e48cc025cae04daaed9d36fc7b70bc292721d9f5057ae37b24981#0 +metadata: null +mint: null +outputs: +- address: addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg + address era: Shelley + amount: + lovelace: 5000000 + datum: null + network: Testnet + payment credential key hash: 5ebab53d2d9785718cf4105fe7a9e5d6e271e6c4aa0ad934082c1938 + reference script: null + stake reference: null +redeemers: [] +reference inputs: [] +required signers (payment key hashes needed for scripts): null +return collateral: null +scripts: [] +starting account balance intervals: +- address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 + interval: + coin: 300 Lovelace + kind: exact + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 +- address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + kind: bothBounds + lower: 0 Lovelace + upper: 400 Lovelace + network: Testnet + stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 +sub-transactions: +- account balance intervals: + - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 + interval: + kind: bothBounds + lower: 100 Lovelace + upper: 200 Lovelace + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 + - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + coin: 42 Lovelace + kind: exact + network: Testnet + stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 + id: 61ba2d8821e417661702f29f7ce1d3aea71fabd53d3594be3e87ff9ddb647188 +- account balance intervals: null + id: c45b108f5a31036036618ef8213be0df21e556c7fd53e35d0254ba8fe959d261 +total collateral: null +treasuryDonation: 0 +validity range: + lower bound: null + upper bound: null +voters: {} +withdrawals: null +witnesses: +- key: VKey (VerKeyEd25519DSIGN "48d968a47923d491cbba6548e61cc3e347e658d1818d7bea90cf7d04ca13dbd1") + signature: SignedDSIGN (SigEd25519DSIGN "81e5b2b75f6370e1255ea705f5a1ff7f41e35e7716b0afbfff54b545028b4a4607babfcc4e2272f3fe53ba86234d14715f3bb5a168f4314dd51dd88e5564b807") diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals.json b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals.json new file mode 100644 index 0000000000..1c6bdaec70 --- /dev/null +++ b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals.json @@ -0,0 +1,114 @@ +{ + "account balance intervals": [ + { + "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", + "interval": { + "kind": "lowerBound", + "lower": "100 Lovelace" + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "kind": "upperBound", + "upper": "200 Lovelace" + }, + "network": "Testnet", + "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" + } + ], + "auxiliary scripts": null, + "certificates": null, + "collateral inputs": [], + "currentTreasuryValue": null, + "datums": [], + "direct deposits": null, + "era": "Dijkstra", + "fee": "200000 Lovelace", + "governance actions": [], + "inputs": [ + "63e6a9a8e58e48cc025cae04daaed9d36fc7b70bc292721d9f5057ae37b24981#0" + ], + "metadata": null, + "mint": null, + "outputs": [ + { + "address": "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg", + "address era": "Shelley", + "amount": { + "lovelace": 5000000 + }, + "datum": null, + "network": "Testnet", + "payment credential key hash": "5ebab53d2d9785718cf4105fe7a9e5d6e271e6c4aa0ad934082c1938", + "reference script": null, + "stake reference": null + } + ], + "redeemers": [], + "reference inputs": [], + "required signers (payment key hashes needed for scripts)": null, + "return collateral": null, + "scripts": [], + "starting account balance intervals": [ + { + "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", + "interval": { + "coin": "300 Lovelace", + "kind": "exact" + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "kind": "bothBounds", + "lower": "0 Lovelace", + "upper": "400 Lovelace" + }, + "network": "Testnet", + "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" + } + ], + "sub-transactions": [ + { + "account balance intervals": [ + { + "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", + "interval": { + "kind": "bothBounds", + "lower": "100 Lovelace", + "upper": "200 Lovelace" + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "coin": "42 Lovelace", + "kind": "exact" + }, + "network": "Testnet", + "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" + } + ], + "id": "61ba2d8821e417661702f29f7ce1d3aea71fabd53d3594be3e87ff9ddb647188" + }, + { + "account balance intervals": null, + "id": "c45b108f5a31036036618ef8213be0df21e556c7fd53e35d0254ba8fe959d261" + } + ], + "total collateral": null, + "treasuryDonation": 0, + "validity range": { + "lower bound": null, + "upper bound": null + }, + "voters": {}, + "withdrawals": null +} \ No newline at end of file diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals.yaml b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals.yaml new file mode 100644 index 0000000000..ba445dd94f --- /dev/null +++ b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals.yaml @@ -0,0 +1,80 @@ +account balance intervals: +- address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 + interval: + kind: lowerBound + lower: 100 Lovelace + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 +- address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + kind: upperBound + upper: 200 Lovelace + network: Testnet + stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 +auxiliary scripts: null +certificates: null +collateral inputs: [] +currentTreasuryValue: null +datums: [] +direct deposits: null +era: Dijkstra +fee: 200000 Lovelace +governance actions: [] +inputs: +- 63e6a9a8e58e48cc025cae04daaed9d36fc7b70bc292721d9f5057ae37b24981#0 +metadata: null +mint: null +outputs: +- address: addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg + address era: Shelley + amount: + lovelace: 5000000 + datum: null + network: Testnet + payment credential key hash: 5ebab53d2d9785718cf4105fe7a9e5d6e271e6c4aa0ad934082c1938 + reference script: null + stake reference: null +redeemers: [] +reference inputs: [] +required signers (payment key hashes needed for scripts): null +return collateral: null +scripts: [] +starting account balance intervals: +- address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 + interval: + coin: 300 Lovelace + kind: exact + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 +- address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + kind: bothBounds + lower: 0 Lovelace + upper: 400 Lovelace + network: Testnet + stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 +sub-transactions: +- account balance intervals: + - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 + interval: + kind: bothBounds + lower: 100 Lovelace + upper: 200 Lovelace + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 + - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + coin: 42 Lovelace + kind: exact + network: Testnet + stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 + id: 61ba2d8821e417661702f29f7ce1d3aea71fabd53d3594be3e87ff9ddb647188 +- account balance intervals: null + id: c45b108f5a31036036618ef8213be0df21e556c7fd53e35d0254ba8fe959d261 +total collateral: null +treasuryDonation: 0 +validity range: + lower bound: null + upper bound: null +voters: {} +withdrawals: null diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction/build-raw-plutus-v4-script-witnesses.out b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction/build-raw-plutus-v4-script-witnesses.out index d90dcdf76b..b64b5e44a6 100644 --- a/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction/build-raw-plutus-v4-script-witnesses.out +++ b/cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction/build-raw-plutus-v4-script-witnesses.out @@ -1,3 +1,4 @@ +account balance intervals: null auxiliary scripts: null certificates: - Stake address registration: @@ -111,6 +112,8 @@ scripts: script: '46450101002499' type: plutus script hash: 31a78786b5989dc6fd2d4ab15b297069e94106ecacceb8eb29e1b681 +starting account balance intervals: null +sub-transactions: null total collateral: null treasuryDonation: 0 validity range: diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/help.cli b/cardano-cli/test/cardano-cli-golden/files/golden/help.cli index 429eafe95d..300da6da21 100644 --- a/cardano-cli/test/cardano-cli-golden/files/golden/help.cli +++ b/cardano-cli/test/cardano-cli-golden/files/golden/help.cli @@ -5362,6 +5362,7 @@ Usage: cardano-cli dijkstra sub-transaction build-raw (--tx-in TX_IN --withdrawal-reference-tx-in-execution-units (INT, INT) ]] [--direct-deposit STAKE_ADDRESS+LOVELACE] + [--account-balance-interval STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER, as literal characters, e.g. + 'stake_test1...>=100,<200'. Each account may occur + once per option. --json-metadata-no-schema Use the "no schema" conversion from JSON to tx metadata (default). diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build-estimate.cli b/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build-estimate.cli index 4da2de3f51..038b1991e3 100644 --- a/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build-estimate.cli +++ b/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build-estimate.cli @@ -116,6 +116,8 @@ Usage: cardano-cli dijkstra transaction build-estimate --shelley-key-witnesses I --withdrawal-reference-tx-in-execution-units (INT, INT) ]] [--direct-deposit STAKE_ADDRESS+LOVELACE] + [--account-balance-interval STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER, as literal characters, e.g. + 'stake_test1...>=100,<200'. Each account may occur + once per option. + --starting-account-balance-interval STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER, as literal characters, + e.g. 'stake_test1...>=100,<200'. Each account may + occur once per option. --tx-total-collateral INTEGER The total amount of collateral that will be collected as fees in the event of a Plutus script failure. Must diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build-raw.cli b/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build-raw.cli index 88fcb2eac8..6211603119 100644 --- a/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build-raw.cli +++ b/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build-raw.cli @@ -112,6 +112,8 @@ Usage: cardano-cli dijkstra transaction build-raw (--tx-in TX_IN --withdrawal-reference-tx-in-execution-units (INT, INT) ]] [--direct-deposit STAKE_ADDRESS+LOVELACE] + [--account-balance-interval STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER, as literal characters, e.g. + 'stake_test1...>=100,<200'. Each account may occur + once per option. + --starting-account-balance-interval STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER, as literal characters, + e.g. 'stake_test1...>=100,<200'. Each account may + occur once per option. --json-metadata-no-schema Use the "no schema" conversion from JSON to tx metadata (default). diff --git a/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build.cli b/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build.cli index 529aa239f9..0474854bfb 100644 --- a/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build.cli +++ b/cardano-cli/test/cardano-cli-golden/files/golden/help/dijkstra_transaction_build.cli @@ -109,6 +109,8 @@ Usage: cardano-cli dijkstra transaction build ) ]] [--direct-deposit STAKE_ADDRESS+LOVELACE] + [--account-balance-interval STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER,=LOWER|=LOWER,=LOWER|=LOWER, as literal characters, e.g. + 'stake_test1...>=100,<200'. Each account may occur + once per option. + --starting-account-balance-interval STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER, as literal characters, + e.g. 'stake_test1...>=100,<200'. Each account may + occur once per option. --json-metadata-no-schema Use the "no schema" conversion from JSON to tx metadata (default). diff --git a/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/AccountBalanceInterval.hs b/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/AccountBalanceInterval.hs new file mode 100644 index 0000000000..1d45d00fa3 --- /dev/null +++ b/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/AccountBalanceInterval.hs @@ -0,0 +1,234 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | CIP-159 account balance interval tests. +module Test.Cli.Transaction.AccountBalanceInterval where + +import Cardano.Api +import Cardano.Api.Experimental qualified as Exp +import Cardano.Api.Ledger qualified as L + +import Cardano.Ledger.BaseTypes (Exclusive (..), Inclusive (..)) +import Cardano.Ledger.Coin qualified as L +import Cardano.Ledger.Core qualified as L +import Cardano.Ledger.Dijkstra.Scripts qualified as L +import Cardano.Ledger.Dijkstra.TxBody qualified as L + +import Control.Monad (forM_, void) +import Control.Monad.Trans.Resource (ResourceT) +import Data.Aeson qualified as Aeson +import Data.Foldable (toList) +import Data.List (isInfixOf) +import Data.Map.Strict qualified as Map +import Data.Text qualified as Text +import Lens.Micro ((&), (.~), (^.)) +import System.Directory (doesFileExist) +import System.Exit (ExitCode (..)) + +import Test.Cardano.CLI.Util + +import Hedgehog (Property, (===)) +import Hedgehog qualified as H +import Hedgehog.Extras qualified as H +import Test.Cli.Transaction.DirectDeposit (account, accountAddress, otherAccount) +import Test.Cli.Transaction.SubTransaction + ( buildSubTransaction + , readDijkstraTransaction + , signSubTransaction + , signingKey + , subTransactionArgs + , transactionArgs + , txIn + ) + +-- Exercise the CLI, ledger encoding, signing and embedding together: accepting +-- a flag is insufficient if a builder later discards the corresponding field. +hprop_account_balance_intervals_roundtrip :: Property +hprop_account_balance_intervals_roundtrip = + watchdogProp . propertyOnce $ H.moduleWorkspace "tmp" $ \tempDir -> do + outFile <- H.noteTempFile tempDir "tx.body" + subFile <- H.noteTempFile tempDir "sub.body" + signedFile <- H.noteTempFile tempDir "sub.signed" + address <- accountAddress + otherAddress <- + toShelleyStakeAddr <$> H.evalMaybe (deserialiseAddress AsStakeAddress $ Text.pack otherAccount) + let amount = 100 + otherAmount = 200 + cases = + [ exact 0 + , exact maxAmount + , exact amount + , (">=" ++ show amount, L.AccountBalanceLowerBound (Inclusive $ L.Coin amount)) + , ("<" ++ show otherAmount, L.AccountBalanceUpperBound (Exclusive $ L.Coin otherAmount)) + , between amount otherAmount + , between 0 1 + , between (maxAmount - 1) maxAmount + , between 0 maxAmount + ] + forM_ cases $ \(expression, interval) -> do + H.noteShow_ expression + let args = intervalArgs "--account-balance-interval" [(account, expression), (otherAccount, "=42")] + startingArgs = + intervalArgs "--starting-account-balance-interval" [(account, "=300"), (otherAccount, expression)] + expected = + L.AccountBalanceIntervals $ + Map.fromList [(address, interval), (otherAddress, L.AccountBalanceExact 42)] + expectedStarting = + L.AccountBalanceIntervals $ + Map.fromList [(address, L.AccountBalanceExact 300), (otherAddress, interval)] + Exp.UnsignedSubTx sub <- buildSubTransaction subFile args + sub ^. L.bodyTxL . L.accountBalanceIntervalsTxBodyL === expected + signSubTransaction subFile signingKey signedFile + void $ + execCardanoCLI $ + transactionArgs "dijkstra" + ++ args + ++ startingArgs + ++ ["--signed-sub-tx-file", signedFile, "--out-file", outFile] + ShelleyTx _ tx <- readDijkstraTransaction outFile + let body = tx ^. L.bodyTxL + body ^. L.accountBalanceIntervalsTxBodyL === expected + body ^. L.startingAccountBalanceIntervalsTxBodyL === expectedStarting + map (^. L.bodyTxL . L.accountBalanceIntervalsTxBodyL) (toList $ body ^. L.subTransactionsTxBodyL) + === [expected] + where + maxAmount = 18446744073709551615 + exact amount = ("=" ++ show amount, L.AccountBalanceExact $ L.Coin amount) + between lower upper = + ( ">=" ++ show lower ++ ",<" ++ show upper + , L.AccountBalanceBothBounds (Inclusive $ L.Coin lower) (Exclusive $ L.Coin upper) + ) + +hprop_account_balance_intervals_reject_invalid_arguments :: Property +hprop_account_balance_intervals_reject_invalid_arguments = + watchdogProp . propertyOnce $ H.moduleWorkspace "tmp" $ \tempDir -> do + outFile <- H.noteTempFile tempDir "unused" + address <- accountAddress + let hexAddress = Text.unpack $ serialiseToRawBytesHexText (fromShelleyStakeAddr address) + invalid = + map + (account ++) + [ "" + , "=" + , "=-1" + , "=18446744073709551616" + , ">=18446744073709551616" + , "<18446744073709551616" + , "=1garbage" + , "==1" + , ">1" + , "<=2" + , ">=1,<=2" + , "<2,>=1" + , ">=1,<" + , ">=1,<2,=1" + , " =1" + , ">= 1" + , ">=1, <2" + , "=1 " + , "+100+null" + , ">=null, do + forM_ invalid $ \argument -> + void $ assertRejected outFile $ base ++ [flag, argument] + forM_ + [ ">=0,<0" + , ">=100,<100" + , ">=200,<100" + , ">=18446744073709551615,<0" + , ">=18446744073709551615,<18446744073709551615" + ] + $ \expression -> do + stderr <- assertRejected outFile $ base ++ intervalArgs flag [(account, expression)] + H.assertWith stderr ("The lower bound must be strictly less than the upper bound" `isInfixOf`) + stderr <- assertRejected outFile $ base ++ intervalArgs flag [(account, ">=1"), (account, "<2")] + H.assertWith stderr (("Duplicate accounts in " ++ flag ++ ": " ++ account) `isInfixOf`) + +hprop_account_balance_intervals_gating :: Property +hprop_account_balance_intervals_gating = + watchdogProp . propertyOnce $ do + forM_ ["build", "build-raw", "build-estimate"] $ \command -> do + help <- execCardanoCLI ["dijkstra", "transaction", command, "--help"] + forM_ ["--account-balance-interval", "--starting-account-balance-interval"] $ \flag -> do + H.assertWith help (flag `isInfixOf`) + forM_ ["conway", "latest"] $ \era -> + assertInvalidOption flag [era, "transaction", command] + assertInvalidOption "--starting-account-balance-interval" subTransactionArgs + +hprop_account_balance_intervals_validate_before_node_query :: Property +hprop_account_balance_intervals_validate_before_node_query = + watchdogProp . propertyOnce $ H.moduleWorkspace "tmp" $ \tempDir -> do + outFile <- H.noteTempFile tempDir "unused" + socketPath <- H.noteTempFile tempDir "missing.socket" + forM_ ["--account-balance-interval", "--starting-account-balance-interval"] $ \flag -> do + stderr <- + assertRejected outFile $ + concat + [ ["dijkstra", "transaction", "build"] + , ["--testnet-magic", "42", "--socket-path", socketPath] + , ["--tx-in", txIn] + , ["--change-address", "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg"] + , intervalArgs flag [(account, "=1"), (account, "=2")] + ] + H.assertWith stderr (("Duplicate accounts in " ++ flag ++ ": " ++ account) `isInfixOf`) + +hprop_account_balance_intervals_build_estimate :: Property +hprop_account_balance_intervals_build_estimate = + watchdogProp . propertyOnce $ H.moduleWorkspace "tmp" $ \tempDir -> do + paramsFile <- H.noteTempFile tempDir "params.json" + outFile <- H.noteTempFile tempDir "tx.body" + let inputValue = Coin 100000000 + params = + (L.PParams L.emptyPParamsIdentity :: L.PParams (Exp.LedgerEra Exp.DijkstraEra)) + & L.ppMaxTxSizeL .~ 16384 + & L.ppTxFeePerByteL .~ L.CoinPerByte (L.CompactCoin 44) + & L.ppTxFeeFixedL .~ 155381 + H.evalIO $ Aeson.encodeFile paramsFile params + void $ + execCardanoCLI $ + concat + [ ["dijkstra", "transaction", "build-estimate"] + , ["--tx-in", txIn, "--total-utxo-value", show (L.unCoin inputValue)] + , ["--shelley-key-witnesses", "1", "--protocol-params-file", paramsFile] + , ["--change-address", "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg"] + , intervalArgs "--account-balance-interval" [(account, ">=100,<200")] + , intervalArgs "--starting-account-balance-interval" [(account, "=300")] + , ["--out-file", outFile] + ] + ShelleyTx _ tx <- readDijkstraTransaction outFile + address <- accountAddress + let body = tx ^. L.bodyTxL + changeAmount = sum [output ^. L.coinTxOutL | output <- toList $ body ^. L.outputsTxBodyL] + body ^. L.accountBalanceIntervalsTxBodyL + === L.AccountBalanceIntervals + (Map.singleton address (L.AccountBalanceBothBounds (Inclusive 100) (Exclusive 200))) + body ^. L.startingAccountBalanceIntervalsTxBodyL + === L.AccountBalanceIntervals (Map.singleton address (L.AccountBalanceExact 300)) + changeAmount === inputValue - body ^. L.feeTxBodyL + +intervalArgs :: String -> [(String, String)] -> [String] +intervalArgs flag entries = concat [[flag, address ++ expression] | (address, expression) <- entries] + +-- Failed builds must not leave an output transaction behind. +assertRejected :: FilePath -> [String] -> H.PropertyT (ResourceT IO) String +assertRejected outFile args = do + (exitCode, _, stderr) <- execDetailCardanoCLI $ args ++ ["--out-file", outFile] + exitCode === ExitFailure 1 + H.evalIO (doesFileExist outFile) >>= (=== False) + pure stderr + +assertInvalidOption :: String -> [String] -> H.PropertyT (ResourceT IO) () +assertInvalidOption flag args = do + (exitCode, _, stderr) <- execDetailCardanoCLI $ args ++ [flag, account ++ "=1"] + exitCode === ExitFailure 1 + H.assertWith stderr (("Invalid option `" ++ flag ++ "'") `isInfixOf`) diff --git a/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/DirectDeposit.hs b/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/DirectDeposit.hs index 2fdf789042..49879cd4f8 100644 --- a/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/DirectDeposit.hs +++ b/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/DirectDeposit.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} +-- | CIP-159 direct deposit tests. module Test.Cli.Transaction.DirectDeposit where import Cardano.Api @@ -48,8 +49,8 @@ accountAddress = toShelleyStakeAddr <$> H.evalMaybe (deserialiseAddress AsStakeA -- Decode both the top-level and signed nested transaction to catch deposits -- being accepted by the parser but lost during construction or embedding. -hprop_cip159_direct_deposits_roundtrip :: Property -hprop_cip159_direct_deposits_roundtrip = +hprop_direct_deposits_roundtrip :: Property +hprop_direct_deposits_roundtrip = watchdogProp . propertyOnce $ H.moduleWorkspace "tmp" $ \tempDir -> do outFile <- H.noteTempFile tempDir "tx.body" subFile <- H.noteTempFile tempDir "sub.body" @@ -78,13 +79,11 @@ hprop_cip159_direct_deposits_roundtrip = ShelleyTx _ tx <- readDijkstraTransaction outFile let body = tx ^. L.bodyTxL body ^. L.directDepositsTxBodyL === expected - let nested = toList $ body ^. L.subTransactionsTxBodyL - length nested === 1 - forM_ nested $ \child -> - child ^. L.bodyTxL . L.directDepositsTxBodyL === expected + map (^. L.bodyTxL . L.directDepositsTxBodyL) (toList $ body ^. L.subTransactionsTxBodyL) + === [expected] -hprop_cip159_rejects_invalid_direct_deposits :: Property -hprop_cip159_rejects_invalid_direct_deposits = +hprop_rejects_invalid_direct_deposits :: Property +hprop_rejects_invalid_direct_deposits = watchdogProp . propertyOnce $ H.moduleWorkspace "tmp" $ \tempDir -> do outFile <- H.noteTempFile tempDir "unused" let invalid = @@ -107,37 +106,28 @@ hprop_cip159_rejects_invalid_direct_deposits = H.evalIO (doesFileExist outFile) >>= (=== False) -- | Invalid arguments must be reported even when the node is unavailable. -hprop_cip159_build_rejects_duplicate_deposits_before_node_query :: Property -hprop_cip159_build_rejects_duplicate_deposits_before_node_query = +hprop_build_rejects_duplicate_deposits_before_node_query :: Property +hprop_build_rejects_duplicate_deposits_before_node_query = watchdogProp . propertyOnce $ H.moduleWorkspace "tmp" $ \tempDir -> do outFile <- H.noteTempFile tempDir "unused" socketPath <- H.noteTempFile tempDir "missing.socket" (exitCode, _, stderr) <- - execDetailCardanoCLI - [ "dijkstra" - , "transaction" - , "build" - , "--testnet-magic" - , "42" - , "--socket-path" - , socketPath - , "--tx-in" - , txIn - , "--change-address" - , "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg" - , "--direct-deposit" - , account ++ "+1" - , "--direct-deposit" - , account ++ "+2" - , "--out-file" - , outFile - ] + execDetailCardanoCLI $ + concat + [ ["dijkstra", "transaction", "build"] + , ["--testnet-magic", "42", "--socket-path", socketPath] + , ["--tx-in", txIn] + , ["--change-address", "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg"] + , ["--direct-deposit", account ++ "+1"] + , ["--direct-deposit", account ++ "+2"] + , ["--out-file", outFile] + ] exitCode === ExitFailure 1 H.assertWith stderr (("Duplicate direct-deposit accounts: " ++ account) `isInfixOf`) H.evalIO (doesFileExist outFile) >>= (=== False) -hprop_cip159_direct_deposit_era_gating :: Property -hprop_cip159_direct_deposit_era_gating = +hprop_direct_deposit_era_gating :: Property +hprop_direct_deposit_era_gating = watchdogProp . propertyOnce $ do forM_ ["build", "build-raw", "build-estimate"] $ \command -> do help <- execCardanoCLI ["dijkstra", "transaction", command, "--help"] @@ -152,8 +142,8 @@ hprop_cip159_direct_deposit_era_gating = -- | With 100 ADA of inputs and direct deposits of 20 and 30 ADA, build-estimate -- must return 50 ADA minus the estimated fee as change. This catches deposits being -- recorded in the body but omitted from the change calculation. -hprop_cip159_build_estimate_deducts_deposits_from_change :: Property -hprop_cip159_build_estimate_deducts_deposits_from_change = +hprop_build_estimate_deducts_deposits_from_change :: Property +hprop_build_estimate_deducts_deposits_from_change = watchdogProp . propertyOnce $ H.moduleWorkspace "tmp" $ \tempDir -> do paramsFile <- H.noteTempFile tempDir "params.json" outFile <- H.noteTempFile tempDir "tx.body" @@ -166,31 +156,18 @@ hprop_cip159_build_estimate_deducts_deposits_from_change = & L.ppTxFeePerByteL .~ L.CoinPerByte (L.CompactCoin 44) & L.ppTxFeeFixedL .~ 155381 H.evalIO $ Aeson.encodeFile paramsFile params - -- build-estimate works offline: supply the input value explicitly. There - -- are no --tx-out arguments, so every generated output is change. - H.note_ "The 100 ADA input must fund deposits of 20 and 30 ADA, the estimated fee, and change." + -- With no explicit outputs, the balance after deposits and fees becomes change. void $ - execCardanoCLI - [ "dijkstra" - , "transaction" - , "build-estimate" - , "--tx-in" - , txIn - , "--total-utxo-value" - , show (L.unCoin inputValue) - , "--shelley-key-witnesses" - , "1" - , "--protocol-params-file" - , paramsFile - , "--change-address" - , "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg" - , "--direct-deposit" - , account ++ "+" ++ show (L.unCoin depositAmount) - , "--direct-deposit" - , otherAccount ++ "+" ++ show (L.unCoin otherDepositAmount) - , "--out-file" - , outFile - ] + execCardanoCLI $ + concat + [ ["dijkstra", "transaction", "build-estimate"] + , ["--tx-in", txIn, "--total-utxo-value", show (L.unCoin inputValue)] + , ["--shelley-key-witnesses", "1", "--protocol-params-file", paramsFile] + , ["--change-address", "addr_test1vp0t4dfa9ktc2uvv7sg9leafuhtwyu0xcj4q4kf5pqkpjwqhklklg"] + , ["--direct-deposit", account ++ "+" ++ show (L.unCoin depositAmount)] + , ["--direct-deposit", otherAccount ++ "+" ++ show (L.unCoin otherDepositAmount)] + , ["--out-file", outFile] + ] ShelleyTx _ tx <- readDijkstraTransaction outFile address <- accountAddress otherAddress <- @@ -198,12 +175,7 @@ hprop_cip159_build_estimate_deducts_deposits_from_change = let body = tx ^. L.bodyTxL estimatedFee = body ^. L.feeTxBodyL changeAmount = sum [output ^. L.coinTxOutL | output <- toList $ body ^. L.outputsTxBodyL] - -- Check that the requested deposit was included in the serialized body. body ^. L.directDepositsTxBodyL === L.DirectDeposits (Map.fromList [(address, depositAmount), (otherAddress, otherDepositAmount)]) - -- Use the fee calculated by build-estimate, since it depends on transaction - -- size. The remaining change must be 100 ADA - 20 ADA - 30 ADA - that fee. - H.note_ $ "Estimated fee (lovelace): " ++ show (L.unCoin estimatedFee) - H.note_ $ "Returned change (lovelace): " ++ show (L.unCoin changeAmount) changeAmount === inputValue - depositAmount - otherDepositAmount - estimatedFee