Cairo 1: Contract Syntax is Evolving

@FeedTheFed sometimes we need a @view function to “mutate the state” (without committing it obviously) for the purpose of its calculations.
For example, is_valid_signature needs to consider whether a time-locked remove_signer_with_etd has expired - in which case is_valid_signature will fail if it was called with the removed signer.
Similarly, another use-case is our “sign-later” multisig impl which needs to know whether a pending multi-sig request has expired

We implement this by running the time-locked request handler at the beginning of the @view function - this brings the contract into the expected state, and then subsequent logic is run on that state.

I want to make sure this is still supported in the suggested changes

GM,
At the moment if I have a pure function, I’ll have to make such an interface:

#[starknet::interface]
trait SomeTrait<TContractState> {
    fn some_fn(self: @TContractState, some_param: felt252) -> bool;
}

But if this function is pure (it doesn’t even require to read the contract state) it’ll just cluter the code by adding some overhead and could lead to some questions like
“Why do I have an argument that isn’t even used/useful?”

Is this intended?

Thx in advance :slight_smile:

ATM, this can be generalized in the future. We simplified the emit syntax a bit by auto-implementing into for the event’s structs, so emit can take the struct directly and it will be converted to the corresponding variant (see here).

I really appreciate the simplification about events!
Makes the code a lot more readable with complex events :rocket:

And the functions implementation and type name could be anything, like PrivateFunctions/PrivateMethods/PrivateTrait/etc.? As in, does the trait that’s mentioned – PrivateFunctionsTrait – have to be explicitly defined anywhere or anything goes and these are just arbitrary chosen names?

When will components be released?

What is the recommended way to implement extensibility before that happens? It’s not clear how ContractState from external libraries is explicitly passed in under the new syntax.

Anything goes, for private functions you can use the #[generate_trait] attribute in order to avoid having to choose a name for the trait (note that you don’t HAVE to define a separate impl for them, it’s convenient though as it will allow using the dot operator with self.private_function)

Aiming at <1 month from today. In the meantime, you can initialize ContractState unsafely as follows:

let state: OtherContractModule::ContractState = OtherContractModule::unsafe_new_contract_state();
let res = OtherContractModule::ExternalImplName::external_function_name(@state);

For functions that take state by ref define let mut state = ... and pass it by ref. Note that it’s only a hackish way to do things before we have components.

Apologies for the late reply, really appreciate your feedback!

  • The events attribute are untouched for now, in the future, we can consider not having to explicitly deriving (99% of events will probably derive the Event trait rather than define a custom implementation).

  • We simplified the events emission syntax (there’s a generated Into impl from the structs to the Event variants).

Re components

  • Contract vs component - If you only want to use this logic as part of a larger context, then components are the thing for you. I think OZ standards are the classical example for components (I want my contract to be “ownable” or “upgradable”, I don’t want a new deployed contract, I just want the functionality)

  • We’re trying to keep the fancy generics visible to the developer to a minimum. In 1-2 weeks I’ll publish another post focused on components, and we can discuss the more mature design there. Would love to get your input then.

We use a quite chunky monolithic contract that calls out to other modules for logic whenever possible live on mainnet right now (Carmine governance).

Until components arrive, what’s the way to access the state of the contract from other modules? I can’t call .read / .write on the state obtained through unsafe_new_contract_state since that uses InternalContractStateTrait which has the same name but is defined differently for every single type that resides in storage.

Is this really the best that can be done?

let state : ActualContract::ContractState = ActualContract::unsafe_new_contract_state();
let yay = ActualContract::storage_member_name::InternalContractStateTrait::read(@state.storage_member_name, param);

just a small clarification regarding internal functions.
Do you mean

impl PrivateFunctons of PrivateFunctionsTrait {
...
}

in the example you have ... PrivateFunctionsTrait` { is
` a typo ?

I have a couple of questions that involve the strategy through which the component implementation is able to retrieve a reference to it’s state.

  1. What happens when two components use the same name for their storage? (the name being the argument to contract_state(name))
  2. What if they use the same name for their storage but only have one entry in the contracts’ storage struct?

Example scenarios:

  • I’m using two components which both inherit / depend on another base (stateful) component, and I’d like to keep their state separate.
  • I might want to have the same component twice.
  • I’m using two libraries with components and they use the same storage names.
  1. In the post you mention:

When we write self.component_snap(), the compiler looks for impls that have a component_snap method whose first argument is of the same type as self, in this case TContractState. Luckily, it finds it in the GetComponent<TContractState, OwnableStorage> impl, I, on which OwnableImpl depends. Again we were able to utilize standard Cairo 1.0 behavior in our smart contract syntax.

However the dependency is on GetComponent<TContractState, OwnableState>> OwnableStorage vs Ownable State. Is this a typo, or is there something else that’s implicitly ensuring this depencency is present?

Good points!

  1. Each component will have a different address space (up to hash collisions); it will define an offset that will be used to hash addresses used from the component.
  2. A component instance must appear in your Storage struct. It affects your storage/events, so the Storage and Event types have to be changed accordingly. For example, I can have two instances of the same component, e.g. with ownable1: OwnableStorage and ownable2: OwnableStorage, where both are annotated with some attributed (this attribute will tell the Starknet plugin in the compiler to generate a HasComponent Impl).
  3. It’s a typo, should be OwnableState (from there you can access storage variables).

Hey, apologies for the delay, if you want to access storage directly it’s indeed a bit more cumbersome with unsafe contract state, but there’s a better option if you import the right trait.

Take this example, then you can replace:

let mut state = Governance::unsafe_new_contract_state();
let applied: felt252 = proposal_applied::InternalContractStateTrait::read(
   @state.proposal_applied, prop_id
);

with

use governance::contract::Governance::proposal_appliedContractStateTrait;
...
let mut state = Governance::unsafe_new_contract_state();
let applied = state.proposal_applied.read(prop_id);

By importing the generated trait per storage variable, yo can access the unsafe state directly.

Yes, a typo (need more characters)

  1. Each component will have a different address space (up to hash collisions); it will define an offset that will be used to hash addresses used from the component.

Perfect!

How does this work? (feel free to drop me a link if I should just go read some docs :see_no_evil:)

From your description above it seems like the compiler uses the name passed to contract_state(…) to find the right component storage. However, from what you describe it would need to use additional information (how do you distinguish between ownable1 and ownable2).

  1. A component instance must appear in your Storage struct. It affects your storage/events, so the Storage and Event types have to be changed accordingly. For example, I can have two instances of the same component, e.g. with ownable1: OwnableStorage and ownable2: OwnableStorage, where both are annotated with some attributed (this attribute will tell the Starknet plugin in the compiler to generate a HasComponent Impl).

How would this work in terms of linking the implementations? Do I do two impls? How do I link one implementation to ownable1 and the other to ownable2?

Re offset, the GetComponent trait will probably include an function that returns the offset, the implementation for this trait will be generated by the compiler based on names given in the using contract, so offest for different component instances can differ. Note that the design is not yet finalized, hopefully will publish a post dedicated to components in the near future.

Re linking to impl, again this is not finalized but:

   impl Ownable = OwnableImpl<ContractState, _>;

Would probably have to explicitly say which GetComponent impls to use, as now there are two and the compiler can’t infer which one to choose.

What’s the status of Components? Can we expect them to be included in the next Starknet version (0.13)?

Hopefully, long before that (a few weeks, 0.13 is early Q4)