Skip to content

Commit 08bf43b

Browse files
Fully internalise balancing, fix tests
1 parent 9cf0c14 commit 08bf43b

File tree

8 files changed

+181
-187
lines changed

8 files changed

+181
-187
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,13 @@ The fake PAB consists of the following modules:
125125
- **BotPlutusInterface** main entry point
126126
- **BotPlutusInterface.Server** Servant server, handling http endpoint calls and websockets
127127
- **BotPlutusInterface.Contract** handling contract effects by creating the necessary files and calling cardano-cli commands (a few effects are mocked)
128-
- **BotPlutusInterface.PreBalance** doing some preparations so the cli can process the rest (non-ada asset balancing, addig tx inputs, adding minimum lovelaces, add signatories)
128+
- **BotPlutusInterface.Balance** doing some preparations so the cli can process the rest (non-ada asset balancing, addig tx inputs, adding minimum lovelaces, add signatories)
129129
- **BotPlutusInterface.CardanoCLI** wrappers for cardano-cli commands
130130
- For development purposes, I created an ssh wrapper, so I can call relay these commands through an ssh connection. This is not nice, unsafe, and pretty slow, avoid using it if you can.
131131
- **BotPlutusInterface.UtxoParser** parse the output of the `cardano-cli query utxo` command
132132
- **BotPlutusInterface.Files** functions for handling script, datum and redeemer files
133133
- **BotPlutusInterface.Types** configuration for the fake pab
134-
- **BotPlutusInterface.PreBalance** prepare a transaction before sending to the cli for balancing. This includes:
134+
- **BotPlutusInterface.Balance** prepare a transaction before sending to the cli for balancing. This includes:
135135
- adding tx inputs to cover fees and outputs
136136
- adding collaterals,
137137
- modifying tx outs to contain the minimum amount of lovelaces

bot-plutus-interface.cabal

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ library
8181
BotPlutusInterface.Contract
8282
BotPlutusInterface.Effects
8383
BotPlutusInterface.Files
84-
BotPlutusInterface.PreBalance
84+
BotPlutusInterface.Balance
8585
BotPlutusInterface.Types
8686
BotPlutusInterface.UtxoParser
8787

@@ -143,7 +143,7 @@ test-suite bot-plutus-interface-test
143143
ghc-options: -fplugin-opt PlutusTx.Plugin:defer-errors
144144
other-modules:
145145
Spec.BotPlutusInterface.Contract
146-
Spec.BotPlutusInterface.PreBalance
146+
Spec.BotPlutusInterface.Balance
147147
Spec.BotPlutusInterface.UtxoParser
148148
Spec.MockContract
149149

src/BotPlutusInterface/PreBalance.hs renamed to src/BotPlutusInterface/Balance.hs

