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
89import BotPlutusInterface.CardanoCLI qualified as CardanoCLI
910import BotPlutusInterface.Effects (PABEffect , createDirectoryIfMissing , printLog )
1011import BotPlutusInterface.Files (DummyPrivKey , unDummyPrivateKey )
1112import BotPlutusInterface.Files qualified as Files
1213import BotPlutusInterface.Types (LogLevel (Debug ), PABConfig )
13- import Cardano.Api.Shelley (Lovelace (Lovelace ), ProtocolParameters (protocolParamUTxOCostPerWord ))
1414import Control.Monad (foldM , void , zipWithM )
1515import Control.Monad.Freer (Eff , Member )
1616import Control.Monad.Trans.Class (lift )
1717import Control.Monad.Trans.Either (EitherT , hoistEither , newEitherT , runEitherT )
18- import Data.Either.Combinators (maybeToRight , rightToMaybe )
18+ import Data.Either.Combinators (rightToMaybe )
1919import Data.Kind (Type )
2020import Data.List (partition , (\\) )
2121import Data.Map (Map )
@@ -47,27 +47,26 @@ import Ledger.Tx (
4747 TxOutRef (.. ),
4848 )
4949import Ledger.Tx qualified as Tx
50- import Ledger.Value (Value ( Value ), getValue )
50+ import Ledger.Value (Value )
5151import Ledger.Value qualified as Value
5252import Plutus.V1.Ledger.Api (
5353 Credential (PubKeyCredential , ScriptCredential ),
5454 CurrencySymbol (.. ),
5555 TokenName (.. ),
5656 )
57- import PlutusTx.AssocMap qualified as AssocMap
5857import 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
120144calculateMinUtxos ::
121145 forall (w :: Type ) (effs :: [Type -> Type ]).
@@ -127,24 +151,36 @@ calculateMinUtxos ::
127151calculateMinUtxos 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
150186collectTxIns :: 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
289347showText :: forall (a :: Type ). Show a => a -> Text
290348showText = 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-
300350minus :: Value -> Value -> Value
301351minus x y =
302352 let negativeValues = map (\ (c, t, a) -> (c, t, - a)) $ Value. flattenValue y
0 commit comments