Skip to content

Commit 667e1b0

Browse files
author
Robert 'Probie' Offner
committed
Auto-balance multiasset transactions
Previously we gave up when the non-Ada part of a transaction wasn't balanced. We now balance the transaction and correctly update the fee accordingly (since the fee will be higher). We also return an error in the case where the is non-Ada change, but not at least minUTxO change (e.g. in the case where the Ada is already balanced). Resolves: #3068
1 parent 40458ee commit 667e1b0

File tree

5 files changed

+88
-21
lines changed

5 files changed

+88
-21
lines changed

cardano-api/ChangeLog.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
- Expose convenience functions `executeQueryCardanoMode`, `determineEra`, `constructBalancedTx` and `queryStateForBalancedTx` ([PR 4446](https://github.com/input-output-hk/cardano-node/pull/4446))
1010

11+
- Auto-balance multi asset transactions ([PR 4450](https://github.com/input-output-hk/cardano-node/pull/4450))
12+
1113
### Bugs
1214

1315
- Allow reading text envelopes from pipes ([PR 4384](https://github.com/input-output-hk/cardano-node/pull/4384))

cardano-api/cardano-api.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ test-suite cardano-api-test
227227
, time
228228

229229
other-modules: Test.Cardano.Api.Crypto
230+
Test.Cardano.Api.Fees
230231
Test.Cardano.Api.Genesis
231232
Test.Cardano.Api.Json
232233
Test.Cardano.Api.KeysByron

cardano-api/src/Cardano/Api/Fees.hs

+20-21
Original file line numberDiff line numberDiff line change
@@ -763,11 +763,6 @@ data TxBodyErrorAutoBalance =
763763
-- | One or more of the scripts were expected to fail validation, but none did.
764764
| TxBodyScriptBadScriptValidity
765765

766-
-- | The balance of the non-ada assets is not zero. The 'Value' here is
767-
-- that residual non-zero balance. The 'makeTransactionBodyAutoBalance'
768-
-- function only automatically balances ada, not other assets.
769-
| TxBodyErrorAssetBalanceWrong Value
770-
771766
-- | There is not enough ada to cover both the outputs and the fees.
772767
-- The transaction should be changed to provide more input ada, or
773768
-- otherwise adjusted to need less (e.g. outputs, script etc).
@@ -826,13 +821,6 @@ instance Error TxBodyErrorAutoBalance where
826821
displayError TxBodyScriptBadScriptValidity =
827822
"One or more of the scripts were expected to fail validation, but none did."
828823

829-
displayError (TxBodyErrorAssetBalanceWrong _value) =
830-
"The transaction does not correctly balance in its non-ada assets. "
831-
++ "The balance between inputs and outputs should sum to zero. "
832-
++ "The actual balance is: "
833-
++ "TODO: move the Value renderer and parser from the CLI into the API and use them here"
834-
-- TODO: do this ^^
835-
836824
displayError (TxBodyErrorAdaBalanceNegative lovelace) =
837825
"The transaction does not balance in its use of ada. The net balance "
838826
++ "of the transaction is negative: " ++ show lovelace ++ " lovelace. "
@@ -971,13 +959,23 @@ makeTransactionBodyAutoBalance eraInMode systemstart history pparams
971959
-- output and fee. Yes this means this current code will only work for
972960
-- final fee of less than around 4000 ada (2^32-1 lovelace) and change output
973961
-- of less than around 18 trillion ada (2^64-1 lovelace).
962+
-- However, since at this point we know how much non-Ada change to give
963+
-- we can use the true values for that.
964+
965+
let outgoingNonAda = mconcat [filterValue isNotAda v | (TxOut _ (TxOutValue _ v) _ _) <- txOuts txbodycontent]
966+
let incomingNonAda = mconcat [filterValue isNotAda v | (TxOut _ (TxOutValue _ v) _ _) <- Map.elems $ unUTxO utxo]
967+
let nonAdaChange = incomingNonAda <> negateValue outgoingNonAda
968+
969+
let changeTxOut = case multiAssetSupportedInEra cardanoEra of
970+
Left _ -> lovelaceToTxOutValue $ Lovelace (2^(64 :: Integer)) - 1
971+
Right multiAsset -> TxOutValue multiAsset (lovelaceToValue (Lovelace (2^(64 :: Integer)) - 1) <> nonAdaChange)
974972

975973
let (dummyCollRet, dummyTotColl) = maybeDummyTotalCollAndCollReturnOutput txbodycontent changeaddr
976974
txbody1 <- first TxBodyError $ -- TODO: impossible to fail now
977975
createAndValidateTransactionBody txbodycontent1 {
978976
txFee = TxFeeExplicit explicitTxFees $ Lovelace (2^(32 :: Integer) - 1),
979977
txOuts = TxOut changeaddr
980-
(lovelaceToTxOutValue $ Lovelace (2^(64 :: Integer)) - 1)
978+
changeTxOut
981979
TxOutDatumNone ReferenceScriptNone
982980
: txOuts txbodycontent,
983981
txReturnCollateral = dummyCollRet,
@@ -1009,13 +1007,7 @@ makeTransactionBodyAutoBalance eraInMode systemstart history pparams
10091007

10101008
-- check if the balance is positive or negative
10111009
-- in one case we can produce change, in the other the inputs are insufficient
1012-
case balance of
1013-
TxOutAdaOnly _ _ -> balanceCheck balance
1014-
TxOutValue _ v ->
1015-
case valueToLovelace v of
1016-
Nothing -> Left $ TxBodyErrorNonAdaAssetsUnbalanced v
1017-
Just _ -> balanceCheck balance
1018-
1010+
balanceCheck balance
10191011

10201012
--TODO: we could add the extra fee for the CBOR encoding of the change,
10211013
-- now that we know the magnitude of the change: i.e. 1-8 bytes extra.
@@ -1135,7 +1127,7 @@ makeTransactionBodyAutoBalance eraInMode systemstart history pparams
11351127

11361128
balanceCheck :: TxOutValue era -> Either TxBodyErrorAutoBalance ()
11371129
balanceCheck balance
1138-
| txOutValueToLovelace balance == 0 = return ()
1130+
| txOutValueToLovelace balance == 0 && onlyAda (txOutValueToValue balance) = return ()
11391131
| txOutValueToLovelace balance < 0 =
11401132
Left . TxBodyErrorAdaBalanceNegative $ txOutValueToLovelace balance
11411133
| otherwise =
@@ -1145,6 +1137,13 @@ makeTransactionBodyAutoBalance eraInMode systemstart history pparams
11451137
Left err -> Left err
11461138
Right _ -> Right ()
11471139

1140+
isNotAda :: AssetId -> Bool
1141+
isNotAda AdaAssetId = False
1142+
isNotAda _ = True
1143+
1144+
onlyAda :: Value -> Bool
1145+
onlyAda = null . valueToList . filterValue isNotAda
1146+
11481147
checkMinUTxOValue
11491148
:: TxOut CtxTx era
11501149
-> ProtocolParameters
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
module Test.Cardano.Api.Fees (tests) where
2+
3+
import Data.Either (isLeft)
4+
import Data.Maybe (fromJust)
5+
import Data.Time.Format.ISO8601 (iso8601ParseM)
6+
import Gen.Cardano.Api.Typed (genValueDefault)
7+
import Hedgehog (Property, assert, forAll, property)
8+
import Hedgehog.Gen (list)
9+
import Hedgehog.Range (linear)
10+
import Prelude
11+
import Test.Tasty (TestTree, testGroup)
12+
import Test.Tasty.Hedgehog (testPropertyNamed)
13+
14+
import Cardano.Api
15+
import Cardano.Api.Shelley (shelleyGenesisDefaults)
16+
import Ouroboros.Consensus.HardFork.History (mkInterpreter, summarize)
17+
import Ouroboros.Consensus.Shelley.Node (ShelleyGenesis (..))
18+
19+
runAutoBalance :: [Value] -> [Value] -> Either TxBodyErrorAutoBalance (BalancedTxBody AlonzoEra)
20+
runAutoBalance _inValues _outValues = makeTransactionBodyAutoBalance
21+
AlonzoEraInCardanoMode
22+
(SystemStart $ fromJust $ iso8601ParseM "1970-01-01Z00:00:00")
23+
(EraHistory CardanoMode (mkInterpreter (summarize _ _ _)))
24+
pparams
25+
_
26+
_
27+
TxBodyContent
28+
{ txIns = _
29+
, txInsCollateral = _
30+
, txInsReference = _
31+
, txOuts = _
32+
, txTotalCollateral = _
33+
, txReturnCollateral = _
34+
, txFee = _
35+
, txValidityRange = _
36+
, txMetadata = _
37+
, txAuxScripts = _
38+
, txExtraKeyWits = _
39+
, txProtocolParams = BuildTxWith (Just pparams)
40+
, txWithdrawals = _
41+
, txCertificates = _
42+
, txUpdateProposal = TxUpdateProposalNone
43+
, txMintValue = _
44+
, txScriptValidity = _
45+
}
46+
(AddressInEra
47+
(ShelleyAddressInEra ShelleyBasedEraAlonzo)
48+
(makeShelleyAddress (Testnet (NetworkMagic 42))
49+
(PaymentCredentialByKey _)
50+
NoStakeAddress))
51+
Nothing
52+
where
53+
pparams = _ (sgProtocolParams shelleyGenesisDefaults)
54+
55+
prop_noFeeFailure :: Property
56+
prop_noFeeFailure = property $ do
57+
v <- forAll $ list (linear 0 10) genValueDefault
58+
assert $ isLeft $ runAutoBalance v v
59+
60+
tests :: TestTree
61+
tests =
62+
testGroup "Cardano.Api.Fees"
63+
[ testPropertyNamed "fail on no fee" "fail on no fee" prop_noFeeFailure]

cardano-api/test/cardano-api-test.hs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Cardano.Prelude
55
import Test.Tasty (TestTree, defaultMain, testGroup)
66

77
import qualified Test.Cardano.Api.Crypto
8+
import qualified Test.Cardano.Api.Fees
89
import qualified Test.Cardano.Api.Json
910
import qualified Test.Cardano.Api.KeysByron
1011
import qualified Test.Cardano.Api.Ledger
@@ -30,6 +31,7 @@ tests :: TestTree
3031
tests =
3132
testGroup "Cardano.Api"
3233
[ Test.Cardano.Api.Crypto.tests
34+
, Test.Cardano.Api.Fees.tests
3335
, Test.Cardano.Api.Json.tests
3436
, Test.Cardano.Api.KeysByron.tests
3537
, Test.Cardano.Api.Ledger.tests

0 commit comments

Comments
 (0)