Skip to content

Commit d2e3e87

Browse files
committed
Add support for AwaitTxStatusChangeReq contract request
Change-type: minor Signed-off-by: Giovanni Garufi <giovanni@mlabs.city>
1 parent 0e3a56c commit d2e3e87

File tree

6 files changed

+131
-50
lines changed

6 files changed

+131
-50
lines changed

examples/plutus-game/app/Main.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import Data.Aeson.TH (defaultOptions, deriveJSON)
2424
import Data.ByteString.Lazy qualified as LazyByteString
2525
import Data.Default (def)
2626
import Data.Maybe (fromMaybe)
27+
import Ledger.Address (StakePubKeyHash (..))
2728
import Playground.Types (FunctionSchema)
2829
import Schema (FormSchema)
2930
import Servant.Client.Core (BaseUrl (BaseUrl), Scheme (Http))
@@ -60,11 +61,12 @@ main = do
6061
, pcProtocolParams = protocolParams
6162
, pcTipPollingInterval = 10_000_000
6263
, pcSlotConfig = def
63-
, pcOwnPubKeyHash = "0f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f97546"
64+
, pcOwnPubKeyHash = "54667d9a5b790a8640fe4662812060a782aa5a3a266cf75ff3869be3"
65+
, pcOwnStakePubKeyHash = Just $ StakePubKeyHash "7f8286c1eae3afc4241480cc534ab89f28383f4673bc5aa27e0582b0"
6466
, pcScriptFileDir = "./scripts"
6567
, pcSigningKeyFileDir = "./signing-keys"
6668
, pcTxFileDir = "./txs"
67-
, pcDryRun = True
69+
, pcDryRun = False
6870
, pcLogLevel = Debug
6971
, pcProtocolParamsFile = "./protocol.json"
7072
, pcForceBudget = Just (1000, 1000)

src/BotPlutusInterface/Balance.hs

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ balanceTxIO ::
7373
balanceTxIO pabConf ownPkh unbalancedTx =
7474
runEitherT $
7575
do
76-
utxos <- newEitherT $ CardanoCLI.utxosAt @w pabConf $ Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
76+
utxos <- newEitherT $ CardanoCLI.utxosAt @w pabConf changeAddr
7777
privKeys <- newEitherT $ Files.readPrivateKeys @w pabConf
7878
let utxoIndex = fmap Tx.toTxOut utxos <> unBalancedTxUtxoIndex unbalancedTx
7979
requiredSigs = map Ledger.unPaymentPubKeyHash $ Map.keys (unBalancedTxRequiredSignatories unbalancedTx)
@@ -101,17 +101,19 @@ balanceTxIO pabConf ownPkh unbalancedTx =
101101
-- If we have change but no change UTxO, we need to add an output for it
102102
-- We'll add a minimal output, run the loop again so it gets minUTxO, then update change
103103
balancedTxWithChange <-
104-
if adaChange /= 0 && not (hasChangeUTxO ownPkh balancedTx)
105-
then fst <$> loop utxoIndex privKeys minUtxos (addOutput ownPkh balancedTx)
104+
if adaChange /= 0 && not (hasChangeUTxO changeAddr balancedTx)
105+
then fst <$> loop utxoIndex privKeys minUtxos (addOutput changeAddr balancedTx)
106106
else pure balancedTx
107107

108108
-- Get the updated change, add it to the tx
109109
let finalAdaChange = getAdaChange utxoIndex balancedTxWithChange
110-
fullyBalancedTx = addAdaChange ownPkh finalAdaChange balancedTxWithChange
110+
fullyBalancedTx = addAdaChange changeAddr finalAdaChange balancedTxWithChange
111111

112112
-- finally, we must update the signatories
113113
hoistEither $ addSignatories ownPkh privKeys requiredSigs fullyBalancedTx
114114
where
115+
changeAddr :: Address
116+
changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) (pabConf.pcOwnStakePubKeyHash)
115117
loop ::
116118
Map TxOutRef TxOut ->
117119
Map PubKeyHash DummyPrivKey ->
@@ -130,7 +132,7 @@ balanceTxIO pabConf ownPkh unbalancedTx =
130132

131133
-- Calculate fees by pre-balancing the tx, building it, and running the CLI on result
132134
txWithoutFees <-
133-
hoistEither $ balanceTxStep minUtxos utxoIndex ownPkh $ tx `withFee` 0
135+
hoistEither $ balanceTxStep minUtxos utxoIndex changeAddr $ tx `withFee` 0
134136

