Skip to content

Commit 6944c3b

Browse files
committed
add case to handle min utxo in balance algorithm
1 parent b6d08ab commit 6944c3b

File tree

3 files changed

+141
-22
lines changed

3 files changed

+141
-22
lines changed

src/BotPlutusInterface/Balance.hs

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module BotPlutusInterface.Balance (
1313

1414
import BotPlutusInterface.BodyBuilder qualified as BodyBuilder
1515
import BotPlutusInterface.CardanoCLI qualified as CardanoCLI
16-
import BotPlutusInterface.CardanoNode.Effects (NodeQuery (UtxosAt))
16+
import BotPlutusInterface.CardanoNode.Effects (NodeQuery (MinUtxo, UtxosAt))
1717
import BotPlutusInterface.CoinSelection (selectTxIns)
1818
import BotPlutusInterface.Collateral (removeCollateralFromMap)
1919
import BotPlutusInterface.Effects (
@@ -160,10 +160,11 @@ balanceTxIO' balanceCfg pabConf ownPkh unbalancedTx =
160160

161161
-- Balance the tx
162162
balancedTx <- balanceTxLoop utxoIndex privKeys preBalancedTx
163+
changeTxOutWithMinAmt <- newEitherT $ addOutput @w changeAddr balancedTx
163164

164165
-- Get current Ada change
165166
let adaChange = getAdaChange utxoIndex balancedTx
166-
bTx = balanceTxLoop utxoIndex privKeys (addOutput changeAddr balancedTx)
167+
bTx = balanceTxLoop utxoIndex privKeys changeTxOutWithMinAmt
167168

168169
-- Checks if there's ada change left, if there is then we check
169170
-- if `bcSeparateChange` is true, if this is the case then we create a new UTxO at
@@ -289,7 +290,7 @@ balanceTxStep ::
289290
balanceTxStep balanceCfg utxos changeAddr tx =
290291
runEitherT $
291292
(newEitherT . balanceTxIns @w utxos) tx
292-
>>= hoistEither . handleNonAdaChange balanceCfg changeAddr utxos
293+
>>= newEitherT . handleNonAdaChange @w balanceCfg changeAddr utxos
293294

294295
-- | Get change value of a transaction, taking inputs, outputs, mint and fees into account
295296
getChange :: Map TxOutRef TxOut -> Tx -> Value
@@ -347,9 +348,19 @@ txUsesScripts Tx {txInputs, txMintScripts} =
347348
txInputs
348349

349350
-- | Ensures all non ada change goes back to user
350-
handleNonAdaChange :: BalanceConfig -> Address -> Map TxOutRef TxOut -> Tx -> Either Text Tx
351-
handleNonAdaChange balanceCfg changeAddr utxos tx =
352-
let nonAdaChange = getNonAdaChange utxos tx
351+
handleNonAdaChange ::
352+
forall (w :: Type) (effs :: [Type -> Type]).
353+
Member (PABEffect w) effs =>
354+
BalanceConfig ->
355+
Address ->
356+
Map TxOutRef TxOut ->
357+
Tx ->
358+
Eff effs (Either Text Tx)
359+
handleNonAdaChange balanceCfg changeAddr utxos tx = runEitherT $ do
360+
let nonAdaChange :: Value
361+
nonAdaChange = getNonAdaChange utxos tx
362+
363+
predicate :: TxOut -> Bool
353364
predicate =
354365
if bcSeparateChange balanceCfg
355366
then
@@ -358,20 +369,30 @@ handleNonAdaChange balanceCfg changeAddr utxos tx =
358369
&& not (justLovelace $ Tx.txOutValue txout)
359370
)
360371
else (\txout -> Tx.txOutAddress txout == changeAddr)
372+
373+
newOutput :: TxOut
361374
newOutput =
362375
TxOut
363376
{ txOutAddress = changeAddr
364377
, txOutValue = nonAdaChange
365378
, txOutDatumHash = Nothing
366379
}
380+
381+
newOutputWithMinAmt <-
382+
firstEitherT (Text.pack . show) $
383+
newEitherT $
384+
queryNode @w (MinUtxo newOutput)
385+
386+
let outputs :: [TxOut]
367387
outputs =
368388
modifyFirst
369389
predicate
370-
(Just . maybe newOutput (addValueToTxOut nonAdaChange))
390+
(Just . maybe newOutputWithMinAmt (addValueToTxOut nonAdaChange))
371391
(txOutputs tx)
372-
in if isValueNat nonAdaChange
373-
then Right $ if Value.isZero nonAdaChange then tx else tx {txOutputs = outputs}
374-
else Left "Not enough inputs to balance tokens."
392+
393+
if isValueNat nonAdaChange
394+
then return $ if Value.isZero nonAdaChange then tx else tx {txOutputs = outputs}
395+
else throwE "Not enough inputs to balance tokens."
375396

376397
{- | `addAdaChange` checks if `bcSeparateChange` is true,
377398
if it is then we add the ada change to seperate `TxOut` at changeAddr that contains only ada,
@@ -401,16 +422,29 @@ addAdaChange balanceCfg changeAddr change tx
401422
addValueToTxOut :: Value -> TxOut -> TxOut
402423
addValueToTxOut val txOut = txOut {txOutValue = txOutValue txOut <> val}
403424

404-
-- | Adds a 1 lovelace output to a transaction
405-
addOutput :: Address -> Tx -> Tx
406-
addOutput changeAddr tx = tx {txOutputs = txOutputs tx ++ [changeTxOut]}
407-
where
408-
changeTxOut =
409-
TxOut
410-
{ txOutAddress = changeAddr
411-
, txOutValue = Ada.lovelaceValueOf 1
412-
, txOutDatumHash = Nothing
413-
}
425+
-- | creates a Tx output with min lovelace.
426+
addOutput ::
427+
forall (w :: Type) (effs :: [Type -> Type]).
428+
Member (PABEffect w) effs =>
429+
Address ->
430+
Tx ->
431+
Eff effs (Either Text Tx)
432+
addOutput changeAddr tx =
433+
runEitherT $ do
434+
let changeTxOut :: TxOut
435+
changeTxOut =
436+
TxOut
437+
{ txOutAddress = changeAddr
438+
, txOutValue = Ada.lovelaceValueOf 1
439+
, txOutDatumHash = Nothing
440+
}
441+
442+
changeTxOutWithMinAmt <-
443+
firstEitherT (Text.pack . show) $
444+
newEitherT $
445+
queryNode @w (MinUtxo changeTxOut)
446+
447+
return $ tx {txOutputs = txOutputs tx ++ [changeTxOutWithMinAmt]}
414448

415449
{- | Add the required signatories to the transaction. Be aware the the signature itself is invalid,
416450
and will be ignored. Only the pub key hashes are used, mapped to signing key files on disk.

src/BotPlutusInterface/CardanoNode/Effects.hs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
-}
1111
module BotPlutusInterface.CardanoNode.Effects (
1212
utxosAt,
13+
pparams,
14+
minUtxo,
1315
handleNodeQuery,
1416
runNodeQuery,
1517
NodeQuery (..),
@@ -21,32 +23,44 @@ import BotPlutusInterface.CardanoNode.Query (
2123
QueryConstraint,
2224
connectionInfo,
2325
queryBabbageEra,
26+
queryInCardanoMode,
2427
toQueryError,
2528
)
2629

2730
import BotPlutusInterface.CardanoAPI (
2831
addressInEraToAny,
2932
fromCardanoTxOut,
3033
)
34+
3135
import BotPlutusInterface.Types (PABConfig)
3236
import Cardano.Api (LocalNodeConnectInfo (..))
3337
import Cardano.Api qualified as CApi
38+
import Cardano.Api.Shelley qualified as CApi.S
39+
import Cardano.Ledger.Shelley.API.Wallet (
40+
CLI (evaluateMinLovelaceOutput),
41+
)
3442
import Control.Lens (folded, to, (^..))
3543
import Control.Monad.Freer (Eff, Members, interpret, runM, send, type (~>))
3644
import Control.Monad.Freer.Reader (Reader, ask, runReader)
3745
import Control.Monad.Trans.Class (lift)
3846
import Control.Monad.Trans.Either (firstEitherT, hoistEither, newEitherT, runEitherT)
47+
import Control.Monad.Trans.Except (throwE)
3948
import Data.Map (Map)
4049
import Data.Map qualified as Map
4150
import Data.Set qualified as Set
51+
import Ledger qualified
52+
import Ledger.Ada qualified as Ada
4253
import Ledger.Address (Address)
4354
import Ledger.Tx (ChainIndexTxOut (..))
4455
import Ledger.Tx.CardanoAPI qualified as TxApi
56+
import Ledger.Validation (Coin (Coin))
4557
import Plutus.V2.Ledger.Tx qualified as V2
4658
import Prelude
4759

4860
data NodeQuery a where
4961
UtxosAt :: Address -> NodeQuery (Either NodeQueryError (Map V2.TxOutRef ChainIndexTxOut))
62+
PParams :: NodeQuery (Either NodeQueryError CApi.S.ProtocolParameters)
63+
MinUtxo :: Ledger.TxOut -> NodeQuery (Either NodeQueryError Ledger.TxOut)
5064

5165
utxosAt ::
5266
forall effs.
@@ -55,13 +69,28 @@ utxosAt ::
5569
Eff effs (Either NodeQueryError (Map V2.TxOutRef ChainIndexTxOut))
5670
utxosAt = send . UtxosAt
5771

72+
pparams ::
73+
forall effs.
74+
Members '[NodeQuery] effs =>
75+
Eff effs (Either NodeQueryError CApi.S.ProtocolParameters)
76+
pparams = send PParams
77+
78+
minUtxo ::
79+
forall effs.
80+
Members '[NodeQuery] effs =>
81+
Ledger.TxOut ->
82+
Eff effs (Either NodeQueryError Ledger.TxOut)
83+
minUtxo = send . MinUtxo
84+
5885
handleNodeQuery ::
5986
forall effs.
6087
QueryConstraint effs =>
6188
Eff (NodeQuery ': effs) ~> Eff effs
6289
handleNodeQuery =
6390
interpret $ \case
6491
UtxosAt addr -> handleUtxosAt addr
92+
PParams -> queryPParams
93+
MinUtxo txout -> handleMinUtxo txout
6594

6695
handleUtxosAt ::
6796
forall effs.
@@ -92,9 +121,56 @@ handleUtxosAt addr = runEitherT $ do
92121

93122
return $ Map.fromList $ zip txOutRefs chainIndexTxOuts
94123

124+
handleMinUtxo ::
125+
forall effs.
126+
QueryConstraint effs =>
127+
Ledger.TxOut ->
128+
Eff effs (Either NodeQueryError Ledger.TxOut)
129+
handleMinUtxo txout = runEitherT $ do
130+
conn <- lift $ ask @NodeConn
131+
132+
params <- newEitherT queryPParams
133+
134+
let pparamsInEra = CApi.toLedgerPParams CApi.ShelleyBasedEraBabbage params
135+
netId = localNodeNetworkId conn
136+
137+
ctxout <-
138+
firstEitherT toQueryError $
139+
hoistEither $
140+
TxApi.toCardanoTxOut netId TxApi.toCardanoTxOutDatumHash txout
141+
142+
let (Coin minTxOut) =
143+
evaluateMinLovelaceOutput pparamsInEra $
144+
CApi.S.toShelleyTxOut CApi.ShelleyBasedEraBabbage ctxout
145+
146+
missingLovelace = Ada.lovelaceOf minTxOut - Ada.fromValue (Ledger.txOutValue txout)
147+
148+
if missingLovelace > 0
149+
then
150+
newEitherT $
151+
handleMinUtxo (txout {Ledger.txOutValue = Ledger.txOutValue txout <> Ada.toValue missingLovelace})
152+
else return txout
153+
95154
runNodeQuery :: PABConfig -> Eff '[NodeQuery, Reader NodeConn, IO] ~> IO
96155
runNodeQuery conf effs = do
97156
conn <- connectionInfo conf
98157
runM $
99158
runReader conn $
100159
handleNodeQuery effs
160+
161+
-- Helpers
162+
163+
queryPParams ::
164+
forall effs.
165+
QueryConstraint effs =>
166+
Eff effs (Either NodeQueryError CApi.S.ProtocolParameters)
167+
queryPParams = runEitherT $ do
168+
let query =
169+
CApi.QueryInEra CApi.BabbageEraInCardanoMode $
170+
CApi.QueryInShelleyBasedEra CApi.ShelleyBasedEraBabbage CApi.QueryProtocolParameters
171+
172+
result <- newEitherT $ queryInCardanoMode query
173+
174+
case result of
175+
Right params -> return params
176+
Left err -> throwE $ toQueryError err

test/Spec/MockContract.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ module Spec.MockContract (
5353
) where
5454

5555
import BotPlutusInterface.CardanoCLI (unsafeSerialiseAddress)
56-
import BotPlutusInterface.CardanoNode.Effects (NodeQuery (UtxosAt))
56+
import BotPlutusInterface.CardanoNode.Effects (NodeQuery (MinUtxo, PParams, UtxosAt))
57+
import BotPlutusInterface.CardanoNode.Query (toQueryError)
5758
import BotPlutusInterface.Collateral (removeCollateralFromPage)
5859
import BotPlutusInterface.Contract (handleContract)
5960
import BotPlutusInterface.Effects (PABEffect (..), ShellArgs (..))
@@ -112,6 +113,7 @@ import Data.ByteString.Lazy qualified as LBS
112113
import Data.ByteString.Short qualified as SBS
113114
import Data.Default (Default (def))
114115
import Data.Either.Combinators (fromRight, mapLeft)
116+
import Data.Either.Extra (maybeToEither)
115117
import Data.Hex (hex, unhex)
116118
import Data.Kind (Type)
117119
import Data.List (isPrefixOf, sortOn)
@@ -375,7 +377,14 @@ runPABEffectPure initState req =
375377
go (SetInMemCollateral collateral) = modify @(MockContractState w) $ set collateralUtxo (Just collateral)
376378
go (QueryNode (UtxosAt _addr)) = do
377379
state <- get @(MockContractState w)
378-
return $ return $ Map.fromList (state ^. utxos)
380+
return $ Right $ Map.fromList (state ^. utxos)
381+
go (QueryNode (MinUtxo utxo)) = return $ Right utxo
382+
go (QueryNode PParams) =
383+
maybeToEither (toQueryError @String "Not able to get ProtocolParameters.")
384+
. pcProtocolParams
385+
. cePABConfig
386+
. _contractEnv
387+
<$> get @(MockContractState w)
379388
incSlot :: forall (v :: Type). MockContract w v -> MockContract w v
380389
incSlot mc =
381390
mc <* modify @(MockContractState w) (tip %~ incTip)

0 commit comments

Comments
 (0)