|
13 | 13 | {-# LANGUAGE TypeApplications #-} |
14 | 14 |
|
15 | 15 | module Cardano.Api.Experimental.Tx.Internal.Fee |
16 | | - ( TxBodyErrorAutoBalance (..) |
| 16 | + ( FeeCalculationError (..) |
| 17 | + , TxBodyErrorAutoBalance (..) |
17 | 18 | , TxFeeEstimationError (..) |
18 | 19 | , calculateMinimumUTxO |
| 20 | + , calcMinFeeRecursive |
19 | 21 | , collectTxBodyScriptWitnesses |
20 | 22 | , estimateBalancedTxBody |
21 | 23 | , evaluateTransactionExecutionUnits |
@@ -83,6 +85,7 @@ import Data.Maybe |
83 | 85 | import Data.OSet.Strict qualified as OSet |
84 | 86 | import Data.Ord (Down (Down), comparing) |
85 | 87 | import Data.Ratio |
| 88 | +import Data.Sequence.Strict qualified as Seq |
86 | 89 | import Data.Set (Set) |
87 | 90 | import Data.Set qualified as Set |
88 | 91 | import GHC.Exts (IsList (..)) |
@@ -657,6 +660,200 @@ evaluateTransactionFee |
657 | 660 | evaluateTransactionFee pp (UnsignedTx tx) keywitcount byronwitcount refScriptsSize = |
658 | 661 | L.estimateMinFeeTx pp tx (fromIntegral keywitcount) (fromIntegral byronwitcount) refScriptsSize |
659 | 662 |
|
| 663 | +data FeeCalculationError |
| 664 | + = -- | Updating an existing change output resulted in negative ADA. |
| 665 | + NotEnoughAdaForChangeOutput Coin |
| 666 | + | -- | Creating a new change output would require negative ADA. |
| 667 | + NotEnoughAdaForNewOutput Coin |
| 668 | + | NonAdaAssetsUnbalanced L.MultiAsset |
| 669 | + | -- | @MinUTxONotMet actual required@: an output does not meet the minimum UTxO requirement. |
| 670 | + MinUTxONotMet L.Coin L.Coin |
| 671 | + | FeeCalculationDidNotConverge |
| 672 | + deriving (Show, Eq) |
| 673 | + |
| 674 | +instance Error FeeCalculationError where |
| 675 | + prettyError (NotEnoughAdaForChangeOutput balance) = |
| 676 | + mconcat |
| 677 | + [ "Not enough ADA when updating existing change output. Balance: " |
| 678 | + , pretty balance |
| 679 | + , "\nThis means that the transaction does not have enough ada to cover the fees. The usual solution is to provide more inputs, or inputs with more ada." |
| 680 | + ] |
| 681 | + prettyError (NotEnoughAdaForNewOutput balance) = |
| 682 | + mconcat |
| 683 | + [ "Not enough ADA when creating new change output. Balance: " |
| 684 | + , pretty balance |
| 685 | + , "\nThis means that the transaction does not have enough ada to cover the fees. The usual solution is to provide more inputs, or inputs with more ada." |
| 686 | + ] |
| 687 | + prettyError (NonAdaAssetsUnbalanced multiAsset) = |
| 688 | + mconcat |
| 689 | + [ "Non-ADA assets are unbalanced: " |
| 690 | + , pshow multiAsset |
| 691 | + , "\nThe transaction inputs and minted values do not match the outputs for one or more native tokens." |
| 692 | + ] |
| 693 | + prettyError (MinUTxONotMet actual required) = |
| 694 | + mconcat |
| 695 | + [ "An output does not meet the minimum UTxO requirement." |
| 696 | + , "\nActual ADA in output: " <> pretty actual |
| 697 | + , "\nMinimum required: " <> pretty required |
| 698 | + , "\nThe usual solution is to provide more ADA inputs to cover the minimum UTxO for outputs carrying native tokens." |
| 699 | + ] |
| 700 | + prettyError FeeCalculationDidNotConverge = |
| 701 | + "Fee calculation did not converge after the maximum number of iterations." |
| 702 | + |
| 703 | +-- | Recursively calculate the minimum fee for a transaction and balance it. |
| 704 | +-- |
| 705 | +-- Starting from the provided transaction, this function iteratively adjusts |
| 706 | +-- the fee field and output values until the transaction is fully balanced |
| 707 | +-- (i.e. @inputs + mint + withdrawals + refunds = outputs + fee + deposits@ |
| 708 | +-- for all value components: ADA and every native token). |
| 709 | +-- |
| 710 | +-- Before entering the iterative loop the multi-asset balance is checked. |
| 711 | +-- Because fee adjustments only affect ADA, a negative multi-asset balance |
| 712 | +-- is unrecoverable and the function returns 'NonAdaAssetsUnbalanced' |
| 713 | +-- immediately. |
| 714 | +-- |
| 715 | +-- On each iteration the balance is computed via 'evaluateTransactionBalance' |
| 716 | +-- and the minimum fee via @calcMinFeeTx@. The function then proceeds based |
| 717 | +-- on the following cases, evaluated in order: |
| 718 | +-- |
| 719 | +-- * __Case 1 – Fee converged, balance is zero__: The transaction is fully |
| 720 | +-- balanced. Before returning, all outputs are checked against the minimum |
| 721 | +-- UTxO requirement ('MinUTxONotMet'). Note: a 'MinUTxONotMet' error at |
| 722 | +-- this point typically means that Case 2 distributed surplus multi-assets |
| 723 | +-- to an output on a prior iteration but there was not enough ADA surplus |
| 724 | +-- to satisfy the increased @coinPerUTxOByte@ requirement for that output. |
| 725 | +-- The remedy is to provide additional ADA inputs. |
| 726 | +-- |
| 727 | +-- * __Case 2 – Fee converged, non-zero balance__: There is surplus or |
| 728 | +-- deficit ADA, excess multi-assets (e.g. from minting), or both. A new |
| 729 | +-- change output is created at the provided change address with the |
| 730 | +-- balance and appended to the end of the existing outputs; if a change |
| 731 | +-- output already exists it is updated in place. If the resulting change |
| 732 | +-- output would have negative ADA, the transaction is unrecoverable and |
| 733 | +-- 'NotEnoughAdaForChangeOutput' or 'NotEnoughAdaForNewOutput' is returned. Otherwise the function recurses, because |
| 734 | +-- the changed output may alter the transaction size and therefore the |
| 735 | +-- required fee, and must also satisfy the minimum UTxO |
| 736 | +-- (@coinPerUTxOByte@) constraint. |
| 737 | +-- |
| 738 | +-- * __Case 3 – Fee has not converged__: The fee field is set to the newly |
| 739 | +-- computed minimum fee and the function recurses. |
| 740 | +-- |
| 741 | +-- A maximum iteration limit (currently 50) guards against non-termination. |
| 742 | +-- In practice convergence occurs within 2–3 iterations. |
| 743 | +calcMinFeeRecursive |
| 744 | + :: forall era |
| 745 | + . IsEra era |
| 746 | + => L.Addr |
| 747 | + -- ^ Change address. Any surplus value (ADA and/or native tokens) is |
| 748 | + -- sent to a new output at this address, appended at the end of the |
| 749 | + -- existing outputs. |
| 750 | + -> UnsignedTx (LedgerEra era) |
| 751 | + -> L.UTxO (LedgerEra era) |
| 752 | + -> L.PParams (LedgerEra era) |
| 753 | + -> Set PoolId |
| 754 | + -- ^ The set of registered stake pools. Pool registrations for pools |
| 755 | + -- already in this set are treated as re-registrations (no deposit |
| 756 | + -- required on the produced side). |
| 757 | + -> Map StakeCredential L.Coin |
| 758 | + -- ^ Deposits for stake credentials being deregistered in this |
| 759 | + -- transaction. These are counted as refunds on the consumed side. |
| 760 | + -> Map (Ledger.Credential Ledger.DRepRole) L.Coin |
| 761 | + -- ^ Deposits for DRep credentials being deregistered in this |
| 762 | + -- transaction. These are counted as refunds on the consumed side. |
| 763 | + -> Int |
| 764 | + -- ^ Number of extra key hashes for native scripts |
| 765 | + -> Either FeeCalculationError (UnsignedTx (LedgerEra era)) |
| 766 | +calcMinFeeRecursive changeAddr unsignedTx utxo pparams poolids stakeDelegDeposits drepDelegDeposits nExtraWitnesses |
| 767 | + -- If multi-assets are non-negative initially, they stay non-negative across |
| 768 | + -- iterations (only ADA and fee change), so check once upfront. |
| 769 | + | multiAssetIsNegative = |
| 770 | + Left $ NonAdaAssetsUnbalanced multiAssets |
| 771 | + | otherwise = |
| 772 | + go maxIterations unsignedTx |
| 773 | + where |
| 774 | + initialBalance = evaluateTransactionBalance pparams poolids stakeDelegDeposits drepDelegDeposits utxo unsignedTx |
| 775 | + multiAssets = |
| 776 | + obtainCommonConstraints (useEra @era) $ |
| 777 | + let L.MaryValue _ ma = initialBalance |
| 778 | + in ma |
| 779 | + -- Check whether any native token quantity is negative. |
| 780 | + -- ADA is zeroed out so it doesn't influence the check. |
| 781 | + multiAssetIsNegative = |
| 782 | + obtainCommonConstraints (useEra @era) $ |
| 783 | + not (L.pointwise (>=) (L.MaryValue (L.Coin 0) multiAssets) mempty) |
| 784 | + maxIterations :: Int |
| 785 | + maxIterations = 50 |
| 786 | + |
| 787 | + go |
| 788 | + :: Int |
| 789 | + -> UnsignedTx (LedgerEra era) |
| 790 | + -> Either FeeCalculationError (UnsignedTx (LedgerEra era)) |
| 791 | + go 0 _ = Left FeeCalculationDidNotConverge |
| 792 | + go n unSignTx@(UnsignedTx ledgerTx) |
| 793 | + | minFee == txBodyFee && L.isZero txBalanceValue = do |
| 794 | + -- Case 1 |
| 795 | + let outs = toList $ ledgerTx ^. L.bodyTxL . L.outputsTxBodyL |
| 796 | + mapM_ (checkOutputMinUTxO pparams) outs |
| 797 | + return unSignTx |
| 798 | + | minFee == txBodyFee = do |
| 799 | + -- Case 2 |
| 800 | + balancedOuts <- balanceTxOuts @era changeAddr txBalanceValue unSignTx |
| 801 | + let updatedTx = UnsignedTx (ledgerTx & L.bodyTxL . L.outputsTxBodyL .~ balancedOuts) |
| 802 | + go (n - 1) updatedTx |
| 803 | + | otherwise = |
| 804 | + -- Case 3 |
| 805 | + let newTx = UnsignedTx (ledgerTx & L.bodyTxL . L.feeTxBodyL .~ minFee) |
| 806 | + in go (n - 1) newTx |
| 807 | + where |
| 808 | + minFee = obtainCommonConstraints (useEra @era) $ L.calcMinFeeTx utxo pparams ledgerTx nExtraWitnesses |
| 809 | + txBodyFee = ledgerTx ^. L.bodyTxL . L.feeTxBodyL |
| 810 | + txBalanceValue = |
| 811 | + evaluateTransactionBalance pparams poolids stakeDelegDeposits drepDelegDeposits utxo unSignTx |
| 812 | + |
| 813 | +checkOutputMinUTxO |
| 814 | + :: forall era |
| 815 | + . IsEra era |
| 816 | + => Ledger.PParams (LedgerEra era) |
| 817 | + -> L.TxOut (LedgerEra era) |
| 818 | + -> Either FeeCalculationError () |
| 819 | +checkOutputMinUTxO pp out = |
| 820 | + obtainCommonConstraints (useEra @era) $ |
| 821 | + let txout = TxOut out |
| 822 | + in case checkMinUTxOValue pp txout of |
| 823 | + Right () -> Right () |
| 824 | + Left (TxOut offending, minRequired) -> |
| 825 | + Left $ MinUTxONotMet (offending ^. L.coinTxOutL) minRequired |
| 826 | + |
| 827 | +balanceTxOuts |
| 828 | + :: forall era |
| 829 | + . HasCallStack |
| 830 | + => IsEra era |
| 831 | + => L.Addr |
| 832 | + -> L.Value (LedgerEra era) |
| 833 | + -> UnsignedTx (LedgerEra era) |
| 834 | + -> Either FeeCalculationError (Seq.StrictSeq (L.TxOut (LedgerEra era))) |
| 835 | +balanceTxOuts changeAddr txBalance (UnsignedTx tx) = |
| 836 | + obtainCommonConstraints (useEra @era) $ |
| 837 | + let outs = tx ^. L.bodyTxL . L.outputsTxBodyL |
| 838 | + in case outs of |
| 839 | + rest Seq.:|> lastOut |
| 840 | + | lastOut ^. L.addrTxOutL == changeAddr -> |
| 841 | + -- Update existing change output in place. |
| 842 | + -- We compute the new value before writing it into the TxOut, |
| 843 | + -- because the ledger's TxOut setter throws an exception on |
| 844 | + -- negative values. |
| 845 | + let newValue = (lastOut ^. L.valueTxOutL) <> txBalance |
| 846 | + changeCoin = L.coin newValue |
| 847 | + in if changeCoin < 0 |
| 848 | + then Left $ NotEnoughAdaForChangeOutput changeCoin |
| 849 | + else Right $ rest Seq.:|> (lastOut & L.valueTxOutL .~ newValue) |
| 850 | + _ -> |
| 851 | + -- Append a new change output |
| 852 | + let changeCoin = L.coin txBalance |
| 853 | + in if changeCoin < 0 |
| 854 | + then Left $ NotEnoughAdaForNewOutput changeCoin |
| 855 | + else Right $ outs Seq.:|> L.mkBasicTxOut changeAddr txBalance |
| 856 | + |
660 | 857 | -- Essentially we check for the existence of collateral inputs. If they exist we |
661 | 858 | -- create a fictitious collateral return output. Why? Because we need to put dummy values |
662 | 859 | -- to get a fee estimate (i.e we overestimate the fee). The required collateral depends |
|
0 commit comments