Lines changed: 43 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
{-# LANGUAGE AllowAmbiguousTypes #-}
22

3-
module BotPlutusInterface.PreBalance (
4-
preBalanceTx,
5-
preBalanceTxIO,
3+
module BotPlutusInterface.Balance (
4+
balanceTxStep,
5+
balanceTxIO,
66
) where
77

88
import BotPlutusInterface.CardanoCLI qualified as CardanoCLI
99
import BotPlutusInterface.Effects (PABEffect, createDirectoryIfMissing, printLog)
1010
import BotPlutusInterface.Files (DummyPrivKey, unDummyPrivateKey)
1111
import BotPlutusInterface.Files qualified as Files
1212
import BotPlutusInterface.Types (LogLevel (Debug), PABConfig)
13-
import Cardano.Api.Shelley (Lovelace (Lovelace), ProtocolParameters (protocolParamUTxOCostPerWord))
1413
import Control.Monad (foldM, void, zipWithM)
1514
import Control.Monad.Freer (Eff, Member)
1615
import Control.Monad.Trans.Class (lift)
1716
import Control.Monad.Trans.Either (EitherT, hoistEither, newEitherT, runEitherT)
18-
import Data.Either.Combinators (maybeToRight, rightToMaybe)
17+
import Data.Either.Combinators (rightToMaybe)
1918
import Data.Kind (Type)
2019
import Data.List (partition, (\\))
2120
import Data.Map (Map)
@@ -47,27 +46,26 @@ import Ledger.Tx (
4746
TxOutRef (..),
4847
)
4948
import Ledger.Tx qualified as Tx
50-
import Ledger.Value (Value (Value), getValue)
49+
import Ledger.Value (Value)
5150
import Ledger.Value qualified as Value
5251
import Plutus.V1.Ledger.Api (
5352
Credential (PubKeyCredential, ScriptCredential),
5453
CurrencySymbol (..),
5554
TokenName (..),
5655
)
57-
import PlutusTx.AssocMap qualified as AssocMap
5856
import Prelude
5957

6058
{- | Collect necessary tx inputs and collaterals, add minimum lovelace values and balance non ada
6159
assets
6260
-}
63-
preBalanceTxIO ::
61+
balanceTxIO ::
6462
forall (w :: Type) (effs :: [Type -> Type]).
6563
Member (PABEffect w) effs =>
6664
PABConfig ->
6765
PubKeyHash ->
6866
UnbalancedTx ->
6967
Eff effs (Either Text Tx)
70-
preBalanceTxIO pabConf ownPkh unbalancedTx =
68+
balanceTxIO pabConf ownPkh unbalancedTx =
7169
runEitherT $
7270
do
7371
utxos <- newEitherT $ CardanoCLI.utxosAt @w pabConf $ Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
@@ -83,16 +81,23 @@ preBalanceTxIO pabConf ownPkh unbalancedTx =
8381

8482
lift $ printLog @w Debug $ show utxoIndex
8583

86-
loop utxoIndex privKeys requiredSigs [] tx
84+
-- Adds required collaterals, only needs to happen once
85+
-- Also adds signatures for fee calculation
86+
preBalancedTx <- hoistEither $ addTxCollaterals utxoIndex tx >>= addSignatories ownPkh privKeys requiredSigs
87+
88+
-- Balance the tx
89+
balancedTx <- loop utxoIndex privKeys [] preBalancedTx
90+
91+
-- finally, we must update the signatories
92+
hoistEither $ addSignatories ownPkh privKeys requiredSigs balancedTx
8793
where
8894
loop ::
8995
Map TxOutRef TxOut ->
9096
Map PubKeyHash DummyPrivKey ->
91-
[PubKeyHash] ->
9297
[(TxOut, Integer)] ->
9398
Tx ->
9499
EitherT Text (Eff effs) Tx
95-
loop utxoIndex privKeys requiredSigs prevMinUtxos tx = do
100+
loop utxoIndex privKeys prevMinUtxos tx = do
96101
void $ lift $ Files.writeAll @w pabConf tx
97102
nextMinUtxos <-
98103
newEitherT $
@@ -102,20 +107,24 @@ preBalanceTxIO pabConf ownPkh unbalancedTx =
102107

103108
lift $ printLog @w Debug $ "Min utxos: " ++ show minUtxos
104109

110+
-- Calculate fees by pre-balancing the tx, building it, and running the CLI on result
105111
txWithoutFees <-
106-
hoistEither $ preBalanceTx pabConf.pcProtocolParams minUtxos 0 utxoIndex ownPkh privKeys requiredSigs tx
112+
hoistEither $ balanceTxStep minUtxos 0 utxoIndex ownPkh tx
107113

108114
lift $ createDirectoryIfMissing @w False (Text.unpack pabConf.pcTxFileDir)
109-
newEitherT $ CardanoCLI.buildTx @w pabConf privKeys ownPkh (CardanoCLI.BuildRaw 0) txWithoutFees
115+
newEitherT $ CardanoCLI.buildTx @w pabConf privKeys txWithoutFees
110116
fees <- newEitherT $ CardanoCLI.calculateMinFee @w pabConf txWithoutFees
111117

112118
lift $ printLog @w Debug $ "Fees: " ++ show fees
113119

114-
balancedTx <- hoistEither $ preBalanceTx pabConf.pcProtocolParams minUtxos fees utxoIndex ownPkh privKeys requiredSigs tx
120+
-- Rebalance the initial tx with the above fees
121+
balancedTx <- hoistEither $ balanceTxStep minUtxos fees utxoIndex ownPkh tx
122+
123+
let balanceTxWithFees = balancedTx {txFee = Ada.lovelaceValueOf fees}
115124

116-
if balancedTx == tx
117-
then pure balancedTx
118-
else loop utxoIndex privKeys requiredSigs minUtxos balancedTx
125+
if balanceTxWithFees == tx
126+
then pure balanceTxWithFees
127+
else loop utxoIndex privKeys minUtxos balanceTxWithFees
119128

120129
calculateMinUtxos ::
121130
forall (w :: Type) (effs :: [Type -> Type]).
@@ -127,24 +136,17 @@ calculateMinUtxos ::
127136
calculateMinUtxos pabConf datums txOuts =
128137
zipWithM (fmap . (,)) txOuts <$> mapM (CardanoCLI.calculateMinUtxo @w pabConf datums) txOuts
129138

130-
preBalanceTx ::
131-
ProtocolParameters ->
139+
balanceTxStep ::
132140
[(TxOut, Integer)] ->
133141
Integer ->
134142
Map TxOutRef TxOut ->
135143
PubKeyHash ->
136-
Map PubKeyHash DummyPrivKey ->
137-
[PubKeyHash] ->
138144
Tx ->
139145
Either Text Tx
140-
preBalanceTx pparams minUtxos fees utxos ownPkh privKeys requiredSigs tx =
141-
addTxCollaterals utxos tx
142-
>>= balanceTxIns pparams utxos fees
143-
>>= balanceNonAdaOuts ownPkh utxos
144-
>>= Right . addLovelaces minUtxos
145-
>>= balanceTxIns pparams utxos fees -- Adding more inputs if required
146-
>>= balanceNonAdaOuts ownPkh utxos
147-
>>= addSignatories ownPkh privKeys requiredSigs
146+
balanceTxStep minUtxos fees utxos ownPkh tx =
147+
Right (addLovelaces minUtxos tx)
148+
>>= balanceTxIns utxos fees
149+
>>= handleChange ownPkh utxos fees
148150

149151
-- | Getting the necessary utxos to cover the fees for the transaction
150152
collectTxIns :: Set TxIn -> Map TxOutRef TxOut -> Value -> Either Text (Set TxIn)
@@ -202,18 +204,13 @@ addLovelaces minLovelaces tx =
202204
$ txOutputs tx
203205
in tx {txOutputs = lovelacesAdded}
204206

205-
balanceTxIns :: ProtocolParameters -> Map TxOutRef TxOut -> Integer -> Tx -> Either Text Tx
206-
balanceTxIns pparams utxos fees tx = do
207-
Lovelace utxoCost <-
208-
maybeToRight "UTxOCostPerWord parameter not found" $ protocolParamUTxOCostPerWord pparams
207+
balanceTxIns :: Map TxOutRef TxOut -> Integer -> Tx -> Either Text Tx
208+
balanceTxIns utxos fees tx = do
209209
let txOuts = Tx.txOutputs tx
210210
nonMintedValue = mconcat (map Tx.txOutValue txOuts) `minus` txMint tx
211-
-- An ada-only UTxO entry is 29 words. More details about min utxo calculation can be found here:
212-
-- https://github.com/cardano-foundation/CIPs/tree/master/CIP-0028#rationale-for-parameter-choices
213-
changeMinUtxo = 29 * utxoCost
214211
minSpending =
215212
mconcat
216-
[ Ada.lovelaceValueOf (fees + changeMinUtxo)
213+
[ Ada.lovelaceValueOf fees
217214
, nonMintedValue
218215
]
219216
txIns <- collectTxIns (txInputs tx) utxos minSpending
@@ -235,28 +232,28 @@ addTxCollaterals utxos tx = do
235232
_ -> Left "There are no utxos to be used as collateral"
236233
filterAdaOnly = Map.filter (isAdaOnly . txOutValue)
237234

238-
-- | We need to balance non ada values, as the cardano-cli is unable to balance them (as of 2021/09/24)
239-
balanceNonAdaOuts :: PubKeyHash -> Map TxOutRef TxOut -> Tx -> Either Text Tx
240-
balanceNonAdaOuts ownPkh utxos tx =
235+
-- | Ensures all change goes back to user
236+
handleChange :: PubKeyHash -> Map TxOutRef TxOut -> Integer -> Tx -> Either Text Tx
237+
handleChange ownPkh utxos fees tx =
241238
let changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
242239
txInRefs = map Tx.txInRef $ Set.toList $ txInputs tx
243240
inputValue = mconcat $ map Tx.txOutValue $ mapMaybe (`Map.lookup` utxos) txInRefs
244241
outputValue = mconcat $ map Tx.txOutValue $ txOutputs tx
245242
nonMintedOutputValue = outputValue `minus` txMint tx
246-
nonAdaChange = filterNonAda inputValue `minus` filterNonAda nonMintedOutputValue
243+
change = (inputValue `minus` nonMintedOutputValue) `minus` Ada.lovelaceValueOf fees
247244
outputs =
248245
case partition ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx of
249246
([], txOuts) ->
250247
TxOut
251248
{ txOutAddress = changeAddr
252-
, txOutValue = nonAdaChange
249+
, txOutValue = change
253250
, txOutDatumHash = Nothing
254251
} :
255252
txOuts
256253
(txOut@TxOut {txOutValue = v} : txOuts, txOuts') ->
257-
txOut {txOutValue = v <> nonAdaChange} : (txOuts <> txOuts')
258-
in if isValueNat nonAdaChange
259-
then Right $ if Value.isZero nonAdaChange then tx else tx {txOutputs = outputs}
254+
txOut {txOutValue = v <> change} : (txOuts <> txOuts')
255+
in if isValueNat change
256+
then Right $ if Value.isZero change then tx else tx {txOutputs = outputs}
260257
else Left "Not enough inputs to balance tokens."
261258

262259
{- | Add the required signatorioes to the transaction. Be aware the the signature itself is invalid,
@@ -289,14 +286,6 @@ validateRange _ = True
289286
showText :: forall (a :: Type). Show a => a -> Text
290287
showText = Text.pack . show
291288

292-
-- | Filter by key for Associated maps (why doesn't this exist?)
293-
filterKey :: (k -> Bool) -> AssocMap.Map k v -> AssocMap.Map k v
294-
filterKey f = AssocMap.mapMaybeWithKey $ \k v -> if f k then Just v else Nothing
295-
296-
-- | Filter a value to contain only non ada assets
297-
filterNonAda :: Value -> Value
298-
filterNonAda = Value . filterKey (/= Ada.adaSymbol) . getValue
299-
300289
minus :: Value -> Value -> Value
301290
minus x y =
302291
let negativeValues = map (\(c, t, a) -> (c, t, - a)) $ Value.flattenValue y

src/BotPlutusInterface/CardanoCLI.hs

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
{-# LANGUAGE NamedFieldPuns #-}
33

44
module BotPlutusInterface.CardanoCLI (
5-
BuildMode (..),
65
submitTx,
76
calculateMinUtxo,
87
calculateMinFee,
@@ -80,6 +79,7 @@ import Ledger.TxId (TxId (..))
8079
import Ledger.Value (Value)
8180
import Ledger.Value qualified as Value
8281
import Plutus.Contract.CardanoAPI (toCardanoAddress)
82+
import Plutus.V1.Ledger.Ada (fromValue, getLovelace)
8383
import Plutus.V1.Ledger.Api (
8484
BuiltinData,
8585
CurrencySymbol (..),
@@ -190,27 +190,17 @@ calculateMinFee pabConf tx =
190190
, cmdOutParser = mapLeft Text.pack . parseOnly UtxoParser.feeParser . Text.pack
191191
}
192192

193-
data BuildMode = BuildRaw Integer | BuildAuto
194-
deriving stock (Show)
195-
196-
isRawBuildMode :: BuildMode -> Bool
197-
isRawBuildMode (BuildRaw _) = True
198-
isRawBuildMode _ = False
199-
200193
-- | Build a tx body and write it to disk
201194
buildTx ::
202195
forall (w :: Type) (effs :: [Type -> Type]).
203196
Member (PABEffect w) effs =>
204197
PABConfig ->
205198
Map PubKeyHash DummyPrivKey ->
206-
PubKeyHash ->
207-
BuildMode ->
208199
Tx ->
209200
Eff effs (Either Text ())
210-
buildTx pabConf privKeys ownPkh buildMode tx =
201+
buildTx pabConf privKeys tx =
211202
callCommand @w $ ShellArgs "cardano-cli" opts (const ())
212203
where
213-
ownAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
214204
requiredSigners =
215205
concatMap
216206
( \pubKey ->
@@ -226,20 +216,14 @@ buildTx pabConf privKeys ownPkh buildMode tx =
226216
(Map.keys (Ledger.txSignatures tx))
227217
opts =
228218
mconcat
229-
[ ["transaction", if isRawBuildMode buildMode then "build-raw" else "build", "--alonzo-era"]
230-
, txInOpts pabConf buildMode (txInputs tx)
219+
[ ["transaction", "build-raw", "--alonzo-era"]
220+
, txInOpts pabConf (txInputs tx)
231221
, txInCollateralOpts (txCollateral tx)
232222
, txOutOpts pabConf (txData tx) (txOutputs tx)
233-
, mintOpts pabConf buildMode (txMintScripts tx) (txRedeemers tx) (txMint tx)
223+
, mintOpts pabConf (txMintScripts tx) (txRedeemers tx) (txMint tx)
234224
, validRangeOpts (txValidRange tx)
235225
, requiredSigners
236-
, case buildMode of
237-
BuildRaw fee -> ["--fee", showText fee]
238-
BuildAuto ->
239-
mconcat
240-
[ ["--change-address", unsafeSerialiseAddress pabConf.pcNetwork ownAddr]
241-
, networkOpt pabConf
242-
]
226+
, ["--fee", showText . getLovelace . fromValue $ txFee tx]
243227
, mconcat
244228
[ ["--protocol-params-file", pabConf.pcProtocolParamsFile]
245229
, ["--out-file", txFilePath pabConf "raw" tx]
@@ -289,8 +273,8 @@ submitTx pabConf tx =
289273
)
290274
(const ())
291275

292-
txInOpts :: PABConfig -> BuildMode -> Set TxIn -> [Text]
293-
txInOpts pabConf buildMode =
276+
txInOpts :: PABConfig -> Set TxIn -> [Text]
277+
txInOpts pabConf =
294278
concatMap
295279
( \(TxIn txOutRef txInType) ->
296280
mconcat
@@ -315,9 +299,10 @@ txInOpts pabConf buildMode =
315299
[ "--tx-in-redeemer-file"
316300
, redeemerJsonFilePath pabConf (Ledger.redeemerHash redeemer)
317301
]
318-
, if isRawBuildMode buildMode
319-
then ["--tx-in-execution-units", exBudgetToCliArg exBudget]
320-
else []
302+
,
303+
[ "--tx-in-execution-units"
304+
, exBudgetToCliArg exBudget
305+
]
321306
]
322307
Just ConsumePublicKeyAddress -> []
323308
Just ConsumeSimpleScriptAddress -> []
@@ -331,8 +316,8 @@ txInCollateralOpts =
331316
concatMap (\(TxIn txOutRef _) -> ["--tx-in-collateral", txOutRefToCliArg txOutRef]) . Set.toList
332317

333318
-- Minting options
334-
mintOpts :: PABConfig -> BuildMode -> Set Scripts.MintingPolicy -> Redeemers -> Value -> [Text]
335-
mintOpts pabConf buildMode mintingPolicies redeemers mintValue =
319+
mintOpts :: PABConfig -> Set Scripts.MintingPolicy -> Redeemers -> Value -> [Text]
320+
mintOpts pabConf mintingPolicies redeemers mintValue =
336321
mconcat
337322
[ mconcat $
338323
concatMap
@@ -348,9 +333,7 @@ mintOpts pabConf buildMode mintingPolicies redeemers mintValue =
348333
toOpts r =
349334
[ ["--mint-script-file", policyScriptFilePath pabConf curSymbol]
350335
, ["--mint-redeemer-file", redeemerJsonFilePath pabConf (Ledger.redeemerHash r)]
351-
, if isRawBuildMode buildMode
352-
then ["--mint-execution-units", exBudgetToCliArg (exBudget r)]
353-
else []
336+
, ["--mint-execution-units", exBudgetToCliArg (exBudget r)]
354337
]
355338
in mconcat $ maybeToList $ fmap toOpts redeemer
356339
)

0 commit comments

Comments
 (0)