diff --git a/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Balance.hs b/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Balance.hs index 177a49ef016..bf5db4cbc0d 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Balance.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Balance.hs @@ -80,7 +80,6 @@ module Cardano.Wallet.Primitive.CoinSelection.Balance , runSelectionNonEmpty , runSelectionNonEmptyWith , RunSelectionParams (..) - , SelectionState (..) -- * Running a selection step , runSelectionStep @@ -150,6 +149,8 @@ import Cardano.Wallet.Primitive.Types.Tx ) import Cardano.Wallet.Primitive.Types.UTxOIndex ( SelectionFilter (..), UTxOIndex (..) ) +import Cardano.Wallet.Primitive.Types.UTxOSelection + ( IsUTxOSelection, UTxOSelection, UTxOSelectionNonEmpty ) import Control.Monad.Random.Class ( MonadRandom (..) ) import Data.Bifunctor @@ -189,6 +190,7 @@ import qualified Cardano.Wallet.Primitive.Types.TokenMap as TokenMap import qualified Cardano.Wallet.Primitive.Types.TokenQuantity as TokenQuantity import qualified Cardano.Wallet.Primitive.Types.Tx as Tx import qualified Cardano.Wallet.Primitive.Types.UTxOIndex as UTxOIndex +import qualified Cardano.Wallet.Primitive.Types.UTxOSelection as UTxOSelection import qualified Data.Foldable as F import qualified Data.List as L import qualified Data.List.NonEmpty as NE @@ -849,7 +851,7 @@ performSelectionNonEmpty constraints params Nothing -> pure $ Left EmptyUTxO Just selection -> do - let utxoSelected = selected selection + let utxoSelected = UTxOSelection.selectedIndex selection let utxoBalanceSelected = UTxOIndex.balance utxoSelected if utxoBalanceRequired `leq` utxoBalanceSelected then makeChangeRepeatedly selection @@ -877,10 +879,6 @@ performSelectionNonEmpty constraints params , utxoBalanceRequired } - mkInputsSelected :: UTxOIndex -> NonEmpty (TxIn, TxOut) - mkInputsSelected = - fromMaybe invariantSelectAnyInputs . NE.nonEmpty . UTxOIndex.toList - selectionLimit :: SelectionLimit selectionLimit = computeSelectionLimit (F.toList outputsToCover) @@ -939,10 +937,10 @@ performSelectionNonEmpty constraints params -- assets of the final resulting selection). -- predictChange - :: UTxOIndex + :: UTxOSelectionNonEmpty -> [Set AssetId] - predictChange inputsPreSelected = either - (const $ invariantResultWithNoCost inputsPreSelected) + predictChange s = either + (const $ invariantResultWithNoCost $ UTxOSelection.selectedIndex s) (fmap (TokenMap.getAssets . view #tokens)) (makeChange MakeChangeCriteria { minCoinFor = noMinimumCoin @@ -957,7 +955,7 @@ performSelectionNonEmpty constraints params } ) where - inputBundles = view #tokens . snd <$> mkInputsSelected inputsPreSelected + inputBundles = view #tokens . snd <$> UTxOSelection.selectedList s outputBundles = view #tokens <$> outputsToCover noMinimumCoin :: TokenMap -> Coin @@ -981,7 +979,7 @@ performSelectionNonEmpty constraints params -- function won't make associated outputs for them. -- makeChangeRepeatedly - :: SelectionState + :: UTxOSelectionNonEmpty -> m (Either SelectionError (SelectionResultOf (NonEmpty TxOut) TokenBundle)) makeChangeRepeatedly s = case mChangeGenerated of @@ -1051,7 +1049,7 @@ performSelectionNonEmpty constraints params selectOneEntry = selectCoinQuantity selectionLimit - SelectionState {selected} = s + selected = UTxOSelection.selectedIndex s requiredCost = computeMinimumCost SelectionSkeleton { skeletonInputCount = UTxOIndex.size selected @@ -1061,19 +1059,8 @@ performSelectionNonEmpty constraints params , skeletonAssetsToBurn = assetsToBurn } - skeletonChange = predictChange selected - inputsSelected = mkInputsSelected selected - - invariantSelectAnyInputs = - -- This should be impossible, as we have already determined - -- that the UTxO balance is sufficient. - error $ unlines - [ "performSelection: unable to select any inputs!" - , "UTxO balance required:" - , show utxoBalanceRequired - , "UTxO balance available:" - , show utxoBalanceAvailable - ] + skeletonChange = predictChange s + inputsSelected = UTxOSelection.selectedList s invariantResultWithNoCost inputs_ = error $ unlines -- This should be impossible, as the 'makeChange' function should @@ -1211,14 +1198,6 @@ instance Buildable (SelectionResult TxOut) where -- Running a selection (without making change) -------------------------------------------------------------------------------- -data SelectionState = SelectionState - { selected - :: !UTxOIndex - , leftover - :: !UTxOIndex - } - deriving (Eq, Generic, Show) - -- | Parameters for 'runSelection'. -- data RunSelectionParams = RunSelectionParams @@ -1232,26 +1211,25 @@ data RunSelectionParams = RunSelectionParams deriving (Eq, Generic, Show) runSelectionNonEmpty - :: MonadRandom m => RunSelectionParams -> m (Maybe SelectionState) + :: MonadRandom m => RunSelectionParams -> m (Maybe UTxOSelectionNonEmpty) runSelectionNonEmpty = (=<<) <$> runSelectionNonEmptyWith . selectCoinQuantity . view #selectionLimit <*> runSelection runSelectionNonEmptyWith :: Monad m - => (SelectionState -> m (Maybe SelectionState)) - -> SelectionState - -> m (Maybe SelectionState) -runSelectionNonEmptyWith selectSingleEntry result - | UTxOIndex.null (selected result) = - result & selectSingleEntry - | otherwise = - pure (Just result) + => (UTxOSelection -> m (Maybe UTxOSelectionNonEmpty)) + -> UTxOSelection + -> m (Maybe UTxOSelectionNonEmpty) +runSelectionNonEmptyWith selectSingleEntry result = + UTxOSelection.toNonEmpty result & maybe + (result & selectSingleEntry) + (pure . Just) runSelection - :: forall m. MonadRandom m => RunSelectionParams -> m SelectionState + :: forall m. MonadRandom m => RunSelectionParams -> m UTxOSelection runSelection params = - runRoundRobinM initialState id selectors + runRoundRobinM initialState UTxOSelection.fromNonEmpty selectors where RunSelectionParams { selectionLimit @@ -1259,17 +1237,14 @@ runSelection params = , minimumBalance } = params - initialState :: SelectionState - initialState = SelectionState - { selected = UTxOIndex.empty - , leftover = utxoAvailable - } + initialState :: UTxOSelection + initialState = UTxOSelection.fromIndex utxoAvailable (const False) -- NOTE: We run the 'coinSelector' last, because we know that every input -- necessarily has a non-zero ada amount. By running the other selectors -- first, we increase the probability that the coin selector will be able -- to terminate without needing to select an additional coin. - selectors :: [SelectionState -> m (Maybe SelectionState)] + selectors :: [UTxOSelection -> m (Maybe UTxOSelectionNonEmpty)] selectors = reverse (coinSelector : fmap assetSelector minimumAssetQuantities) where @@ -1285,9 +1260,10 @@ assetSelectionLens :: MonadRandom m => SelectionLimit -> (AssetId, TokenQuantity) - -> SelectionLens m SelectionState + -> SelectionLens m UTxOSelection UTxOSelectionNonEmpty assetSelectionLens limit (asset, minimumAssetQuantity) = SelectionLens - { currentQuantity = assetQuantity asset . selected + { currentQuantity = assetQuantity asset . UTxOSelection.selectedIndex + , updatedQuantity = assetQuantity asset . UTxOSelection.selectedIndex , minimumQuantity = unTokenQuantity minimumAssetQuantity , selectQuantity = selectAssetQuantity asset limit } @@ -1297,9 +1273,10 @@ coinSelectionLens => SelectionLimit -> Coin -- ^ Minimum coin quantity. - -> SelectionLens m SelectionState + -> SelectionLens m UTxOSelection UTxOSelectionNonEmpty coinSelectionLens limit minimumCoinQuantity = SelectionLens - { currentQuantity = coinQuantity . selected + { currentQuantity = coinQuantity . UTxOSelection.selectedIndex + , updatedQuantity = coinQuantity . UTxOSelection.selectedIndex , minimumQuantity = fromIntegral $ unCoin minimumCoinQuantity , selectQuantity = selectCoinQuantity limit } @@ -1308,10 +1285,11 @@ coinSelectionLens limit minimumCoinQuantity = SelectionLens -- selectAssetQuantity :: MonadRandom m + => IsUTxOSelection utxoSelection => AssetId -> SelectionLimit - -> SelectionState - -> m (Maybe SelectionState) + -> utxoSelection + -> m (Maybe UTxOSelectionNonEmpty) selectAssetQuantity asset = selectMatchingQuantity (WithAssetOnly asset :| [WithAsset asset]) @@ -1319,9 +1297,10 @@ selectAssetQuantity asset = -- selectCoinQuantity :: MonadRandom m + => IsUTxOSelection utxoSelection => SelectionLimit - -> SelectionState - -> m (Maybe SelectionState) + -> utxoSelection + -> m (Maybe UTxOSelectionNonEmpty) selectCoinQuantity = selectMatchingQuantity (WithAdaOnly :| [Any]) @@ -1343,31 +1322,32 @@ selectCoinQuantity = -- selectMatchingQuantity :: MonadRandom m + => IsUTxOSelection utxoSelection => NonEmpty SelectionFilter -- ^ A list of selection filters to be traversed from left-to-right, -- in descending order of priority. -> SelectionLimit -- ^ A limit to adhere to when selecting entries. - -> SelectionState + -> utxoSelection -- ^ The current selection state. - -> m (Maybe SelectionState) + -> m (Maybe UTxOSelectionNonEmpty) -- ^ An updated selection state that includes a matching UTxO entry, -- or 'Nothing' if no such entry could be found. selectMatchingQuantity filters limit s | limitReached = pure Nothing - | otherwise = - fmap updateState <$> - UTxOIndex.selectRandomWithPriority (leftover s) filters + | otherwise = do + selected <- UTxOIndex.selectRandomWithPriority + (UTxOSelection.leftoverIndex s) filters + pure (updateState =<< selected) + where limitReached = case limit of - MaximumInputLimit m -> UTxOIndex.size (selected s) >= m + MaximumInputLimit m -> UTxOSelection.selectedCount s >= m NoLimit -> False - updateState ((i, o), remaining) = SelectionState - { leftover = remaining - , selected = UTxOIndex.insert i o (selected s) - } + updateState :: ((TxIn, TxOut), UTxOIndex) -> Maybe UTxOSelectionNonEmpty + updateState ((i, _o), _remaining) = UTxOSelection.select i s -------------------------------------------------------------------------------- -- Running a selection step @@ -1378,11 +1358,13 @@ selectMatchingQuantity filters limit s -- A 'SelectionLens' gives 'runSelectionStep' just the information it needs to -- make a decision, and no more. -- -data SelectionLens m state = SelectionLens +data SelectionLens m state state' = SelectionLens { currentQuantity :: state -> Natural + , updatedQuantity + :: state' -> Natural , selectQuantity - :: state -> m (Maybe state) + :: state -> m (Maybe state') , minimumQuantity :: Natural } @@ -1405,25 +1387,33 @@ data SelectionLens m state = SelectionLens -- output token quantity, but not further away. -- runSelectionStep - :: forall m state. Monad m - => SelectionLens m state + :: forall m state state'. Monad m + => SelectionLens m state state' -> state - -> m (Maybe state) + -> m (Maybe state') runSelectionStep lens s | currentQuantity s < minimumQuantity = selectQuantity s | otherwise = (requireImprovement =<<) <$> selectQuantity s where - SelectionLens {currentQuantity, selectQuantity, minimumQuantity} = lens - - requireImprovement :: state -> Maybe state + SelectionLens + { currentQuantity + , updatedQuantity + , minimumQuantity + , selectQuantity + } = lens + + requireImprovement :: state' -> Maybe state' requireImprovement s' - | distanceFromTarget s' < distanceFromTarget s = Just s' + | updatedDistanceFromTarget s' < currentDistanceFromTarget s = Just s' | otherwise = Nothing - distanceFromTarget :: state -> Natural - distanceFromTarget = distance targetQuantity . currentQuantity + currentDistanceFromTarget :: state -> Natural + currentDistanceFromTarget = distance targetQuantity . currentQuantity + + updatedDistanceFromTarget :: state' -> Natural + updatedDistanceFromTarget = distance targetQuantity . updatedQuantity targetQuantity :: Natural targetQuantity = minimumQuantity * 2 diff --git a/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Gen.hs b/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Gen.hs index 7a852ea82ce..a7af98d6429 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Gen.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Gen.hs @@ -5,10 +5,8 @@ module Cardano.Wallet.Primitive.CoinSelection.Gen ( genSelectionLimit , genSelectionSkeleton - , genSelectionState , shrinkSelectionLimit , shrinkSelectionSkeleton - , shrinkSelectionState ) where @@ -18,23 +16,15 @@ import Cardano.Wallet.Primitive.CoinSelection.Balance ( SelectionLimit , SelectionLimitOf (..) , SelectionSkeleton (..) - , SelectionState (..) ) import Cardano.Wallet.Primitive.Types.TokenMap.Gen ( genAssetId, genTokenMap, shrinkAssetId, shrinkTokenMap ) import Cardano.Wallet.Primitive.Types.Tx.Gen ( genTxOut, shrinkTxOut ) -import Cardano.Wallet.Primitive.Types.UTxOIndex.Gen - ( genUTxOIndex, shrinkUTxOIndex ) -import Data.Function - ( (&) ) -import Data.Generics.Internal.VL.Lens - ( over, view ) import Test.QuickCheck ( Gen , NonNegative (..) , arbitrary - , liftShrink2 , listOf , oneof , shrink @@ -44,7 +34,6 @@ import Test.QuickCheck import Test.QuickCheck.Extra ( liftShrink5 ) -import qualified Cardano.Wallet.Primitive.Types.UTxOIndex as UTxOIndex import qualified Data.Set as Set -------------------------------------------------------------------------------- @@ -110,28 +99,3 @@ shrinkSelectionSkeleton = skeletonToTuple (SelectionSkeleton a b c d e) = (a, b, c, d, e) tupleToSkeleton (a, b, c, d, e) = (SelectionSkeleton a b c d e) - --------------------------------------------------------------------------------- --- Selection states --------------------------------------------------------------------------------- - -genSelectionState :: Gen SelectionState -genSelectionState = - makeSelectionStateValid <$> genSelectionStateUnvalidated - where - genSelectionStateUnvalidated :: Gen SelectionState - genSelectionStateUnvalidated = SelectionState - <$> genUTxOIndex - <*> genUTxOIndex - -shrinkSelectionState :: SelectionState -> [SelectionState] -shrinkSelectionState = fmap makeSelectionStateValid <$> - shrinkMapBy tupleToState stateToTuple - (liftShrink2 shrinkUTxOIndex shrinkUTxOIndex) - where - stateToTuple (SelectionState a b) = (a, b) - tupleToState (a, b) = (SelectionState a b) - -makeSelectionStateValid :: SelectionState -> SelectionState -makeSelectionStateValid state = state - & over #leftover (`UTxOIndex.difference` view #selected state) diff --git a/lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelection/BalanceSpec.hs b/lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelection/BalanceSpec.hs index 531ee8e166f..13aa3f1a44d 100644 --- a/lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelection/BalanceSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelection/BalanceSpec.hs @@ -41,7 +41,6 @@ import Cardano.Wallet.Primitive.CoinSelection.Balance , SelectionResult , SelectionResultOf (..) , SelectionSkeleton (..) - , SelectionState (..) , UnableToConstructChangeError (..) , addMintValueToChangeMaps , addMintValuesToChangeMaps @@ -82,9 +81,7 @@ import Cardano.Wallet.Primitive.CoinSelection.Balance ) import Cardano.Wallet.Primitive.CoinSelection.Gen ( genSelectionLimit - , genSelectionState , shrinkSelectionLimit - , shrinkSelectionState ) import Cardano.Wallet.Primitive.Types.Address ( Address (..) ) @@ -124,13 +121,17 @@ import Cardano.Wallet.Primitive.Types.Tx , txOutMaxTokenQuantity ) import Cardano.Wallet.Primitive.Types.Tx.Gen - ( genTxOut, shrinkTxOut ) + ( coarbitraryTxIn, genTxOut, shrinkTxOut ) import Cardano.Wallet.Primitive.Types.UTxO ( UTxO (..) ) import Cardano.Wallet.Primitive.Types.UTxOIndex ( SelectionFilter (..), UTxOIndex ) import Cardano.Wallet.Primitive.Types.UTxOIndex.Gen ( genUTxOIndex, genUTxOIndexLarge, genUTxOIndexLargeN, shrinkUTxOIndex ) +import Cardano.Wallet.Primitive.Types.UTxOSelection + ( UTxOSelection, UTxOSelectionNonEmpty ) +import Cardano.Wallet.Primitive.Types.UTxOSelection.Gen + ( genUTxOSelection, shrinkUTxOSelection ) import Control.Monad ( forM_, replicateM ) import Data.Bifunctor @@ -174,6 +175,7 @@ import Test.Hspec.Extra import Test.QuickCheck ( Arbitrary (..) , Blind (..) + , CoArbitrary (..) , Fun , Gen , Positive (..) @@ -187,7 +189,6 @@ import Test.QuickCheck , cover , disjoin , elements - , forAll , frequency , generate , genericShrink @@ -219,6 +220,7 @@ import qualified Cardano.Wallet.Primitive.Types.TokenMap as TokenMap import qualified Cardano.Wallet.Primitive.Types.TokenQuantity as TokenQuantity import qualified Cardano.Wallet.Primitive.Types.UTxO as UTxO import qualified Cardano.Wallet.Primitive.Types.UTxOIndex as UTxOIndex +import qualified Cardano.Wallet.Primitive.Types.UTxOSelection as UTxOSelection import qualified Data.ByteString.Char8 as B8 import qualified Data.Foldable as F import qualified Data.List as L @@ -280,15 +282,6 @@ spec = describe "Cardano.Wallet.Primitive.CoinSelection.BalanceSpec" $ it "prop_performSelectionEmpty" $ property prop_performSelectionEmpty - parallel $ describe "Selection states" $ do - - it "prop_genSelectionState_coverage" $ - property prop_genSelectionState_coverage - it "prop_genSelectionState_valid" $ - property prop_genSelectionState_valid - it "prop_shrinkSelectionState_valid" $ - property prop_shrinkSelectionState_valid - parallel $ describe "Running a selection (without making change)" $ do it "prop_runSelection_UTxO_empty" $ @@ -1213,56 +1206,20 @@ mockPerformSelectionNonEmpty constraints params = Identity $ Right result deficitIn, deficitOut :: TokenBundle (deficitIn, deficitOut) = computeDeficitInOut params --------------------------------------------------------------------------------- --- Selection states --------------------------------------------------------------------------------- - -prop_genSelectionState_coverage :: Property -prop_genSelectionState_coverage = - forAll genSelectionState prop_genSelectionState_coverage_inner - -prop_genSelectionState_coverage_inner :: SelectionState -> Property -prop_genSelectionState_coverage_inner state = - checkCoverage $ - cover 0.1 (noneLeftover && noneSelected) "noneLeftover && noneSelected" $ - cover 1.0 (haveLeftover && noneSelected) "haveLeftover && noneSelected" $ - cover 1.0 (noneLeftover && haveSelected) "noneLeftover && haveSelected" $ - cover 8.0 (haveLeftover && haveSelected) "haveLeftover && haveSelected" $ - property True - where - haveLeftover = view #leftover state /= UTxOIndex.empty - noneLeftover = view #leftover state == UTxOIndex.empty - haveSelected = view #selected state /= UTxOIndex.empty - noneSelected = view #selected state == UTxOIndex.empty - -prop_genSelectionState_valid :: Property -prop_genSelectionState_valid = - forAll genSelectionState isSelectionStateValid - -prop_shrinkSelectionState_valid :: Property -prop_shrinkSelectionState_valid = - forAll genSelectionState $ \state -> - all isSelectionStateValid (shrinkSelectionState state) - -isSelectionStateValid :: SelectionState -> Bool -isSelectionStateValid state = Set.disjoint - (Map.keysSet $ unUTxO $ UTxOIndex.toUTxO $ view #selected state) - (Map.keysSet $ unUTxO $ UTxOIndex.toUTxO $ view #leftover state) - -------------------------------------------------------------------------------- -- Running a selection (without making change) -------------------------------------------------------------------------------- prop_runSelection_UTxO_empty :: TokenBundle -> Property prop_runSelection_UTxO_empty balanceRequested = monadicIO $ do - SelectionState {selected, leftover} <- - run $ runSelection RunSelectionParams + result <- run $ runSelection + RunSelectionParams { selectionLimit = NoLimit , utxoAvailable = UTxOIndex.empty , minimumBalance = balanceRequested } - let balanceSelected = view #balance selected - let balanceLeftover = view #balance leftover + let balanceSelected = UTxOSelection.selectedBalance result + let balanceLeftover = UTxOSelection.leftoverBalance result assertWith "balanceSelected == TokenBundle.empty" (balanceSelected == TokenBundle.empty) @@ -1274,14 +1231,14 @@ prop_runSelection_UTxO_notEnough :: Small UTxOIndex -> Property prop_runSelection_UTxO_notEnough (Small index) = monadicIO $ do - SelectionState {selected, leftover} <- - run $ runSelection RunSelectionParams + result <- run $ runSelection + RunSelectionParams { selectionLimit = NoLimit , utxoAvailable = index , minimumBalance = balanceRequested } - let balanceSelected = view #balance selected - let balanceLeftover = view #balance leftover + let balanceSelected = UTxOSelection.selectedBalance result + let balanceLeftover = UTxOSelection.leftoverBalance result assertWith "balanceSelected == balanceAvailable" (balanceSelected == balanceAvailable) @@ -1296,14 +1253,14 @@ prop_runSelection_UTxO_exactlyEnough :: Small UTxOIndex -> Property prop_runSelection_UTxO_exactlyEnough (Small index) = monadicIO $ do - SelectionState {selected, leftover} <- - run $ runSelection RunSelectionParams + result <- run $ runSelection + RunSelectionParams { selectionLimit = NoLimit , utxoAvailable = index , minimumBalance = balanceRequested } - let balanceSelected = view #balance selected - let balanceLeftover = view #balance leftover + let balanceSelected = UTxOSelection.selectedBalance result + let balanceLeftover = UTxOSelection.leftoverBalance result assertWith "balanceLeftover == TokenBundle.empty" (balanceLeftover == TokenBundle.empty) @@ -1322,14 +1279,14 @@ prop_runSelection_UTxO_moreThanEnough :: Small UTxOIndex -> Property prop_runSelection_UTxO_moreThanEnough (Small index) = monadicIO $ do - SelectionState {selected, leftover} <- - run $ runSelection RunSelectionParams + result <- run $ runSelection + RunSelectionParams { selectionLimit = NoLimit , utxoAvailable = index , minimumBalance = balanceRequested } - let balanceSelected = view #balance selected - let balanceLeftover = view #balance leftover + let balanceSelected = UTxOSelection.selectedBalance result + let balanceLeftover = UTxOSelection.leftoverBalance result monitor $ cover 80 (assetsRequested `Set.isProperSubsetOf` assetsAvailable) "assetsRequested ⊂ assetsAvailable" @@ -1366,14 +1323,14 @@ prop_runSelection_UTxO_muchMoreThanEnough (Blind (Large index)) = withMaxSuccess 100 $ checkCoverage $ monadicIO $ do - SelectionState {selected, leftover} <- - run $ runSelection RunSelectionParams + result <- run $ runSelection + RunSelectionParams { selectionLimit = NoLimit , utxoAvailable = index , minimumBalance = balanceRequested } - let balanceSelected = view #balance selected - let balanceLeftover = view #balance leftover + let balanceSelected = UTxOSelection.selectedBalance result + let balanceLeftover = UTxOSelection.leftoverBalance result monitor $ cover 80 (assetsRequested `Set.isProperSubsetOf` assetsAvailable) "assetsRequested ⊂ assetsAvailable" @@ -1406,14 +1363,8 @@ prop_runSelection_UTxO_muchMoreThanEnough (Blind (Large index)) = -- Running a selection (non-empty) -------------------------------------------------------------------------------- -prop_runSelectionNonEmpty :: SelectionState -> Property -prop_runSelectionNonEmpty result = conjoin - [ prop_genSelectionState_coverage_inner result - , prop_runSelectionNonEmpty_inner result - ] - -prop_runSelectionNonEmpty_inner :: SelectionState -> Property -prop_runSelectionNonEmpty_inner result = +prop_runSelectionNonEmpty :: UTxOSelection -> Property +prop_runSelectionNonEmpty result = case (haveLeftover, haveSelected) of (False, False) -> -- In this case, the available UTxO set was completely empty. @@ -1423,12 +1374,12 @@ prop_runSelectionNonEmpty_inner result = -- In this case, we've already selected all entries from the -- available UTxO, so there's no more work to do. We need to check -- that 'runSelectionNonEmpty' does not expand the selection: - maybeResultNonEmpty === Just result + maybeResultNonEmpty === UTxOSelection.toNonEmpty result (True, True) -> -- In this case, we've already selected some entries from the -- available UTxO, so there's no more work to do. We need to check -- that 'runSelectionNonEmpty' does not expand the selection: - maybeResultNonEmpty === Just result + maybeResultNonEmpty === UTxOSelection.toNonEmpty result (True, False) -> -- This represents the case where 'runSelection' does not select -- anything at all, even though we do have at least one UTxO entry @@ -1437,8 +1388,8 @@ prop_runSelectionNonEmpty_inner result = -- entry, and no more: checkResultNonEmpty where - haveLeftover = view #leftover result /= UTxOIndex.empty - haveSelected = view #selected result /= UTxOIndex.empty + haveLeftover = UTxOSelection.leftoverCount result > 0 + haveSelected = UTxOSelection.selectedCount result > 0 checkResultNonEmpty :: Property checkResultNonEmpty = checkSelectedElement & @@ -1448,27 +1399,30 @@ prop_runSelectionNonEmpty_inner result = checkSelectedElement = do resultNonEmpty <- maybeResultNonEmpty (i, o) <- matchSingletonList $ - UTxOIndex.toList $ view #selected resultNonEmpty + UTxOSelection.selectedList resultNonEmpty pure $ - UTxOIndex.insert i o (view #leftover resultNonEmpty) - === view #leftover result + UTxOIndex.insert i o + (UTxOSelection.leftoverIndex resultNonEmpty) + === UTxOSelection.leftoverIndex result - maybeResultNonEmpty :: Maybe SelectionState + maybeResultNonEmpty :: Maybe UTxOSelectionNonEmpty maybeResultNonEmpty = runIdentity $ runSelectionNonEmptyWith (Identity <$> mockSelectSingleEntry) (result) -mockSelectSingleEntry :: SelectionState -> Maybe SelectionState +mockSelectSingleEntry :: UTxOSelection -> Maybe UTxOSelectionNonEmpty mockSelectSingleEntry state = - selectEntry <$> firstLeftoverEntry state + selectEntry =<< firstLeftoverEntry state where - firstLeftoverEntry :: SelectionState -> Maybe (TxIn, TxOut) - firstLeftoverEntry = Map.lookupMin . unUTxO . UTxOIndex.toUTxO . leftover + firstLeftoverEntry :: UTxOSelection -> Maybe (TxIn, TxOut) + firstLeftoverEntry + = Map.lookupMin + . unUTxO + . UTxOIndex.toUTxO + . UTxOSelection.leftoverIndex - selectEntry :: (TxIn, TxOut) -> SelectionState - selectEntry (i, o) = state - & over #selected (UTxOIndex.insert i o) - & over #leftover (UTxOIndex.delete i) + selectEntry :: (TxIn, TxOut) -> Maybe UTxOSelectionNonEmpty + selectEntry (i, _o) = UTxOSelection.select i state -------------------------------------------------------------------------------- -- Running a selection step @@ -1488,9 +1442,10 @@ runMockSelectionStep :: MockSelectionStepData -> Maybe Natural runMockSelectionStep d = runIdentity $ runSelectionStep lens $ mockSelected d where - lens :: SelectionLens Identity Natural + lens :: SelectionLens Identity Natural Natural lens = SelectionLens { currentQuantity = id + , updatedQuantity = id , minimumQuantity = mockMinimum d , selectQuantity = \s -> pure $ (+ s) <$> mockNext d } @@ -1597,8 +1552,8 @@ prop_assetSelectionLens_givesPriorityToSingletonAssets (Blind (Small u)) = -- _something_ that matches. monitor $ counterexample "Error: unable to select any entry" assert False - Just SelectionState {selected} -> do - let output = head $ snd <$> UTxOIndex.toList selected + Just result -> do + let output = NE.head $ snd <$> UTxOSelection.selectedList result let bundle = view #tokens output case F.toList $ TokenBundle.getAssets bundle of [a] -> assertWith @@ -1610,7 +1565,7 @@ prop_assetSelectionLens_givesPriorityToSingletonAssets (Blind (Small u)) = where asset = Set.findMin $ UTxOIndex.assets u assetCount = Set.size $ UTxOIndex.assets u - initialState = SelectionState UTxOIndex.empty u + initialState = UTxOSelection.fromIndex u (const False) lens = assetSelectionLens NoLimit (asset, minimumAssetQuantity) minimumAssetQuantity = TokenQuantity 1 @@ -1633,15 +1588,15 @@ prop_coinSelectionLens_givesPriorityToCoins (Blind (Small u)) = -- _something_ that matches. monitor $ counterexample "Error: unable to select any entry" assert False - Just SelectionState {selected} -> do - let output = head $ snd <$> UTxOIndex.toList selected + Just result -> do + let output = NE.head $ snd <$> UTxOSelection.selectedList result let bundle = view #tokens output case F.toList $ TokenBundle.getAssets bundle of [] -> assertWith "hasCoin" ( hasCoin) _ -> assertWith "not hasCoin" (not hasCoin) where entryCount = UTxOIndex.size u - initialState = SelectionState UTxOIndex.empty u + initialState = UTxOSelection.fromIndex u (const False) lens = coinSelectionLens NoLimit minimumCoinQuantity minimumCoinQuantity = Coin 1 @@ -3929,9 +3884,9 @@ expectRight = \case Left _a -> error "Expected right" Right b -> b -matchSingletonList :: [a] -> Maybe a +matchSingletonList :: NonEmpty a -> Maybe a matchSingletonList = \case - [a] -> Just a + a :| [] -> Just a _ -> Nothing mockAsset :: ByteString -> AssetId @@ -3946,7 +3901,7 @@ unitTests lbl cases = it (lbl <> " example #" <> show @Int i) test -------------------------------------------------------------------------------- --- Arbitraries +-- Arbitrary instances -------------------------------------------------------------------------------- instance Arbitrary a => Arbitrary (NonEmpty a) where @@ -3995,10 +3950,6 @@ instance Arbitrary SelectionLimit where arbitrary = genSelectionLimit shrink = shrinkSelectionLimit -instance Arbitrary SelectionState where - arbitrary = genSelectionState - shrink = shrinkSelectionState - instance Arbitrary TokenMap where arbitrary = genTokenMapSmallRange shrink = shrinkTokenMap @@ -4011,6 +3962,10 @@ instance Arbitrary TxOut where arbitrary = genTxOut shrink = shrinkTxOut +instance Arbitrary UTxOSelection where + arbitrary = genUTxOSelection =<< arbitrary @(TxIn -> Bool) + shrink = shrinkUTxOSelection + newtype Large a = Large { getLarge :: a } deriving (Eq, Show) @@ -4050,3 +4005,10 @@ instance Arbitrary MockComputeMinimumAdaQuantity where instance Arbitrary MockComputeMinimumCost where arbitrary = genMockComputeMinimumCost shrink = shrinkMockComputeMinimumCost + +-------------------------------------------------------------------------------- +-- CoArbitrary instances +-------------------------------------------------------------------------------- + +instance CoArbitrary TxIn where + coarbitrary = coarbitraryTxIn