Skip to content
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

Replies support #418

Open
9 of 12 tasks
jawoznia opened this issue Aug 22, 2024 · 1 comment · May be fixed by #452
Open
9 of 12 tasks

Replies support #418

jawoznia opened this issue Aug 22, 2024 · 1 comment · May be fixed by #452
Assignees
Labels
enhancement New feature or request

Comments

@jawoznia
Copy link
Collaborator

jawoznia commented Aug 22, 2024

Feature outline based on #29 .

Since Sylvia already supports sv::msg(reply) and we will change the ReplyCtx type this feature should be considered breaking.

Although Sylvia allows marking methods with #[sv::msg(reply)] and generate reply entry point we do not utilize the possibilities of Sylvia macros like with ExecutorBuilder and BoundQuerier.

In this feature we should effortless:

  • Generation of unique reply IDs,
  • Builder for the messages,
  • Dispatching based on the generated IDs.

Affected macros

  • contract
  • entry_points

Desired API

#[contract]
impl Contract {
    #[sv::msg(reply, handlers=[add_admin, update_admin], reply_on=success)]
    fn reply_success(
        &self,
        ctx: ReplyCtx,
        data: DataT,
        arg1: String,
        arg2: u64,
    ) -> Result<Response, ContractError> {
    }

    #[sv::msg(reply, handlers=[add_admin], reply_on=failure)]
    fn reply_failure(
        &self,
        ctx: ReplyCtx,
        error: String,
        arg1: String,
        arg2: u64,
    ) -> Result<Response, ContractError> {
    }

    #[sv::msg(reply, handlers=[remove_admin])]
    fn reply_always(
        &self,
        ctx: ReplyCtx,
        data: Result<DataT, String>,
        arg1: String,
        arg2: u64,
    ) -> Result<Response, ContractError> {
    }

    #[sv::msg(reply)]
    fn no_alias(
        &self,
        ctx: ReplyCtx,
        data: Result<DataT, String>,
        arg1: String,
        arg2: u64,
    ) -> Result<Response, ContractError> {
    }
}

IDs generation

For every unique reply method name or alias an ID should be created.

#[sv::msg(reply, handlers=[alias, on_exec_failed])]
fn on_instantiated(...) {}

#[sv::msg(reply)]
fn on_exec_failed(...) {}

pub mod sv {
    pub const ALIAS_REPLY_ID: u64 = 1;
    pub const ON_EXEC_FAILED_REPLY_ID: u64 = 2;
}

By generating the IDs in Sylvia we remove the responsibility from the user as well as need for some boilerplate code.
The user will still be able to access those IDs in the sv module if needed.

In case an alias is provided it should be used to generate the reply id.

#[sv::msg(reply, handlers=[alias])]
fn on_instantiated(...) {}

#[sv::msg(reply, handlers=[other_alias])]
fn on_exec_failed(...) {}

pub mod sv {
    pub const ALIAS_REPLY_ID: u64 = 1;
    pub const OTHER_ALIAS_REPLY_ID: u64 = 2;
}

Dispatching

We keep the dispatching logic outside of the entry point and instead move it to a method like dispatch_reply() to provide reusability.

The method should match over every generated ID and provide additional checks for result of the submessage.
In case the handler for success or failure is not defined we will simply forward the result.

pub fn dispatch(
    deps: sylvia::cw_std::DepsMut<<Contract as sylvia::types::ContractApi>::CustomQuery>,
    env: sylvia::cw_std::Env,
    msg: sylvia::cw_std::Reply,
) -> std::result::Result<
    sylvia::cw_std::Response<<Contract as sylvia::types::ContractApi>::CustomMsg>,
    sylvia::cw_std::StdError,