135137
exBudget <- newEitherT $ CardanoCLI.buildTx @w pabConf privKeys txWithoutFees
136138
nonBudgettedFees <- newEitherT $ CardanoCLI.calculateMinFee @w pabConf txWithoutFees
@@ -140,7 +142,7 @@ balanceTxIO pabConf ownPkh unbalancedTx =
140142
lift $ printLog @w Debug $ "Fees: " ++ show fees
141143

142144
-- Rebalance the initial tx with the above fees
143-
balancedTx <- hoistEither $ balanceTxStep minUtxos utxoIndex ownPkh $ tx `withFee` fees
145+
balancedTx <- hoistEither $ balanceTxStep minUtxos utxoIndex changeAddr $ tx `withFee` fees
144146

145147
if balancedTx == tx
146148
then pure (balancedTx, minUtxos)
@@ -175,13 +177,13 @@ calculateMinUtxos pabConf datums txOuts =
175177
balanceTxStep ::
176178
[(TxOut, Integer)] ->
177179
Map TxOutRef TxOut ->
178-
PubKeyHash ->
180+
Address ->
179181
Tx ->
180182
Either Text Tx
181-
balanceTxStep minUtxos utxos ownPkh tx =
183+
balanceTxStep minUtxos utxos changeAddr tx =
182184
Right (addLovelaces minUtxos tx)
183185
>>= balanceTxIns utxos
184-
>>= handleNonAdaChange ownPkh utxos
186+
>>= handleNonAdaChange changeAddr utxos
185187

186188
-- | Get change value of a transaction, taking inputs, outputs, mint and fees into account
187189
getChange :: Map TxOutRef TxOut -> Tx -> Value
@@ -288,10 +290,9 @@ addTxCollaterals utxos tx = do
288290
filterAdaOnly = Map.filter (isAdaOnly . txOutValue)
289291

290292
-- | Ensures all non ada change goes back to user
291-
handleNonAdaChange :: PubKeyHash -> Map TxOutRef TxOut -> Tx -> Either Text Tx
292-
handleNonAdaChange ownPkh utxos tx =
293-
let changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
294-
nonAdaChange = getNonAdaChange utxos tx
293+
handleNonAdaChange :: Address -> Map TxOutRef TxOut -> Tx -> Either Text Tx
294+
handleNonAdaChange changeAddr utxos tx =
295+
let nonAdaChange = getNonAdaChange utxos tx
295296
outputs =
296297
case partition ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx of
297298
([], txOuts) ->
@@ -307,38 +308,33 @@ handleNonAdaChange ownPkh utxos tx =
307308
then Right $ if Value.isZero nonAdaChange then tx else tx {txOutputs = outputs}
308309
else Left "Not enough inputs to balance tokens."
309310

310-
hasChangeUTxO :: PubKeyHash -> Tx -> Bool
311-
hasChangeUTxO ownPkh tx =
311+
hasChangeUTxO :: Address -> Tx -> Bool
312+
hasChangeUTxO changeAddr tx =
312313
any ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx
313-
where
314-
changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
315314

