Skip to content

Commit c95b103

Browse files
Merge pull request #48 from mlabs-haskell/sam/always-raw-build
Fully internalise balancing, fix tests
2 parents 9315a82 + 4988f49 commit c95b103

File tree

9 files changed

+286
-197
lines changed

9 files changed

+286
-197
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
BotPlutusInterface.Server
@@ -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.BotPlutusInterface.Server
149149
Spec.MockContract

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

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

3-
module BotPlutusInterface.PreBalance (
4-
preBalanceTx,
5-
preBalanceTxIO,
3+
module BotPlutusInterface.Balance (
4+
balanceTxStep,
5+
balanceTxIO,
6+
withFee,
67
) where
78

89
import BotPlutusInterface.CardanoCLI qualified as CardanoCLI
910
import BotPlutusInterface.Effects (PABEffect, createDirectoryIfMissing, printLog)
1011
import BotPlutusInterface.Files (DummyPrivKey, unDummyPrivateKey)
1112
import BotPlutusInterface.Files qualified as Files
1213
import BotPlutusInterface.Types (LogLevel (Debug), PABConfig)
13-
import Cardano.Api.Shelley (Lovelace (Lovelace), ProtocolParameters (protocolParamUTxOCostPerWord))
1414
import Control.Monad (foldM, void, zipWithM)
1515
import Control.Monad.Freer (Eff, Member)
1616
import Control.Monad.Trans.Class (lift)
1717
import Control.Monad.Trans.Either (EitherT, hoistEither, newEitherT, runEitherT)
18-
import Data.Either.Combinators (maybeToRight, rightToMaybe)
18+
import Data.Either.Combinators (rightToMaybe)
1919
import Data.Kind (Type)
2020
import Data.List (partition, (\\))
2121
import Data.Map (Map)
@@ -47,27 +47,26 @@ import Ledger.Tx (
4747
TxOutRef (..),
4848
)
4949
import Ledger.Tx qualified as Tx
50-
import Ledger.Value (Value (Value), getValue)
50+
import Ledger.Value (Value)
5151
import Ledger.Value qualified as Value
5252
import Plutus.V1.Ledger.Api (
5353
Credential (PubKeyCredential, ScriptCredential),
5454
CurrencySymbol (..),
5555
TokenName (..),
5656
)
57-
import PlutusTx.AssocMap qualified as AssocMap
5857
import Prelude
5958

6059
{- | Collect necessary tx inputs and collaterals, add minimum lovelace values and balance non ada
6160
assets
6261
-}
63-
preBalanceTxIO ::
62+
balanceTxIO ::
6463
forall (w :: Type) (effs :: [Type -> Type]).
6564
Member (PABEffect w) effs =>
6665
PABConfig ->
6766
PubKeyHash ->
6867
UnbalancedTx ->
6968
Eff effs (Either Text Tx)
70-
preBalanceTxIO pabConf ownPkh unbalancedTx =
69+
balanceTxIO pabConf ownPkh unbalancedTx =
7170
runEitherT $
7271
do
7372
utxos <- newEitherT $ CardanoCLI.utxosAt @w pabConf $ Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
@@ -83,16 +82,36 @@ preBalanceTxIO pabConf ownPkh unbalancedTx =
8382

8483
lift $ printLog @w Debug $ show utxoIndex
8584

86-
loop utxoIndex privKeys requiredSigs [] tx
85+
-- Adds required collaterals, only needs to happen once
86+
-- Also adds signatures for fee calculation
87+
preBalancedTx <- hoistEither $ addTxCollaterals utxoIndex tx >>= addSignatories ownPkh privKeys requiredSigs
88+
89+
-- Balance the tx
90+
(balancedTx, minUtxos) <- loop utxoIndex privKeys [] preBalancedTx
91+
92+
-- Get current Ada change
93+
let adaChange = getAdaChange utxoIndex balancedTx
94+
-- If we have change but no change UTxO, we need to add an output for it
95+
-- We'll add a minimal output, run the loop again so it gets minUTxO, then update change
96+
balancedTxWithChange <-
97+
if adaChange /= 0 && not (hasChangeUTxO ownPkh balancedTx)
98+
then fst <$> loop utxoIndex privKeys minUtxos (addOutput ownPkh balancedTx)
99+
else pure balancedTx
100+
101+
-- Get the updated change, add it to the tx
102+
let finalAdaChange = getAdaChange utxoIndex balancedTxWithChange
103+
fullyBalancedTx = addAdaChange ownPkh finalAdaChange balancedTxWithChange
104+
105+
-- finally, we must update the signatories
106+
hoistEither $ addSignatories ownPkh privKeys requiredSigs fullyBalancedTx
87107
where
88108
loop ::
89109
Map TxOutRef TxOut ->
90110
Map PubKeyHash DummyPrivKey ->
91-
[PubKeyHash] ->
92111
[(TxOut, Integer)] ->
93112
Tx ->
94-
EitherT Text (Eff effs) Tx
95-
loop utxoIndex privKeys requiredSigs prevMinUtxos tx = do
113+
EitherT Text (Eff effs) (Tx, [(TxOut, Integer)])
114+
loop utxoIndex privKeys prevMinUtxos tx = do
96115
void $ lift $ Files.writeAll @w pabConf tx
97116
nextMinUtxos <-
98117
newEitherT $
@@ -102,20 +121,25 @@ preBalanceTxIO pabConf ownPkh unbalancedTx =
102121

103122
lift $ printLog @w Debug $ "Min utxos: " ++ show minUtxos
104123

124+
-- Calculate fees by pre-balancing the tx, building it, and running the CLI on result
105125
txWithoutFees <-
106-
hoistEither $ preBalanceTx pabConf.pcProtocolParams minUtxos 0 utxoIndex ownPkh privKeys requiredSigs tx
126+
hoistEither $ balanceTxStep minUtxos utxoIndex ownPkh $ tx `withFee` 0
107127

108128
lift $ createDirectoryIfMissing @w False (Text.unpack pabConf.pcTxFileDir)
109-
newEitherT $ CardanoCLI.buildTx @w pabConf privKeys ownPkh (CardanoCLI.BuildRaw 0) txWithoutFees
129+
newEitherT $ CardanoCLI.buildTx @w pabConf privKeys txWithoutFees
110130
fees <- newEitherT $ CardanoCLI.calculateMinFee @w pabConf txWithoutFees
111131

112132
lift $ printLog @w Debug $ "Fees: " ++ show fees
113133

114-
balancedTx <- hoistEither $ preBalanceTx pabConf.pcProtocolParams minUtxos fees utxoIndex ownPkh privKeys requiredSigs tx
134+
-- Rebalance the initial tx with the above fees
135+
balancedTx <- hoistEither $ balanceTxStep minUtxos utxoIndex ownPkh $ tx `withFee` fees
115136

116137
if balancedTx == tx
117-
then pure balancedTx
118-
else loop utxoIndex privKeys requiredSigs minUtxos balancedTx
138+
then pure (balancedTx, minUtxos)
139+
else loop utxoIndex privKeys minUtxos balancedTx
140+
141+
withFee :: Tx -> Integer -> Tx
142+
withFee tx fee = tx {txFee = Ada.lovelaceValueOf fee}
119143

120144
calculateMinUtxos ::
121145
forall (w :: Type) (effs :: [Type -> Type]).
@@ -127,24 +151,36 @@ calculateMinUtxos ::
127151
calculateMinUtxos pabConf datums txOuts =
128152
zipWithM (fmap . (,)) txOuts <$> mapM (CardanoCLI.calculateMinUtxo @w pabConf datums) txOuts
129153

130-
preBalanceTx ::
131-
ProtocolParameters ->
154+
balanceTxStep ::
132155
[(TxOut, Integer)] ->
133-
Integer ->
134156
Map TxOutRef TxOut ->
135157
PubKeyHash ->
136-
Map PubKeyHash DummyPrivKey ->
137-
[PubKeyHash] ->
138158
Tx ->
139159
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
160+
balanceTxStep minUtxos utxos ownPkh tx =
161+
Right (addLovelaces minUtxos tx)
162+
>>= balanceTxIns utxos
163+
>>= handleNonAdaChange ownPkh utxos
164+
165+
-- | Get change value of a transaction, taking inputs, outputs, mint and fees into account
166+
getChange :: Map TxOutRef TxOut -> Tx -> Value
167+
getChange utxos tx =
168+
let fees = lovelaceValue $ txFee tx
169+
txInRefs = map Tx.txInRef $ Set.toList $ txInputs tx
170+
inputValue = mconcat $ map Tx.txOutValue $ mapMaybe (`Map.lookup` utxos) txInRefs
171+
outputValue = mconcat $ map Tx.txOutValue $ txOutputs tx
172+
nonMintedOutputValue = outputValue `minus` txMint tx
173+
change = (inputValue `minus` nonMintedOutputValue) `minus` Ada.lovelaceValueOf fees
174+
in change
175+
176+
lovelaceValue :: Value -> Integer
177+
lovelaceValue = flip Value.assetClassValueOf $ Value.assetClass "" ""
178+
179+
getAdaChange :: Map TxOutRef TxOut -> Tx -> Integer
180+
getAdaChange utxos = lovelaceValue . getChange utxos
181+
182+
getNonAdaChange :: Map TxOutRef TxOut -> Tx -> Value
183+
getNonAdaChange utxos = Ledger.noAdaValue . getChange utxos
148184

149185
-- | Getting the necessary utxos to cover the fees for the transaction
150186
collectTxIns :: Set TxIn -> Map TxOutRef TxOut -> Value -> Either Text (Set TxIn)
@@ -202,18 +238,13 @@ addLovelaces minLovelaces tx =
202238
$ txOutputs tx
203239
in tx {txOutputs = lovelacesAdded}
204240

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
241+
balanceTxIns :: Map TxOutRef TxOut -> Tx -> Either Text Tx
242+
balanceTxIns utxos tx = do
209243
let txOuts = Tx.txOutputs tx
210244
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
214245
minSpending =
215246
mconcat
216-
[ Ada.lovelaceValueOf (fees + changeMinUtxo)
247+
[ txFee tx
217248
, nonMintedValue
218249
]
219250
txIns <- collectTxIns (txInputs tx) utxos minSpending
@@ -235,15 +266,11 @@ addTxCollaterals utxos tx = do
235266
_ -> Left "There are no utxos to be used as collateral"
236267
filterAdaOnly = Map.filter (isAdaOnly . txOutValue)
237268

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 =
269+
-- | Ensures all non ada change goes back to user
270+
handleNonAdaChange :: PubKeyHash -> Map TxOutRef TxOut -> Tx -> Either Text Tx
271+
handleNonAdaChange ownPkh utxos tx =
241272
let changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
242-
txInRefs = map Tx.txInRef $ Set.toList $ txInputs tx
243-
inputValue = mconcat $ map Tx.txOutValue $ mapMaybe (`Map.lookup` utxos) txInRefs
244-
outputValue = mconcat $ map Tx.txOutValue $ txOutputs tx
245-
nonMintedOutputValue = outputValue `minus` txMint tx
246-
nonAdaChange = filterNonAda inputValue `minus` filterNonAda nonMintedOutputValue
273+
nonAdaChange = getNonAdaChange utxos tx
247274
outputs =
248275
case partition ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx of
249276
([], txOuts) ->
@@ -259,6 +286,37 @@ balanceNonAdaOuts ownPkh utxos tx =
259286
then Right $ if Value.isZero nonAdaChange then tx else tx {txOutputs = outputs}
260287
else Left "Not enough inputs to balance tokens."
261288

289+
hasChangeUTxO :: PubKeyHash -> Tx -> Bool
290+
hasChangeUTxO ownPkh tx =
291+
any ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx
292+
where
293+
changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
294+
295+
-- | Adds ada change to a transaction, assuming there is already an output going to ownPkh. Otherwise, this is identity
296+
addAdaChange :: PubKeyHash -> Integer -> Tx -> Tx
297+
addAdaChange ownPkh change tx =
298+
tx
299+
{ txOutputs =
300+
case partition ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx of
301+
(txOut@TxOut {txOutValue = v} : txOuts, txOuts') ->
302+
txOut {txOutValue = v <> Ada.lovelaceValueOf change} : (txOuts <> txOuts')
303+
_ -> txOutputs tx
304+
}
305+
where
306+
changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
307+
308+
-- | Adds a 1 lovelace output to a transaction
309+
addOutput :: PubKeyHash -> Tx -> Tx
310+
addOutput ownPkh tx = tx {txOutputs = changeTxOut : txOutputs tx}
311+
where
312+
changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
313+
changeTxOut =
314+
TxOut
315+
{ txOutAddress = changeAddr
316+
, txOutValue = Ada.lovelaceValueOf 1
317+
, txOutDatumHash = Nothing
318+
}
319+
262320
{- | Add the required signatorioes to the transaction. Be aware the the signature itself is invalid,
263321
and will be ignored. Only the pub key hashes are used, mapped to signing key files on disk.
264322
-}
@@ -289,14 +347,6 @@ validateRange _ = True
289347
showText :: forall (a :: Type). Show a => a -> Text
290348
showText = Text.pack . show
291349

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-
300350
minus :: Value -> Value -> Value
301351
minus x y =
302352
let negativeValues = map (\(c, t, a) -> (c, t, - a)) $ Value.flattenValue y

0 commit comments

Comments
 (0)