> {
    let Reply {
        id,
        payload,
        gas_used,
        result,
    } = msg;

    match id {
        ADD_ADMIN_REPLY_ID => {
            match result {
                Ok(response) => {
                    let SubMsgResponse { events, data } = response;
                    let AddAdminReplyMsg { arg1, arg2 } = from_json(payload)?;
                    reply_success((deps, env, gas_used, events, data).into(), data.from_data(), arg1, arg2)
                },
                Err(error) => {
                    let AddAdminReplyMsg { arg1, arg2 } = from_json(payload)?;
                    reply_add_failure((deps, env, gas_used, vec![], vec![]).into(), error, arg1, arg2)
                }
        },
        UPDATE_ADMIN_REPLY_ID => {
            match result {
                Ok(response) => {
                    let SubMsgResponse { events, data } = response;
                    let UpdateAdminReplyMsg { arg1, arg2 } = from_json(payload)?;
                    reply_success((deps, env, gas_used, events, data).into(), data.from_data(), arg1, arg2)
                },
                Err(error) => {
                    Err(error)
                }
            }
        },
        REMOVE_ADMIN_REPLY_ID() => {
            let AddAdminReplyMsg { arg1, arg2 } = from_json(payload)?;
            reply_success((deps, env, gas_used, vec![], vec![]).into(), result, arg1, arg2)
        },
        _ => result.map(|_| Err(StdError::generic(format!("Unknown reply id: {}", id)))).map_err(Into::into),
    }
}

Note that above approach requires methods for a single identifier to have the same fields received from the Payload.
You can also define a single handler for an identifier-result pair.

For above to work we have to provide some new functionality.

FromData trait

** UPDATE **
Since Binary implements the Deserialize trait this specialization won't be possible due to overlapping implementations.
Most likely this part of feature will be obsoleted.

FromData trait will be used for deserialization of the Option<Binary>

  • Option<Binary> => Binary
  • Option<Binary> => Option<Binary>
  • Option<Binary> => T where T: Deserialize
  • Option<Binary> => Option<T> where T: Deserialize

The trait is performing Binary deserialization, keeping the special case converting to Binary skipping the deserialization, which would be later performed by the user - when the data field is not Json serialized.

trait FromData {
  fn from_data(data: Option<Binary>) -> Result<Self, String>;
}

ReplyCtx update

We will hide some informations that are less used in the ReplyCtx not to polute the methods signatures.

/// Represantation of `reply` context received in entry point as
pub struct ReplyCtx<'a, C: cosmwasm_std::CustomQuery = Empty> {
    pub deps: DepsMut<'a, C>,
    pub env: Env,
    pub gas_used: u64,
    /// From payload
    /// Empty vector if error
    pub events: Vec<Event>,
    /// From result
    /// Empty vector if error
    pub msg_responses: Vec<MsgResponse>,
}

Payload deserialization

The payload carry some information passed while constructing the SubMsg and works as a context.
We will make it seamless to use by deserializing it in the dispatching.

As this data is an JSON we can deserialize it to some intermediate generated type and pass it's fields to the methods.

Message building

Ideally we should provide a way to build not only WasmMsg::Execute messages, but also the WasmMsg::Instantiate and WasmMsg::Instantiate2.
The execute message should be sent to the external contract and it would be good to reuse the existing ExecutorBuilder for that.
In case of the Instantiate messages we do not have yet have the address so reusing the Remote and ExecutorBuilder is pointless, thus we should move the SubMsg creation to separate SubMsgBuilder type.

Example usage

For the WasmMsg::Execute we would use the existing ExecutorBuilder.

We should preserve the current functionality of constructing WasmMsg to not break the API.

Single Response can carry multiple submessages and we will provide a way to construct them with the schedule method. Additionally, the user will be able to call reply derived method to provide a handler

self.remote
    .executor(&mut resp)
    // Caches the message
    .call_me()
    // Creates submessage with above message and below reply id
    .reply_foo()
    // Adds the submessage to the `Response`
    .schedule()
    .call_me_later()
    .schedule()
    .call_me_again()
    .reply_bar()
    .schedule();

Roadmap

@jawoznia jawoznia added the enhancement New feature or request label Aug 22, 2024
@jawoznia jawoznia self-assigned this Aug 22, 2024
@jawoznia
Copy link
Collaborator Author

jawoznia commented Oct 9, 2024

Update SubMsg

To build on top of the existing infrastructure we can generate a trait with method for every method in the contract marked with sv::msg(reply).
We will generate the implementation for WasmMsg, CosmosMsg and SubMsg constructing the SubMsg from them with appropriate reply_id and reply_on.

Since we know what payload is expected on the reply side we can require user to provide the values in the trait methods.
The serialization of the payload would occur underneath.
This would require user to always provide the payload, even if it would be the Binary::default, but I don't think there is a better approach.

The benefit of this approach is its simplicity as we won't have to provide multiple builders with different states.
Also the SubMsg construction will work well with non sylvia contracts.

Example

pub struct Contract {
    remote: Remote<OtherContract>,
}

impl Contract {
    #[sv::msg(exec)]
    fn send_submsg(&self, ctx: ExecCtx) -> StdResult<Response> {
        let admin = ...;

        let msg = self
            .remote
            .load(ctx.deps.storage)?
            .executor()                                   // Create ExecutorBuilder
            .noop()?                                       // Create `other_contract::ExecuteMsg::Noop` 
            .build()                                         // Create `WasmMsg`
            .remote_instantiated(admin)?;   // Create `SubMsg`
    }

    #[sv::msg(reply)]
    fn remote_instantiated(&self, ctx: ReplyCtx, admin: Addr) -> StdResult<Response> {
        ...
    }
}

@jawoznia jawoznia linked a pull request Nov 5, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant