From 9fea7b6fdfb45b7571fc6d4d90721899d4a1ae09 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 23 Oct 2026 15:18:56 -0400 Subject: [PATCH 1/3] Add CIP-159 account balance intervals to builders and debug views --- .../20261023_account_balance_intervals.yml | 5 ++ .../Cardano/CLI/Compatible/Json/Friendly.hs | 36 ++++++++++++- .../src/Cardano/CLI/EraBased/Common/Option.hs | 54 ++++++++++++++++++- .../CLI/EraBased/Transaction/Command.hs | 7 +++ .../EraBased/Transaction/Internal/Common.hs | 23 +++++++- .../CLI/EraBased/Transaction/Option.hs | 6 +++ .../Cardano/CLI/EraBased/Transaction/Run.hs | 43 ++++++++++++++- .../Transaction/SubTransaction/Command.hs | 4 ++ .../Transaction/SubTransaction/Option.hs | 1 + .../Transaction/SubTransaction/Run.hs | 17 +++--- .../src/Cardano/CLI/Type/Error/TxCmdError.hs | 6 +++ 11 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 .changes/20261023_account_balance_intervals.yml diff --git a/.changes/20261023_account_balance_intervals.yml b/.changes/20261023_account_balance_intervals.yml new file mode 100644 index 0000000000..1d3e977e9d --- /dev/null +++ b/.changes/20261023_account_balance_intervals.yml @@ -0,0 +1,5 @@ +project: cardano-cli +pr: 1483 +kind: + - feature +description: Add comparison-based account balance interval options to Dijkstra transaction builders and expose ordinary, starting, and embedded sub-transaction intervals in JSON and YAML debug views. diff --git a/cardano-cli/src/Cardano/CLI/Compatible/Json/Friendly.hs b/cardano-cli/src/Cardano/CLI/Compatible/Json/Friendly.hs index e8ca4aa83f..85120eb522 100644 --- a/cardano-cli/src/Cardano/CLI/Compatible/Json/Friendly.hs +++ b/cardano-cli/src/Cardano/CLI/Compatible/Json/Friendly.hs @@ -48,6 +48,7 @@ import Cardano.Ledger.Address qualified as L import Cardano.Ledger.Api.Tx qualified as L import Cardano.Ledger.Core qualified as C import Cardano.Ledger.Credential (credKeyHash, credScriptHash) +import Cardano.Ledger.Dijkstra.TxBody qualified as L import Cardano.Ledger.Keys (coerceKeyRole) import Control.Applicative ((<|>)) @@ -198,7 +199,40 @@ 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" .= renderSubTransactions body + ] + +renderSubTransactions + :: L.TxBody C.TopTx (Exp.LedgerEra Exp.DijkstraEra) + -> Aeson.Value +renderSubTransactions body = + array + [ object + [ "id" .= serialiseToRawBytesHexText (Exp.getSignedSubTxId (Exp.SignedSubTx subTx)) + , "account balance intervals" + .= friendlyAccountBalanceIntervals (subTx ^. L.bodyTxL . L.accountBalanceIntervalsTxBodyL) + ] + | subTx <- toList (body ^. L.subTransactionsTxBodyL) + ] + +friendlyAccountBalanceIntervals :: L.AccountBalanceIntervals era -> Aeson.Value +friendlyAccountBalanceIntervals (L.AccountBalanceIntervals intervals) + | Map.null intervals = Null + | otherwise = + array + [ object $ + [ "address" .= serialiseAddress addr + , "interval" .= interval + ] + <> friendlyStakeAddress addr + | (account, interval) <- Map.toList intervals + , let addr = fromShelleyStakeAddr account + ] 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..3583214019 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Common/Option.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Common/Option.hs @@ -40,7 +40,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 +1517,57 @@ 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 <$> (P.char '=' *> parseLovelace)) + <|> (L.AccountBalanceUpperBound . Exclusive <$> (P.char '<' *> parseLovelace)) + <|> do + lower <- Inclusive <$> (P.string ">=" *> parseLovelace) + upper <- optional (P.string ",<" *> (Exclusive <$> parseLovelace)) + pure $ maybe (L.AccountBalanceLowerBound lower) (L.AccountBalanceBothBounds lower) upper + pure (address, interval) + +pAccountBalanceIntervals + :: Exp.Era era + -> Parser [(StakeAddress, L.AccountBalanceInterval (Exp.LedgerEra era))] +pAccountBalanceIntervals era = + pBalanceIntervals + era + "account-balance-interval" + "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 + "starting-account-balance-interval" + "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{=N|>=L|=L, Opt.help + ( timing + <> " Amounts in lovelace; lower bounds inclusive, upper bounds exclusive." + <> " Quote the argument to protect < and > from the shell, 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..1e4fe87818 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Command.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Command.hs @@ -37,6 +37,7 @@ 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) @@ -86,6 +87,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 +157,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 +208,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..54ecd455bd 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,14 @@ 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 "account-balance-interval" accountBalanceIntervals + void $ + fromEitherCli $ + convertAccountBalanceIntervals "starting-account-balance-interval" startingAccountBalanceIntervals checkNodeNetworkId nodeConnInfo let eon = convert currentEra @@ -312,6 +321,8 @@ runTransactionBuildCmd certsAndMaybeScriptWits withdrawalsAndMaybeScriptWits directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals requiredSigners txAuxScripts txMetadata @@ -398,6 +409,8 @@ runTransactionBuildEstimateCmd -- TODO change type , certificates , withdrawals , directDeposits + , accountBalanceIntervals + , startingAccountBalanceIntervals , metadataSchema , scriptFiles , metadataFiles @@ -491,6 +504,8 @@ runTransactionBuildEstimateCmd -- TODO change type certsAndMaybeScriptWits withdrawalsAndMaybeScriptWits directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals requiredSigners 0 txAuxScripts @@ -606,6 +621,8 @@ runTransactionBuildRawCmd , certificates , withdrawals , directDeposits + , accountBalanceIntervals + , startingAccountBalanceIntervals , metadataSchema , scriptFiles , metadataFiles @@ -696,6 +713,8 @@ runTransactionBuildRawCmd certsAndMaybeScriptWits withdrawalsAndMaybeScriptWits directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals requiredSigners txAuxScripts txMetadata @@ -743,6 +762,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 +792,8 @@ runTxBuildRaw certsAndMaybeSriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners txAuxScripts txMetadata @@ -797,6 +820,8 @@ runTxBuildRaw certsAndMaybeSriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners fee txAuxScripts @@ -840,6 +865,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 +899,8 @@ constructTxBodyContent certsAndMaybeScriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners fee txAuxScripts @@ -920,6 +949,10 @@ constructTxBodyContent let validatedCurrentTreasuryValue = unTxCurrentTreasuryValue <$> mCurrentTreasury validatedTreasuryDonation = unTxTreasuryDonation <$> mTreasuryDonation validatedDirectDeposits <- convertDirectDeposits directDeposits + validatedIntervals <- + convertAccountBalanceIntervals "account-balance-interval" accountBalanceIntervals + validatedStartingIntervals <- + convertAccountBalanceIntervals "starting-account-balance-interval" startingAccountBalanceIntervals let validatedWithdrawals = convertWithdrawals withdrawals return ( Exp.defaultTxBodyContent @@ -936,6 +969,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 +1026,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 +1058,8 @@ runTxBuild certsAndMaybeScriptWits withdrawals directDeposits + accountBalanceIntervals + startingAccountBalanceIntervals reqSigners txAuxScripts txMetadata @@ -1090,6 +1129,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..4569df5474 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Run.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/SubTransaction/Run.hs @@ -58,7 +58,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 +80,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 +98,7 @@ runSubTransactionBuildRawCmd , certificates , withdrawals , directDeposits + , accountBalanceIntervals , metadataSchema , scriptFiles , metadataFiles @@ -156,14 +156,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 +179,7 @@ runSubTransactionBuildRawCmd certsAndMaybeScriptWits withdrawalsAndMaybeScriptWits directDeposits + accountBalanceIntervals txAuxScripts txMetadata votingProceduresAndMaybeScriptWits @@ -353,6 +351,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 +376,7 @@ constructSubTx certsAndMaybeScriptWits withdrawals directDeposits + accountBalanceIntervals txAuxScripts txMetadata votingProcedures @@ -423,6 +423,8 @@ constructSubTx TxMetadataInEra _ mDat -> mDat validatedDirectDeposits <- convertDirectDeposits directDeposits + validatedIntervals <- + convertAccountBalanceIntervals "account-balance-interval" accountBalanceIntervals validatedGuards <- first TxCmdGuardsError $ obtainCommonConstraints (Exp.useEra @era) $ @@ -443,6 +445,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) From a6f694f9153a2daf681cf4f13a54351b5170054f Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 23 Oct 2026 15:18:57 -0400 Subject: [PATCH 2/3] Test account balance intervals and transaction debug views --- cardano-cli/cardano-cli.cabal | 1 + .../Test/Golden/Debug/TransactionView.hs | 75 +++++- .../transaction-view-direct-deposits.json | 3 + .../transaction-view-direct-deposits.yaml | 3 + .../transaction-view-intervals-signed.json | 120 +++++++++ .../transaction-view-intervals-signed.yaml | 83 +++++++ .../dijkstra/transaction-view-intervals.json | 114 +++++++++ .../dijkstra/transaction-view-intervals.yaml | 80 ++++++ .../build-raw-plutus-v4-script-witnesses.out | 3 + .../cardano-cli-golden/files/golden/help.cli | 7 + .../dijkstra_sub-transaction_build-raw.cli | 12 + .../dijkstra_transaction_build-estimate.cli | 21 ++ .../help/dijkstra_transaction_build-raw.cli | 21 ++ .../help/dijkstra_transaction_build.cli | 21 ++ .../Cli/Transaction/AccountBalanceInterval.hs | 227 ++++++++++++++++++ .../Test/Cli/Transaction/DirectDeposit.hs | 96 +++----- 16 files changed, 824 insertions(+), 63 deletions(-) create mode 100644 cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals-signed.json create mode 100644 cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals-signed.yaml create mode 100644 cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals.json create mode 100644 cardano-cli/test/cardano-cli-golden/files/golden/dijkstra/transaction-view-intervals.yaml create mode 100644 cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/AccountBalanceInterval.hs diff --git a/cardano-cli/cardano-cli.cabal b/cardano-cli/cardano-cli.cabal index b69ea8e444..021da3bec6 100644 --- a/cardano-cli/cardano-cli.cabal +++ b/cardano-cli/cardano-cli.cabal @@ -431,6 +431,7 @@ test-suite cardano-cli-test Test.Cli.Run.Hash Test.Cli.Run.Query Test.Cli.Shelley.Genesis.Create + Test.Cli.Transaction.AccountBalanceInterval Test.Cli.Transaction.Build Test.Cli.Transaction.DirectDeposit Test.Cli.Transaction.SubTransaction 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..5c5b545240 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": [], "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..1d062b207d 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: [] 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..5947c95a0c --- /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 + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "kind": "upperBound", + "upper": 200 + }, + "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, + "kind": "exact" + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "kind": "bothBounds", + "lower": 0, + "upper": 400 + }, + "network": "Testnet", + "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" + } + ], + "sub-transactions": [ + { + "account balance intervals": [ + { + "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", + "interval": { + "kind": "bothBounds", + "lower": 100, + "upper": 200 + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "coin": 42, + "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..c95ff3f9c4 --- /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 + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 +- address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + kind: upperBound + upper: 200 + 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 + kind: exact + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 +- address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + kind: bothBounds + lower: 0 + upper: 400 + network: Testnet + stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 +sub-transactions: +- account balance intervals: + - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 + interval: + kind: bothBounds + lower: 100 + upper: 200 + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 + - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + coin: 42 + 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..9b4f378623 --- /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 + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "kind": "upperBound", + "upper": 200 + }, + "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, + "kind": "exact" + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "kind": "bothBounds", + "lower": 0, + "upper": 400 + }, + "network": "Testnet", + "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" + } + ], + "sub-transactions": [ + { + "account balance intervals": [ + { + "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", + "interval": { + "kind": "bothBounds", + "lower": 100, + "upper": 200 + }, + "network": "Testnet", + "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" + }, + { + "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", + "interval": { + "coin": 42, + "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..ddabaedde9 --- /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 + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 +- address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + kind: upperBound + upper: 200 + 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 + kind: exact + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 +- address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + kind: bothBounds + lower: 0 + upper: 400 + network: Testnet + stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 +sub-transactions: +- account balance intervals: + - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 + interval: + kind: bothBounds + lower: 100 + upper: 200 + network: Testnet + stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 + - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl + interval: + coin: 42 + 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..e83b954581 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: [] 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..7ab5cc05f7 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{=N|>=L|=L,=L|=L,=L|=L,=L|=L,=L|=L,=L|=L,=L|=L,=L|=L,=L|=L, from + the shell, 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..7fdd959122 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{=N|>=L|=L,=L|=L,=L|=L, from + the shell, e.g. 'stake_test1...>=100,<200'. Each + account may occur once per option. + --starting-account-balance-interval 'STAKE_ADDRESS{=N|>=L|=L, from the shell, 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..cf16905e9c 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{=N|>=L|=L,=L|=L,=L|=L, from + the shell, e.g. 'stake_test1...>=100,<200'. Each + account may occur once per option. + --starting-account-balance-interval 'STAKE_ADDRESS{=N|>=L|=L, from the shell, 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..7f83175aa0 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{=N|>=L|=L,=L|=L,=L|=L, from + the shell, e.g. 'stake_test1...>=100,<200'. Each + account may occur once per option. + --starting-account-balance-interval 'STAKE_ADDRESS{=N|>=L|=L, from the shell, 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..ad3f96ab87 --- /dev/null +++ b/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/AccountBalanceInterval.hs @@ -0,0 +1,227 @@ +{-# 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.Morph (hoist) +import Control.Monad.Trans.Resource (ResourceT, runResourceT) +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 Hedgehog.Gen qualified as Gen +import Hedgehog.Range qualified as Range +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 . H.property . hoist runResourceT $ H.moduleWorkspace "tmp" $ \tempDir -> do + amount <- H.forAll $ Gen.integral $ Range.constant 0 maxAmount + otherAmount <- H.forAll $ Gen.integral $ Range.constant 0 maxAmount + 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 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 amount amount + , between maxAmount 0 + , 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] + 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 From 9003c63ed1942f0f45772299413747a2848dc366 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Mon, 26 Oct 2026 16:25:43 -0400 Subject: [PATCH 3/3] Address account balance interval review feedback --- .../20261023_account_balance_intervals.yml | 2 +- .../Cardano/CLI/Compatible/Json/Friendly.hs | 48 ++++++++++++++----- .../src/Cardano/CLI/EraBased/Common/Option.hs | 33 +++++++++---- .../CLI/EraBased/Transaction/Command.hs | 8 ++++ .../Cardano/CLI/EraBased/Transaction/Run.hs | 12 +++-- .../Transaction/SubTransaction/Run.hs | 3 +- .../transaction-view-direct-deposits.json | 2 +- .../transaction-view-direct-deposits.yaml | 2 +- .../transaction-view-intervals-signed.json | 16 +++---- .../transaction-view-intervals-signed.yaml | 16 +++---- .../dijkstra/transaction-view-intervals.json | 16 +++---- .../dijkstra/transaction-view-intervals.yaml | 16 +++---- .../build-raw-plutus-v4-script-witnesses.out | 2 +- .../cardano-cli-golden/files/golden/help.cli | 14 +++--- .../dijkstra_sub-transaction_build-raw.cli | 11 +++-- .../dijkstra_transaction_build-estimate.cli | 21 ++++---- .../help/dijkstra_transaction_build-raw.cli | 21 ++++---- .../help/dijkstra_transaction_build.cli | 21 ++++---- .../Cli/Transaction/AccountBalanceInterval.hs | 27 +++++++---- 19 files changed, 176 insertions(+), 115 deletions(-) diff --git a/.changes/20261023_account_balance_intervals.yml b/.changes/20261023_account_balance_intervals.yml index 1d3e977e9d..9d23fa7278 100644 --- a/.changes/20261023_account_balance_intervals.yml +++ b/.changes/20261023_account_balance_intervals.yml @@ -2,4 +2,4 @@ project: cardano-cli pr: 1483 kind: - feature -description: Add comparison-based account balance interval options to Dijkstra transaction builders and expose ordinary, starting, and embedded sub-transaction intervals in JSON and YAML debug views. +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, Null + subTransactions -> array $ map renderSubTransaction subTransactions ] -renderSubTransactions - :: L.TxBody C.TopTx (Exp.LedgerEra Exp.DijkstraEra) +renderSubTransaction + :: L.Tx C.SubTx (Exp.LedgerEra Exp.DijkstraEra) -> Aeson.Value -renderSubTransactions body = - array - [ object - [ "id" .= serialiseToRawBytesHexText (Exp.getSignedSubTxId (Exp.SignedSubTx subTx)) - , "account balance intervals" - .= friendlyAccountBalanceIntervals (subTx ^. L.bodyTxL . L.accountBalanceIntervalsTxBodyL) - ] - | subTx <- toList (body ^. L.subTransactionsTxBodyL) +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 @@ -227,13 +227,37 @@ friendlyAccountBalanceIntervals (L.AccountBalanceIntervals intervals) array [ object $ [ "address" .= serialiseAddress addr - , "interval" .= interval + , "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) => L.TxBody C.TopTx (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 3583214019..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 () @@ -1523,13 +1527,22 @@ parseAccountBalanceInterval :: P.Parser (StakeAddress, L.AccountBalanceInterval parseAccountBalanceInterval = do address <- parseAddressAny interval <- - (L.AccountBalanceExact <$> (P.char '=' *> parseLovelace)) - <|> (L.AccountBalanceUpperBound . Exclusive <$> (P.char '<' *> parseLovelace)) + (L.AccountBalanceExact <$> parseExact) + <|> (L.AccountBalanceUpperBound . Exclusive <$> parseUpperBound) <|> do - lower <- Inclusive <$> (P.string ">=" *> parseLovelace) - upper <- optional (P.string ",<" *> (Exclusive <$> parseLovelace)) - pure $ maybe (L.AccountBalanceLowerBound lower) (L.AccountBalanceBothBounds lower) upper + 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 @@ -1537,7 +1550,7 @@ pAccountBalanceIntervals pAccountBalanceIntervals era = pBalanceIntervals era - "account-balance-interval" + 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 @@ -1546,7 +1559,7 @@ pStartingAccountBalanceIntervals pStartingAccountBalanceIntervals era = pBalanceIntervals era - "starting-account-balance-interval" + startingAccountBalanceIntervalOptionName "Check the account balance before the entire transaction, including all sub-transactions. Top-level only." pBalanceIntervals @@ -1560,11 +1573,11 @@ pBalanceIntervals era optionName timing = case era of many $ Opt.option (readerFromParsecParser parseAccountBalanceInterval) $ Opt.long optionName - <> Opt.metavar "'STAKE_ADDRESS{=N|>=L|=L, Opt.metavar "STAKE_ADDRESS{=LOVELACE|>=LOWER|=LOWER, Opt.help ( timing <> " Amounts in lovelace; lower bounds inclusive, upper bounds exclusive." - <> " Quote the argument to protect < and > from the shell, e.g. 'stake_test1...>=100,<200'." + <> " Quote the argument so the shell treats < and > as literal characters, e.g. 'stake_test1...>=100,<200'." <> " Each account may occur once per option." ) diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Command.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Command.hs index 1e4fe87818..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 @@ -42,6 +44,12 @@ 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) diff --git a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Run.hs b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Run.hs index 54ecd455bd..92623a6a92 100644 --- a/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Run.hs +++ b/cardano-cli/src/Cardano/CLI/EraBased/Transaction/Run.hs @@ -166,10 +166,12 @@ runTransactionBuildCmd void $ fromEitherCli $ convertDirectDeposits directDeposits void $ fromEitherCli $ - convertAccountBalanceIntervals "account-balance-interval" accountBalanceIntervals + convertAccountBalanceIntervals accountBalanceIntervalOptionName accountBalanceIntervals void $ fromEitherCli $ - convertAccountBalanceIntervals "starting-account-balance-interval" startingAccountBalanceIntervals + convertAccountBalanceIntervals + startingAccountBalanceIntervalOptionName + startingAccountBalanceIntervals checkNodeNetworkId nodeConnInfo let eon = convert currentEra @@ -950,9 +952,11 @@ constructTxBodyContent validatedTreasuryDonation = unTxTreasuryDonation <$> mTreasuryDonation validatedDirectDeposits <- convertDirectDeposits directDeposits validatedIntervals <- - convertAccountBalanceIntervals "account-balance-interval" accountBalanceIntervals + convertAccountBalanceIntervals accountBalanceIntervalOptionName accountBalanceIntervals validatedStartingIntervals <- - convertAccountBalanceIntervals "starting-account-balance-interval" startingAccountBalanceIntervals + convertAccountBalanceIntervals + startingAccountBalanceIntervalOptionName + startingAccountBalanceIntervals let validatedWithdrawals = convertWithdrawals withdrawals return ( Exp.defaultTxBodyContent 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 4569df5474..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 @@ -424,7 +425,7 @@ constructSubTx validatedDirectDeposits <- convertDirectDeposits directDeposits validatedIntervals <- - convertAccountBalanceIntervals "account-balance-interval" accountBalanceIntervals + convertAccountBalanceIntervals accountBalanceIntervalOptionName accountBalanceIntervals validatedGuards <- first TxCmdGuardsError $ obtainCommonConstraints (Exp.useEra @era) $ 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 5c5b545240..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 @@ -47,7 +47,7 @@ "return collateral": null, "scripts": [], "starting account balance intervals": null, - "sub-transactions": [], + "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 1d062b207d..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 @@ -36,7 +36,7 @@ required signers (payment key hashes needed for scripts): null return collateral: null scripts: [] starting account balance intervals: null -sub-transactions: [] +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 index 5947c95a0c..23b91c28e2 100644 --- 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 @@ -4,7 +4,7 @@ "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", "interval": { "kind": "lowerBound", - "lower": 100 + "lower": "100 Lovelace" }, "network": "Testnet", "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" @@ -13,7 +13,7 @@ "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", "interval": { "kind": "upperBound", - "upper": 200 + "upper": "200 Lovelace" }, "network": "Testnet", "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" @@ -56,7 +56,7 @@ { "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", "interval": { - "coin": 300, + "coin": "300 Lovelace", "kind": "exact" }, "network": "Testnet", @@ -66,8 +66,8 @@ "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", "interval": { "kind": "bothBounds", - "lower": 0, - "upper": 400 + "lower": "0 Lovelace", + "upper": "400 Lovelace" }, "network": "Testnet", "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" @@ -80,8 +80,8 @@ "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", "interval": { "kind": "bothBounds", - "lower": 100, - "upper": 200 + "lower": "100 Lovelace", + "upper": "200 Lovelace" }, "network": "Testnet", "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" @@ -89,7 +89,7 @@ { "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", "interval": { - "coin": 42, + "coin": "42 Lovelace", "kind": "exact" }, "network": "Testnet", 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 index c95ff3f9c4..da11633789 100644 --- 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 @@ -2,13 +2,13 @@ account balance intervals: - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 interval: kind: lowerBound - lower: 100 + lower: 100 Lovelace network: Testnet stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl interval: kind: upperBound - upper: 200 + upper: 200 Lovelace network: Testnet stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 auxiliary scripts: null @@ -42,15 +42,15 @@ scripts: [] starting account balance intervals: - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 interval: - coin: 300 + coin: 300 Lovelace kind: exact network: Testnet stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl interval: kind: bothBounds - lower: 0 - upper: 400 + lower: 0 Lovelace + upper: 400 Lovelace network: Testnet stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 sub-transactions: @@ -58,13 +58,13 @@ sub-transactions: - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 interval: kind: bothBounds - lower: 100 - upper: 200 + lower: 100 Lovelace + upper: 200 Lovelace network: Testnet stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl interval: - coin: 42 + coin: 42 Lovelace kind: exact network: Testnet stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 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 index 9b4f378623..1c6bdaec70 100644 --- 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 @@ -4,7 +4,7 @@ "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", "interval": { "kind": "lowerBound", - "lower": 100 + "lower": "100 Lovelace" }, "network": "Testnet", "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" @@ -13,7 +13,7 @@ "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", "interval": { "kind": "upperBound", - "upper": 200 + "upper": "200 Lovelace" }, "network": "Testnet", "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" @@ -56,7 +56,7 @@ { "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", "interval": { - "coin": 300, + "coin": "300 Lovelace", "kind": "exact" }, "network": "Testnet", @@ -66,8 +66,8 @@ "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", "interval": { "kind": "bothBounds", - "lower": 0, - "upper": 400 + "lower": "0 Lovelace", + "upper": "400 Lovelace" }, "network": "Testnet", "stake credential key hash": "8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605" @@ -80,8 +80,8 @@ "address": "stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99", "interval": { "kind": "bothBounds", - "lower": 100, - "upper": 200 + "lower": "100 Lovelace", + "upper": "200 Lovelace" }, "network": "Testnet", "stake credential script hash": "186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4" @@ -89,7 +89,7 @@ { "address": "stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl", "interval": { - "coin": 42, + "coin": "42 Lovelace", "kind": "exact" }, "network": "Testnet", 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 index ddabaedde9..ba445dd94f 100644 --- 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 @@ -2,13 +2,13 @@ account balance intervals: - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 interval: kind: lowerBound - lower: 100 + lower: 100 Lovelace network: Testnet stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl interval: kind: upperBound - upper: 200 + upper: 200 Lovelace network: Testnet stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 auxiliary scripts: null @@ -42,15 +42,15 @@ scripts: [] starting account balance intervals: - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 interval: - coin: 300 + coin: 300 Lovelace kind: exact network: Testnet stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl interval: kind: bothBounds - lower: 0 - upper: 400 + lower: 0 Lovelace + upper: 400 Lovelace network: Testnet stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 sub-transactions: @@ -58,13 +58,13 @@ sub-transactions: - address: stake_test17qvxuvh64q9zdqgrjt76d42eclk5wgdxtnsun4808cwg0dqxv5r99 interval: kind: bothBounds - lower: 100 - upper: 200 + lower: 100 Lovelace + upper: 200 Lovelace network: Testnet stake credential script hash: 186e32faa80a26810392fda6d559c7ed4721a65ce1c9d4ef3e1c87b4 - address: stake_test1uz855drx5szvz845zqcnq9dc3ez8mqdkqzy7yhmpzcqwvpg59zzfl interval: - coin: 42 + coin: 42 Lovelace kind: exact network: Testnet stake credential key hash: 8f4a3466a404c11eb410313015b88e447d81b60089e25f611600e605 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 e83b954581..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 @@ -113,7 +113,7 @@ scripts: type: plutus script hash: 31a78786b5989dc6fd2d4ab15b297069e94106ecacceb8eb29e1b681 starting account balance intervals: null -sub-transactions: [] +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 7ab5cc05f7..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,7 +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{=N|>=L|=L,=LOWER|=LOWER,=L|=L,=L|=L,=LOWER|=LOWER,=LOWER|=LOWER,=L|=L,=L|=L,=LOWER|=LOWER,=LOWER|=LOWER,=L|=L,=L|=L,=LOWER|=LOWER,=LOWER|=LOWER,=L|=L,=LOWER|=LOWER,=L|=L,=LOWER|=LOWER, from - the shell, e.g. 'stake_test1...>=100,<200'. Each - account may occur once per option. + 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. --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 7fdd959122..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,8 +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{=N|>=L|=L,=L|=L,=LOWER|=LOWER,=LOWER|=LOWER,=L|=L,=LOWER|=LOWER, from - the shell, e.g. 'stake_test1...>=100,<200'. Each - account may occur once per option. - --starting-account-balance-interval 'STAKE_ADDRESS{=N|>=L|=L, 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, from the shell, e.g. - 'stake_test1...>=100,<200'. Each account may occur - once per option. + so the shell treats < and > 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 cf16905e9c..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,8 +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{=N|>=L|=L,=L|=L,=LOWER|=LOWER,=LOWER|=LOWER,=L|=L,=LOWER|=LOWER, from - the shell, e.g. 'stake_test1...>=100,<200'. Each - account may occur once per option. - --starting-account-balance-interval 'STAKE_ADDRESS{=N|>=L|=L, 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, from the shell, e.g. - 'stake_test1...>=100,<200'. Each account may occur - once per option. + so the shell treats < and > 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 7f83175aa0..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,8 +109,8 @@ Usage: cardano-cli dijkstra transaction build ) ]] [--direct-deposit STAKE_ADDRESS+LOVELACE] - [--account-balance-interval 'STAKE_ADDRESS{=N|>=L|=L,=L|=L,=LOWER|=LOWER,=LOWER|=LOWER,=L|=L,=LOWER|=LOWER, from - the shell, e.g. 'stake_test1...>=100,<200'. Each - account may occur once per option. - --starting-account-balance-interval 'STAKE_ADDRESS{=N|>=L|=L, 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, from the shell, e.g. - 'stake_test1...>=100,<200'. Each account may occur - once per option. + so the shell treats < and > 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 index ad3f96ab87..1d45d00fa3 100644 --- a/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/AccountBalanceInterval.hs +++ b/cardano-cli/test/cardano-cli-test/Test/Cli/Transaction/AccountBalanceInterval.hs @@ -15,8 +15,7 @@ import Cardano.Ledger.Dijkstra.Scripts qualified as L import Cardano.Ledger.Dijkstra.TxBody qualified as L import Control.Monad (forM_, void) -import Control.Monad.Morph (hoist) -import Control.Monad.Trans.Resource (ResourceT, runResourceT) +import Control.Monad.Trans.Resource (ResourceT) import Data.Aeson qualified as Aeson import Data.Foldable (toList) import Data.List (isInfixOf) @@ -31,8 +30,6 @@ import Test.Cardano.CLI.Util import Hedgehog (Property, (===)) import Hedgehog qualified as H import Hedgehog.Extras qualified as H -import Hedgehog.Gen qualified as Gen -import Hedgehog.Range qualified as Range import Test.Cli.Transaction.DirectDeposit (account, accountAddress, otherAccount) import Test.Cli.Transaction.SubTransaction ( buildSubTransaction @@ -48,24 +45,24 @@ import Test.Cli.Transaction.SubTransaction -- a flag is insufficient if a builder later discards the corresponding field. hprop_account_balance_intervals_roundtrip :: Property hprop_account_balance_intervals_roundtrip = - watchdogProp . H.property . hoist runResourceT $ H.moduleWorkspace "tmp" $ \tempDir -> do - amount <- H.forAll $ Gen.integral $ Range.constant 0 maxAmount - otherAmount <- H.forAll $ Gen.integral $ Range.constant 0 maxAmount + 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 cases = + 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 amount amount - , between maxAmount 0 + , between 0 1 + , between (maxAmount - 1) maxAmount , between 0 maxAmount ] forM_ cases $ \(expression, interval) -> do @@ -144,6 +141,16 @@ hprop_account_balance_intervals_reject_invalid_arguments = forM_ commands $ \(base, flag) -> 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`)