-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Redesign GYTxMonad
and its instances to be able to submit transactions
#322
Conversation
Summary of important commits:
Note how the Privnet code utilizes |
I suggest reviewing this PR commit by commit. Most of the changes to the Privnet code can be ignored. It's simply updating usage and its summarized in here. My commit summary above might help in reviewing as well. Before, merging - I need to know how the parallel and chaining building could be amended. I'm thinking of removing the whole |
src/GeniusYield/Test/Privnet/Ctx.hs
Outdated
ctxRunBuilder :: Ctx -> User -> GYTxBuilderMonadIO a -> IO a | ||
ctxRunBuilder ctx User {..} = runGYTxBuilderMonadIO (ctxNetworkId ctx) (ctxProviders ctx) [userAddr] userAddr Nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be the change required in most GY app code. Since the app code probably just needs to build tranasactions, not submit them.
addRefScriptCtx ctx user script = do | ||
txBodyRefScript <- ctxRunF ctx user $ Limbo.addRefScript script | ||
addRefScriptCtx ctx user script = ctxRun ctx user $ do | ||
txBodyRefScript <- Limbo.addRefScript script >>= traverse buildTxBody |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Traversing manually instead of having the hack with GYTxMonadNode
over a Traversable f
Right body -> do | ||
let refs = Limbo.findRefScriptsInBody body | ||
ref <- case Map.lookup (Some script) refs of | ||
Just ref -> return ref | ||
Nothing -> fail "Shouldn't happen: no ref in body" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MonadFail removed since we want to unify exceptions as much as possible
Nothing -> fail "Shouldn't happen: no ref in body" | ||
void $ submitTx ctx user body | ||
return ref | ||
Nothing -> throwAppError $ someBackendError "Shouldn't happen: no ref in body" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably better to throw some typed error.
void $ submitTx ctx user body | ||
return ref | ||
Nothing -> throwAppError $ someBackendError "Shouldn't happen: no ref in body" | ||
signAndSubmitConfirmed_ body |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GYTxMonadIO
(should really be used for bot-like usecase, use GYTxBuilderMonadIO
for app-like usecases) now contains the user keys. So signAndSubmitConfirmed_
is a helper that uses the payment key.
@@ -31,6 +31,7 @@ import Control.Monad.Except | |||
import Control.Monad.Random |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More updates to this module will follow in a different PR. Said PR will be able to unify privnet and clb code
-- | Errors encountered during transaction building related functions. | ||
GYBuildTxException :: GYBuildTxError -> GYTxMonadException |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transaction building errors are now a part of GYTxMonadException
Stability : develop | ||
|
||
-} | ||
module GeniusYield.TxBuilder.IO ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was previously known as GYTxMonadNode
. Its interface is vastly simplified now. And it is composed with other less powerful monads that are also usable to a library user.
See PR comment about GYTxMonad hierarchy.
src/GeniusYield/TxBuilder/IO.hs
Outdated
, envPaymentSKey :: !GYPaymentSigningKey | ||
, envStakeSKey :: !(Maybe GYStakeSigningKey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It now contains the user's payment and stake signing key.
Note that this monad is likely not necessary for app-like usecases. E.g the GY app code should probably use GYTxBuilderMonadIO
instead. Also exported here.
newtype RawLogger = RawLogger { unRawLogger :: GYLogContexts -> GYLogNamespace -> GYLogSeverity -> Text -> IO () } | ||
|
||
-- | A logger that does ignores the logs. | ||
unitRawLogger :: RawLogger | ||
unitRawLogger = RawLogger $ \_ _ _ _ -> pure () | ||
|
||
-- | A logger that ignores context and namespace and filters messages based on severity. | ||
simpleRawLogger :: GYLogSeverity -> (Text -> IO ()) -> RawLogger | ||
simpleRawLogger targetSev putLog = RawLogger $ \_ _ sev -> when (targetSev <= sev) . putLog | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The simple logger can filter based on log severity.
Also add law on `someUTxO`
…t of `GYTxMonadException`
…`GYTxMonad` interpreters
8b3f17e
to
481e23b
Compare
I just added |
6db114b
to
5e3c214
Compare
@@ -52,10 +55,12 @@ import GeniusYield.Types | |||
-- Setup | |||
------------------------------------------------------------------------------- | |||
|
|||
-- | This setup represents a two argument function where first argument is for logging & second represents for continuation, in need of `Ctx`. | |||
-- | This setup represents a three argument function where first two arguments are for logging & second is for the continuation, in need of `Ctx`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-- | This setup represents a three argument function where first two arguments are for logging & second is for the continuation, in need of `Ctx`. | |
-- | This setup represents a three argument function where first two arguments are for logging & third is for the continuation, in need of `Ctx`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/GeniusYield/TxBuilder/User.hs
Outdated
-- If something is | ||
-- it as well. But if boolean is `True`, framework would only use it as collateral and reserve it, if value in the given UTxO | ||
-- is exactly 5 ada. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-- If something is | |
-- it as well. But if boolean is `True`, framework would only use it as collateral and reserve it, if value in the given UTxO | |
-- is exactly 5 ada. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
src/GeniusYield/TxBuilder/Class.hs
Outdated
GYTxBuildFailure be -> throwError . GYBuildTxException $ GYBuildTxBalancingError be | ||
-- We know there is precisely one input. | ||
GYTxBuildNoInputs -> error "runGYTxMonadIOF: absurd" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GYTxBuildNoInputs -> error "runGYTxMonadIOF: absurd" | |
GYTxBuildNoInputs -> error "buildTxBodyF: absurd" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/GeniusYield/TxBuilder/Class.hs
Outdated
-> m GYTxBody | ||
buildTxBodyWithStrategy = buildTxBodyWithStrategy' | ||
|
||
buildTxBody :: GYTxBuilderMonad m => GYTxSkeleton v -> m GYTxBody |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: Add haddock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for missing this! I added it now. It's a pretty simple haddock for now but I think it suffices.
-- | ||
-- /Law:/ 'someUTxO' calls made after a call to 'submitTx' may return previously returned UTxOs | ||
-- if they were not affected by the submitted transaction. | ||
submitTx :: GYTx -> m GYTxId |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder whether we should put submitTx
and awaitTxConfirmed'
to some other monad as the current structures assumes the requirement of a signing key but what if built transaction doesn't require signing by such a key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Yes, the monad you're looking for is GYTxBuilderMonad
. That's essentially the replacement for current GYTxMonad
. See txmonad hierarchy and migration guide above and also these two comments:
I think for app-like usecases (most of GY codebase from what I remember) will just switch to GYTxBuilderMonad m => ...
and runGYTxBuilderMonadIO
and that's pretty much it. That won't require the keys.
|
||
-- | Wait for a _recently_ submitted transaction to be confirmed. | ||
-- | ||
-- /Note:/ If used on a transaction submitted long ago, the behavior is undefined. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: Can we avoid it being undefined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's behaviour is well defined, this can be part of query monad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that idea! This should definitely be part of the query monad because yes, it is indeed just a query.
I'd like to wait for the behavior to be defined first, indeed.
|
||
-- | This is expected to give the slot of the latest block. We say "expected" as we cache the result for 5 seconds, that is to say, suppose slot was cached at time @T@, now if query for current block's slot comes within time duration @(T, T + 5)@, then we'll return the cached slot but if say, query happened at time @(T + 5, T + 21)@ where @21@ was taken as an arbitrary number above 5, then we'll query the chain tip and get the slot of the latest block seen by the provider and then store it in our cache, thus new cached value would be served for requests coming within time interval of @(T + 21, T + 26)@. | ||
-- | ||
-- __NOTE:__ It's behaviour is slightly different, solely for our plutus simple model provider where it actually returns the value of the @currentSlot@ variable maintained inside plutus simple model library. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: Need to update this haddock in regards to CLB.
-- | Get available own UTxOs that can be operated upon. | ||
availableUTxOs :: m GYUTxOs | ||
|
||
-- | Return some unspend transaction output translatable to the given language corresponding to the script in question. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-- | Return some unspend transaction output translatable to the given language corresponding to the script in question. | |
-- | Return some unspent transaction output translatable to the given language corresponding to the script in question. |
achieve what it is meant to achieve? The purpose of that contraint is to signal | ||
that the query methods _should_ throw errors of type 'GYTxMonadException'. | ||
After all, some queries are bound to fail. But this expectation does not really | ||
restrict the type of exceptinos. In fact, an IO based monad could still |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
restrict the type of exceptinos. In fact, an IO based monad could still | |
restrict the type of exceptions. In fact, an IO based monad could still |
|
||
-- | 'GYTxQueryMonad' interpretation run under IO. | ||
type role GYTxQueryMonadIO representational | ||
newtype GYTxQueryMonadIO a = GYTxQueryMonadIO { runGYTxQueryMonadIO' :: GYTxQueryIOEnv -> IO a } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't it be a newtype
wrapper around ReaderT GYTxQueryIOEnv IO a
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But even o/w let's not bother about making this change now as we already used similar representation earlier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I started with indeed! But actually it changes the type roles. The a
in ReaderT
is set to nominal
(we cannot change this). We're looking for representational (to allow coercions). The function arrow ((->)
) doesn't have this restriction.
I am fine with it as I see merit in |
@@ -241,8 +244,18 @@ closeScribes genv = genv & logEnvToKatip & K.closeScribes <&> logEnvFromKatip | |||
-- Log configuration | |||
------------------------------------------------------------------------------- | |||
|
|||
newtype RawLogger = RawLogger { unRawLogger :: GYLogContexts -> GYLogNamespace -> GYLogSeverity -> Text -> IO () } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: Can the cfgLogDirector
be just GYLogEnv
instead of Either GYLogEnv GYRawLog
so as to just have Katip as log provider? Maybe info
from testCaseSteps
could be registered as a scribe inside Katip.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds good.
@@ -261,7 +274,7 @@ cfgAddContext i cfg = cfg { cfgLogContexts = addContext i (cfgLogContexts cfg) } | |||
logRun :: (HasCallStack, MonadIO m, StringConv a Text) => GYLogConfiguration -> GYLogSeverity -> a -> m () | |||
logRun GYLogConfiguration {..} sev msg = case cfgLogDirector of | |||
Left cfgLogEnv -> withFrozenCallStack $ K.runKatipT (logEnvToKatip cfgLogEnv) $ K.logLoc (logContextsToKatip cfgLogContexts) (logNamespaceToKatip cfgLogNamespace) (logSeverityToKatip sev) (K.logStr msg) | |||
Right GYRawLog {..} -> liftIO $ rawLogRun (toS @Text @String $ "[Namespaces: " <> Text.intercalate ", " (K.unNamespace $ logNamespaceToKatip cfgLogNamespace) <> "]" <> "[Contexts: " <> logContextsToS cfgLogContexts <> "] " <> toS @_ @Text msg) | |||
Right GYRawLog {..} -> liftIO $ unRawLogger rawLogRun cfgLogContexts cfgLogNamespace sev $ toS msg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears that this change no longer shows contexts & namespaces for privnet logs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I think that should be added to RawLogger
. I agree that it should exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think this should still be able to show contexts and namespaces but it depends on the GYRawLog
. So it's now controlled higher up the call stack. Specifically, it will depend on the GYLogConfiguration
and how it was obtained. The pre-provided simpleRawLogger
does not log them. (I found them to be extra noise in privnet tests).
Feel free to add a more verbose RawLogger
!
Thanks @TotallyNotChase for much need contribution and also thanks for making review easy with thorough comments! |
What is the motivation of getting rid of |
The |
I have removed the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @TotallyNotChase! PR looks good to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
Thank you for the contribution.
Summary
Closes #316
This is a long overdue redesign of
GYTxMonad
. It adds the ability to submit transactions to the monad and splits it into a more lawful hierarchy. In particular:GYTxQueryMonad
- For common utxo and similar queriesInstance:
GYTxQueryMonadIO
GYTxQueryMonad => GYTxSpecialQueryMonad
- For uncommon queries such as protocol parameters, era history etc. (this could potentially be removed, see note below)Instance:
GYTxQueryMonadIO
GYTxQueryMonad => GYTxUserQueryMonad
- For queries while acting as a user (having access to own wallet for querying purposes not signing purposes)Instance:
GYTxBuilderMonadIO
- see below.(GYTxSpecialQueryMonad, GYTxUserQueryMonad, Default (TxBuilderStrategy)) => GYTxBuilderMonad
- For building transactions while acting as a user. This allows different instances to choose their own transaction building method. IfTxBuilderStrategy
is set toGYCoinSelectionStrategy
(default), the implementation defaults to the pre-existing transaction building methods. Therefore, simplyanyclass
deriving this class will be enough to use the transaction building unchanged.instance:
GYTxBuilderMonadIO
GYTxBuilderMonad => GYTxMonad
- For interacting with the blockchain in a way that mutates its state. E.g building and submitting transactions etc. Also is able to sign as a user.Instance:
GYTxMonadIO
(GYTxMonad (TxMonadOf m), GYTxSpecialQueryMonad m) => GYTxGameMonad m
- This is an entirely new feature. This allows simulating multiple users interacting in an environment, building and submitting transactions etc. This is extremely useful in testing. Where we model an environment with many users interacting, waiting for events etc.Each "game' monad has its own "tx" monad. The tx monad code can be lifted into the "game" monad and assigned a user to perform it as.
Instance:
GYTxGameMonadIO
I suggest putting this brief note about the monad hierarchy in the docs so people can choose which monad they need, instead of always reaching for the most powerful one
Note the distinction between a TxQuery monad and the Tx monad. The query monad classes are meant to be non-mutating. In other words, if you are given a
GYTxUserQueryMonad m => m a
for example, and you run it (using a relevant interpreter) - you can be sure that it did not mutate anything on the chain. However, that guarantee does not exist forGYTxMonad
.This sort of a lawful type classes helps in "coloring" functions with types to define their expected behavior. Most code in the GY codebase (where the backend only builds transactions and passes it to frontend) can use just a query monad!
This makes running the monad instances (e.g
GYTxMonadIO
, renamed fromGYTxMonadNode
) much simpler. There is no traversable hack anymore. It's a simple monad interpreter. As you'd expect from any other proper monad in the ecosystem. As a result, transaction defining, building, signing, and submitting - all under one monad feels much more ergonomic.I understand that this is a large PR with breaking changes, but I really think it's for the best. I will annotate most of the significant changes with comments in this PR to make reviewing easier. I will post a summary with each important commit.
Furthermore, almost all commits are self-contained (they are granular and they compile). The only exception is the commit titled "Add 'submitTx' and 'awaitTxConfirmed' under 'GYTxMonad' and refine monad hierarchy". The subsequent commit is required to build the project.
Migration guide
For code that uses submission after building, migration is a bit more work but has a simple pattern. This pattern can be observed in Privnet code, for example.
Code that previously used traversing functions such as
rrunGYTxMonadNodeF
in order to build aGYTxSkeleton
contained within a traversable (e.g for interpetingGYTxMonadNode (Maybe (GYTxSkeleton v))
) will simply use well-known helpers to traverse instead, making things much simpler:For code that only built transactions but did not submit them, it is advised to replace the
GYTxMonadNode
usage withGYTxBuilderMonadIO
. Instead of returning a skeleton, it should usebuildTxBody
on the skeleton. So you'd have:GYTxBuilderMonadIO GYTxBody
. This can be run withrunGYTxBuilderMonadIO
to obtain theGYTxBody
like before. Alternative, define a functionbuildAndRunGYTxBuilderMonadIO :: GYTxBuilderMonadIO (GYTxSkeleton v) -> GYTxBody
, which is simply a composition ofbuildTxBody
followed byrunGYTxBuilderMonadIO
.where:
This way, a very simple find and replace action will complete most of the migration.
Extended ideas
There could be aAdded.GYTxMonad m => GYTxGameMonad m
, which could allow a "game situation" where we have multiple users building and submitting contracts, interacting with one another etc. This would essentially have methods likewaitUntilSlot
,waitUntilBlock
etc. There needs to be a functionasUser :: (GYTxMonad m, GYTxGameMonad n) => User -> m a -> n a
to facilitate the "game" flow.someUTxO
does not actually updateenvUsedSomeUTxOs
inGYTxMonadNode
#318 -GYTxBuilderMonadIO
should actually have itsenvUsedSomeUTxO
somehow exposed toGYTxMonadIO
so that thesubmitTx
implementation ofGYTxMonadIO
can reset it. I'd suggest making this field mutable. That way, a stateful flow can be achieved - which is necessary sinceenvUsedSomeUTxO
is meant to be stateful. I would discourage the usage of the state monad. Just utilizeReaderT _ IO
which we already have.eff
, if it's no longer experimental.Type of Change
Please mark the relevant option(s) for your pull request:
Checklist
Please ensure that your pull request meets the following criteria:
Testing
Please describe the tests you have added or modified, and provide any additional context or instructions needed to run the tests.
Additional Information
If you have any additional information or context to provide, such as screenshots, relevant issues, or other details, please include them here.