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

Write proc-macro to automatically declare and implement the Rpc trait. #62

Open
richardpringle opened this issue Sep 28, 2023 · 3 comments

Comments

@richardpringle
Copy link
Collaborator

Right now, one has to create an Rpc trait, define all the methods, then define the implementation. It would be way nicer if we could just have one impl block that both defined the trait as well as the implementation. This should be pretty easy to accomplish with our own proc-macro.

TODO:
write example of desired output

@dimitrovmaksim
Copy link
Contributor

dimitrovmaksim commented Nov 16, 2023

Hey @richardpringle 👋🏻 I have one question about the proc macro implementation. In timestampvm-rs, the Rpc trait is declared twice, with different sets of methods, one has only the ping method, and the other has 4 methods.

Do you want to implement all of the methods available, even though users may not need them,

#[derive(Rpc)]
struct Api {}

fn main() {
    let api = Api {};
    api.ping();
    api.propose_block();
    api.last_accepted();
    api.get_block();
}

or each method to be a different macro, and the devs to choose which ones to implement, something like:

#[derive(Ping, GetBlock)]
struct Api {}

fn main() {
    let api = Api {};
    api.ping();
    api.get_block();
    api.propose_block(); // method not found
    api.last_accepted(); // method not found
}

@dimitrovmaksim
Copy link
Contributor

dimitrovmaksim commented Nov 16, 2023

Actually now that i'm deep diving into the issue, I realized maybe I'm missing some contex of what the final goal is.

For example the implementation of last_accepted for ChainService relies on the implementation of the VM module, which seems to be "custom" for the timestamp VM, i.e. it could be implemented differenlty, for example use different struct, fields or method names.

@richardpringle
Copy link
Collaborator Author

Actually now that i'm deep diving into the issue, I realized maybe I'm missing some contex of what the final goal is.

For example the implementation of last_accepted for ChainService relies on the implementation of the VM module, which seems to be "custom" for the timestamp VM, i.e. it could be implemented differenlty, for example use different struct, fields or method names.

The initial goal is to prevent the repetition. Something like this:

use avalanche_types::rpc; 

#[rpc]
impl<A> ChainService<A>
where
    A: Send + Sync + Clone + 'static, // not sure that all of these would be needed
{
    #[rpc(name = "ping", alias("timestampvm.ping"))]
    fn ping(&self) -> BoxFuture<Result<crate::api::PingResponse>> {
        log::debug!("ping called");
        Box::pin(async move { Ok(crate::api::PingResponse { success: true }) })
    }

    #[rpc(name = "proposeBlock", alias("timestampvm.proposeBlock"))]
    fn propose_block(&self, args: ProposeBlockArgs) -> BoxFuture<Result<ProposeBlockResponse>> {
        log::debug!("propose_block called");
        let vm = self.vm.clone();

        Box::pin(async move {
            vm.propose_block(args.data)
                .await
                .map_err(create_jsonrpc_error)?;
            Ok(ProposeBlockResponse { success: true })
        })
    }

    #[rpc(name = "lastAccepted", alias("timestampvm.lastAccepted"))]
    fn last_accepted(&self) -> BoxFuture<Result<LastAcceptedResponse>> {
        log::debug!("last accepted method called");
        let vm = self.vm.clone();

        Box::pin(async move {
            let vm_state = vm.state.read().await;
            if let Some(state) = &vm_state.state {
                let last_accepted = state
                    .get_last_accepted_block_id()
                    .await
                    .map_err(create_jsonrpc_error)?;

                return Ok(LastAcceptedResponse { id: last_accepted });
            }

            Err(Error {
                code: ErrorCode::InternalError,
                message: String::from("no state manager found"),
                data: None,
            })
        })
    }

    #[rpc(name = "getBlock", alias("timestampvm.getBlock"))]
    fn get_block(&self, args: GetBlockArgs) -> BoxFuture<Result<GetBlockResponse>> {
        let blk_id = ids::Id::from_str(&args.id).unwrap();
        log::info!("get_block called for {}", blk_id);

        let vm = self.vm.clone();

        Box::pin(async move {
            let vm_state = vm.state.read().await;
            if let Some(state) = &vm_state.state {
                let block = state
                    .get_block(&blk_id)
                    .await
                    .map_err(create_jsonrpc_error)?;

                return Ok(GetBlockResponse { block });
            }

            Err(Error {
                code: ErrorCode::InternalError,
                message: String::from("no state manager found"),
                data: None,
            })
        })
    }
}

But I'm not sure how feasible this is. Right now, that rpc attribute is coming from the deprecated jsonrpc_derive crate instead of the updated jsonrpsee crate.

We might need to take a step back and look at how this interacts with avalanche-types, though. There may be a simpler way to create an rpc-server.

I would skip this issue for now. If you want, you could migrate to jsonrpsee. We want something that's maintained in here so that it can get the latest hyper updates (in case of any vulnerabilities)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants