feat: contracttrait macro#1
Open
willemneal wants to merge 18 commits intomainfrom
Open
Conversation
bd23c7d to
12315d0
Compare
12315d0 to
8ac4ff8
Compare
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR introduces two macros to make contract's more composable. To motivate their use consider the following traits and how one would implement them to be easily added to a contract.
Example implementation
First we need to provide some default implementations:
Next we need our contract struct which will expose the methods.
As you can tell this is a lot of boilerplate if you wanted to add this functionality to your contract.
Associated type
One thing we can do to make this less boilerplate is make use of associated types and default trait methods. This way when defining the trait we can provide the default or base implementation which will be used.
However, we have a problem after we successfully compile this code. The generated contract will have no exposed methods, because the
contractimplmacro can only generate the external methods for those.So with this:
So without
contractimplbeing able to look up the definition of initialtraitwe are pretty much back where we started.Introducing the
contracttraitmacroThe main idea behind
contracttraitmacro is that we can make it the trait's responsibility to generate the methods needed forcontractimpl.Will generate edit the trait to add default methods using an associated type and a declarative macro:
Interestingly, since declarative macro's are in a different namespace it can share the same name as the trait.
Then to use in our example:
That's it!
It even lets you override the default implementation if you provide a second argument. The other benefit here is that we separate
Contract's implementation of the trait from it's public interface which will come in handy later!Trait Extensions
Not to be confused with extension traits, what I'm calling trait extensions are special types which let you combine or extend multiple implementations of a trait.
In the initial implementation,
Upgradable's default didn't handle authentication at all.Let's consider making an
Administratableextension. This is not a runNow we just need to make
AdministratableExtUpgradable:This makes use of blanket implementations in rust. It is saying for any type
Twhich isAdministratableand typeNwhich isUpgradableimplementsUpgradableforAdministrtatableExt.Then we can use this in place of the default.
This means the
upgradewill be called on the extension, which will useContract's implementation ofAdministratable, to require that the admin provided authentication, then it will useUpgrader, which implementsUpgradable, to call its version ofupgrade.To use it with the macro:
While this might seem over complex it allows for more complicated composability. Take this example of a FungibleToken which is both
PausableandFungibleBlockListUsing
contracttraitwhen implementing traitWhile the macros make it easier we can do better. You can also use
contracttraitwhen implementing the trait on your contract.Generates:
More options and compile time checks
contracttraitcan also provide additional arguments:default_requiredEnsures that a default is provided.
This ensures that the following will fail to compile:
#[internal]Since there is a separation with what is exposed by
#[contractimpl]and the trait implementation we can have some methods be internal only.Now you can use the internal method with in the contract for example with trait extensions:
References in arguments
One limitation of the external interface generated by
#[contractimpl]is that the arguments of an exposed method must pass by value (besides the required Env). E.g.fn set_admin(env: &Env, new_admin: soroban_sdk::Address)Again we can take advantage of the separation of the trait interface and the external interface. When generating a trait method the external argument can be a value and then pass to the reference to the contract trait method.
Generates a macro with a case:
This has one downside though and that is that the derived
Contractstruct will have two methods namedadmin_setwhich differ in their signature so in some cases this requires casting to remove ambiguity.