316315
-- | Adds ada change to a transaction, assuming there is already an output going to ownPkh. Otherwise, this is identity
317-
addAdaChange :: PubKeyHash -> Integer -> Tx -> Tx
318-
addAdaChange ownPkh change tx =
316+
addAdaChange :: Address -> Integer -> Tx -> Tx
317+
addAdaChange changeAddr change tx =
319318
tx
320319
{ txOutputs =
321320
case partition ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx of
322321
(txOut@TxOut {txOutValue = v} : txOuts, txOuts') ->
323322
txOut {txOutValue = v <> Ada.lovelaceValueOf change} : (txOuts <> txOuts')
324323
_ -> txOutputs tx
325324
}
326-
where
327-
changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
328325

329326
-- | Adds a 1 lovelace output to a transaction
330-
addOutput :: PubKeyHash -> Tx -> Tx
331-
addOutput ownPkh tx = tx {txOutputs = changeTxOut : txOutputs tx}
327+
addOutput :: Address -> Tx -> Tx
328+
addOutput changeAddr tx = tx {txOutputs = changeTxOut : txOutputs tx}
332329
where
333-
changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing
334330
changeTxOut =
335331
TxOut
336332
{ txOutAddress = changeAddr
337333
, txOutValue = Ada.lovelaceValueOf 1
338334
, txOutDatumHash = Nothing
339335
}
340336

341-
{- | Add the required signatorioes to the transaction. Be aware the the signature itself is invalid,
337+
{- | Add the required signatories to the transaction. Be aware the the signature itself is invalid,
342338
and will be ignored. Only the pub key hashes are used, mapped to signing key files on disk.
343339
-}
344340
addSignatories :: PubKeyHash -> Map PubKeyHash DummyPrivKey -> [PubKeyHash] -> Tx -> Either Text Tx

src/BotPlutusInterface/Contract.hs

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@ import BotPlutusInterface.Effects (
1212
logToContract,
1313
printLog,
1414
queryChainIndex,
15+
readFileTextEnvelope,
1516
threadDelay,
1617
uploadDir,
1718
)
1819
import BotPlutusInterface.Files (DummyPrivKey (FromSKey, FromVKey))
1920
import BotPlutusInterface.Files qualified as Files
20-
import BotPlutusInterface.Types (ContractEnvironment (..), LogLevel (Debug, Warn), Tip (slot))
21-
import Control.Lens ((^.))
22-
import Control.Monad (void)
21+
import BotPlutusInterface.Types (ContractEnvironment (..), LogLevel (Debug, Warn), Tip (block, slot))
22+
import Cardano.Api (AsType (..), EraInMode (..))
23+
import Control.Lens (preview, (^.))
24+
import Control.Monad (join, void, when)
2325
import Control.Monad.Freer (Eff, Member, interpret, reinterpret, runM, subsume, type (~>))
2426
import Control.Monad.Freer.Error (runError)
2527
import Control.Monad.Freer.Extras.Log (handleLogIgnore)
2628
import Control.Monad.Freer.Extras.Modify (raiseEnd)
2729
import Control.Monad.Freer.Writer (Writer (Tell))
2830
import Control.Monad.Trans.Class (lift)
29-
import Control.Monad.Trans.Either (eitherT, firstEitherT, newEitherT, secondEitherT)
31+
import Control.Monad.Trans.Either (eitherT, firstEitherT, newEitherT)
3032
import Data.Aeson (ToJSON, Value)
3133
import Data.Aeson.Extras (encodeByteString)
3234
import Data.Kind (Type)
@@ -42,13 +44,15 @@ import Ledger.Slot (Slot (Slot))
4244
import Ledger.TimeSlot (posixTimeRangeToContainedSlotRange, posixTimeToEnclosingSlot, slotToEndPOSIXTime)
4345
import Ledger.Tx (CardanoTx)
4446
import Ledger.Tx qualified as Tx
45-
import Plutus.ChainIndex.Types (RollbackState (Committed), TxValidity (..))
47+
import Plutus.ChainIndex.Types (RollbackState (Committed), TxStatus, TxValidity (..))
4648
import Plutus.Contract.Checkpoint (Checkpoint (..))
4749
import Plutus.Contract.Effects (
4850
BalanceTxResponse (..),
51+
ChainIndexQuery (..),
4952
PABReq (..),
5053
PABResp (..),
5154
WriteBalancedTxResponse (..),
55+
_TxIdResponse,
5256
)
5357
import Plutus.Contract.Resumable (Resumable (..))
5458
import Plutus.Contract.Types (Contract (..), ContractEffs)
@@ -148,13 +152,13 @@ handlePABReq contractEnv req = do
148152
PosixTimeRangeToContainedSlotRangeResp $
149153
Right $
150154
posixTimeRangeToContainedSlotRange contractEnv.cePABConfig.pcSlotConfig posixTimeRange
155+
AwaitTxStatusChangeReq txId -> AwaitTxStatusChangeResp txId <$> getTxUpdate @w contractEnv txId
151156
------------------------
152157
-- Unhandled requests --
153158
------------------------
154159
-- AwaitTimeReq t -> pure $ AwaitTimeResp t
155160
-- AwaitUtxoSpentReq txOutRef -> pure $ AwaitUtxoSpentResp ChainIndexTx
156161
-- AwaitUtxoProducedReq Address -> pure $ AwaitUtxoProducedResp (NonEmpty ChainIndexTx)
157-
AwaitTxStatusChangeReq txId -> pure $ AwaitTxStatusChangeResp txId (Committed TxValid ())
158162
-- AwaitTxOutStatusChangeReq TxOutRef
159163
-- ExposeEndpointReq ActiveEndpoint -> ExposeEndpointResp EndpointDescription (EndpointValue JSON.Value)
160164
-- YieldUnbalancedTxReq UnbalancedTx
@@ -163,6 +167,33 @@ handlePABReq contractEnv req = do
163167
printLog @w Debug $ show resp
164168
pure resp
165169

170+
getTxUpdate ::
171+
forall (w :: Type) (effs :: [Type -> Type]).
172+
Member (PABEffect w) effs =>
173+
ContractEnvironment w ->
174+
Ledger.TxId ->
175+
Eff effs TxStatus
176+
getTxUpdate contractEnv txId = do
177+
let minBlockHeight = 3
178+
_ <- findTxByIdOrLoop
179+
printLog @w Debug $ "Found tx in chain index. Waiting for " ++ show minBlockHeight ++ " blocks"
180+
_ <- awaitNBlocks @w contractEnv minBlockHeight
181+
printLog @w Debug "Waited, checking once more for tx rollback"
182+
_ <- findTxByIdOrLoop
183+
printLog @w Debug "Tx is confirmed"
184+
pure $ Committed TxValid ()
185+
where
186+
findTxByIdOrLoop :: Eff effs ()
187+
findTxByIdOrLoop = do
188+
mTx <- join . preview _TxIdResponse <$> (queryChainIndex @w $ TxFromTxId txId)
189+
case mTx of
190+
Just _ -> pure ()
191+
Nothing -> do
192+
printLog @w Debug "Tx not found... looping"
193+
-- Wait for 1 block and try again
194+
_ <- awaitNBlocks @w contractEnv 1
195+
void $ getTxUpdate @w contractEnv txId
196+
166197
-- | This will FULLY balance a transaction
167198
balanceTx ::
168199
forall (w :: Type) (effs :: [Type -> Type]).
@@ -194,7 +225,7 @@ writeBalancedTx contractEnv (Right tx) = do
194225
uploadDir @w pabConf.pcSigningKeyFileDir
195226
createDirectoryIfMissing @w False (Text.unpack pabConf.pcScriptFileDir)
196227

197-
eitherT (pure . WriteBalancedTxFailed . OtherError) (pure . WriteBalancedTxSuccess . Right) $ do
228+
eitherT (pure . WriteBalancedTxFailed . OtherError) (pure . WriteBalancedTxSuccess . Left) $ do
198229
void $ firstEitherT (Text.pack . show) $ newEitherT $ Files.writeAll @w pabConf tx
199230
lift $ uploadDir @w pabConf.pcScriptFileDir
200231

@@ -215,9 +246,16 @@ writeBalancedTx contractEnv (Right tx) = do
215246
, "Signatories (pkh): " <> Text.unwords (map pkhToText requiredSigners)
216247
]
217248

218-
if not pabConf.pcDryRun && signable
219-
then secondEitherT (const tx) $ newEitherT $ CardanoCLI.submitTx @w pabConf tx
220-
else pure tx
249+
let ext = if signable then "signed" else "raw"
250+
path = Text.unpack $ Files.txFilePath pabConf ext tx
251+
-- We read back the tx from file as tx currently has the wrong id (but the one we create with cardano-cli is correct)
252+
alonxoTx <- firstEitherT (Text.pack . show) $ newEitherT $ readFileTextEnvelope @w (AsTx AsAlonzoEra) path
253+
let cardanoTx = Tx.SomeTx alonxoTx AlonzoEraInCardanoMode
254+
255+
when (not pabConf.pcDryRun && signable) $ do
256+
newEitherT $ CardanoCLI.submitTx @w pabConf tx
257+
258+
pure cardanoTx
221259

222260
pkhToText :: Ledger.PubKey -> Text
223261
pkhToText = encodeByteString . fromBuiltin . Ledger.getPubKeyHash . Ledger.pubKeyHash
@@ -239,6 +277,26 @@ awaitSlot contractEnv s@(Slot n) = do
239277
| n < tip'.slot -> pure $ Slot tip'.slot
240278
_ -> awaitSlot contractEnv s
241279

280+
-- | Wait for n Blocks.
281+
awaitNBlocks ::
282+
forall (w :: Type) (effs :: [Type -> Type]).
283+
Member (PABEffect w) effs =>
284+
ContractEnvironment w ->
285+
Integer ->
286+
Eff effs ()
287+
awaitNBlocks contractEnv n = do
288+
current <- currentBlock contractEnv
289+
go current
290+
where
291+
go :: Integer -> Eff effs ()
292+
go start = do
293+
threadDelay @w (fromIntegral contractEnv.cePABConfig.pcTipPollingInterval)
294+
tip <- CardanoCLI.queryTip @w contractEnv.cePABConfig
295+
case tip of
296+
Right tip'
297+
| start + n >= tip'.block -> pure ()
298+
_ -> go start
299+
242300
{- | Wait at least until the given time. Uses the awaitSlot under the hood, so the same constraints
243301
are applying here as well.
244302
-}
@@ -261,6 +319,14 @@ currentSlot ::
261319
currentSlot contractEnv =
262320
Slot . slot . either (error . Text.unpack) id <$> CardanoCLI.queryTip @w contractEnv.cePABConfig
263321

322+
currentBlock ::
323+
forall (w :: Type) (effs :: [Type -> Type]).
324+
Member (PABEffect w) effs =>
325+
ContractEnvironment w ->
326+
Eff effs Integer
327+
currentBlock contractEnv =
328+
block . either (error . Text.unpack) id <$> CardanoCLI.queryTip @w contractEnv.cePABConfig
329+
264330
currentTime ::
265331
forall (w :: Type) (effs :: [Type -> Type]).
266332
Member (PABEffect w) effs =>

src/BotPlutusInterface/Types.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Data.Kind (Type)
2828
import Data.Map (Map)
2929
import Data.Text (Text)
3030
import GHC.Generics (Generic)
31-
import Ledger (PubKeyHash)
31+
import Ledger (PubKeyHash, StakePubKeyHash)
3232
import Ledger.TimeSlot (SlotConfig)
3333
import Network.Wai.Handler.Warp (Port)
3434
import Numeric.Natural (Natural)
@@ -62,6 +62,7 @@ data PABConfig = PABConfig
6262
pcDryRun :: !Bool
6363
, pcLogLevel :: !LogLevel
6464
, pcOwnPubKeyHash :: !PubKeyHash
65+
, pcOwnStakePubKeyHash :: !(Maybe StakePubKeyHash)
6566
, pcTipPollingInterval :: !Natural
6667
, -- | Forced budget for scripts, as optional (CPU Steps, Memory Units)
6768
pcForceBudget :: !(Maybe (Integer, Integer))
@@ -127,6 +128,7 @@ instance Default PABConfig where
127128
, pcProtocolParamsFile = "./protocol.json"
128129
, pcLogLevel = Info
129130
, pcOwnPubKeyHash = ""
131+
, pcOwnStakePubKeyHash = Nothing
130132
, pcForceBudget = Nothing
131133
, pcPort = 9080
132134
, pcEnableTxEndpoint = False

test/Spec/BotPlutusInterface/Balance.hs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ addUtxosForFees = do
6161
tx = mempty {txOutputs = [txout]} `withFee` 500_000
6262
minUtxo = [(txout, 1_000_000)]
6363
utxoIndex = Map.fromList [utxo1, utxo2, utxo3]
64-
ownPkh = pkh1
64+
ownAddr = addr1
6565
balancedTx =
66-
Balance.balanceTxStep minUtxo utxoIndex ownPkh tx
66+
Balance.balanceTxStep minUtxo utxoIndex ownAddr tx
6767

6868
txInputs <$> balancedTx @?= Right (Set.fromList [txIn1, txIn2])
6969

@@ -73,9 +73,9 @@ addUtxosForNativeTokens = do
7373
tx = mempty {txOutputs = [txout]} `withFee` 500_000
7474
minUtxo = [(txout, 1_000_000)]
7575
utxoIndex = Map.fromList [utxo1, utxo2, utxo3, utxo4]
76-
ownPkh = pkh1
76+
ownAddr = addr1
7777
balancedTx =
78-
Balance.balanceTxStep minUtxo utxoIndex ownPkh tx
78+
Balance.balanceTxStep minUtxo utxoIndex ownAddr tx
7979

8080
txInputs <$> balancedTx @?= Right (Set.fromList [txIn1, txIn2, txIn3, txIn4])
8181

@@ -85,8 +85,8 @@ addUtxosForChange = do
8585
tx = mempty {txOutputs = [txout]} `withFee` 500_000
8686
minUtxo = [(txout, 1_000_000)]
8787
utxoIndex = Map.fromList [utxo1, utxo2, utxo3]
88-
ownPkh = pkh1
88+
ownAddr = addr1
8989
balancedTx =
90-
Balance.balanceTxStep minUtxo utxoIndex ownPkh tx
90+
Balance.balanceTxStep minUtxo utxoIndex ownAddr tx
9191

9292
txInputs <$> balancedTx @?= Right (Set.fromList [txIn1, txIn2])

0 commit comments

Comments
 (0)