- Problem:
- Need to build Resolver, Inbox and L2 Node services but provide a consistent API for libXMTP that is quickly iterable on both ends (Gateways and libxmtp)
- Need:
- Easy to create more than one gateway
- Uses: JSONRPC to access on-chain state
- Uses: TBA - EthersRS uses JSON RPC to access chain state, although it is abstracted away
- Uses: TBA - Needs to use EthersRS for inbox contract too
libgateway -> client wrapper around Inbox, Resolver, and L2 Node Interfaces
Each service wants to define it's own endpoints, but libXMTP will need to use all these services regardless. So no matter what we do, we end up building some kind of API library to interact with the services
- make an API client library crate to combine services and provide a consistent interface for libXMTP
libXMTP and everything currently use ProtoBufs
- Is there any reason NOT to use protobufs?
- JSONRPC is standard across the blockchain world
- JSONRPC is easily composable and understandable; can be queried from the commandline without extra steps
- JSONRPC is used by L2 node, Eth, etc, so we're already importing it in our prototype/demo examples, and require it for blockchain interaction anyway
- JSONRPC is supported well in rust via jsonrpsee
- Better Rust WASM Support in JSONRPC (simple HTTP or WebSockets Requests)
- For Protobufs
- Protobufs define a standard to adhere to, already used in libxmtp
- one impl to rule them all, and very well supported and battle tested (Google)
As far as gateway modularization goes, i think it depends on a few directions we want to go. There are a some things that can be agreed upon:
- libXMTP needs one interface/client to access everything defined in these gateways, as it does today with Waku via gRPC.
- implies the creation of an api_client within libxmtp or as a crate regardless -- the 'Frontend'
- Each gateway defines custom functionality that needs to be queried somehow by the client
- We need JSONRPC no matter what, as the native transport of Ethereum/Optimism/L2 chains, and already used under-the-hood in ethers
I think there's a few ways to about this,
- Make all functionality defined by resolver, postal_service a library, one monolithic crate defining a standard server API libxmtp client interacts with.
- Pros:
- Endpoint configuration happens in one place
- Cons:
- Multiple PRs to change or add a feature (not unlike what we have with gRPC today), but with the added complexity of a PR
- Pros:
- Each gateway defines endpoints separately, combine implementations into one Server, or start a server per-service
- Pros:
- still monolithic server from view of client
- able to iterate within each gateway faster
- Services which are less coupled
- Cons:
- No one standard view of our d14n API, discoverability is worse
- could become confusing
- Pros:
file: rpc-definitions/did.rs
OR didethresolver
repository
use jsonrpsee::*;
[rpc(server, client, namespace = "did")]
pub trait DIDRegistry {
[method(name = "resolveDid")]
async fn resolveDid(&self, publicKey: String) -> ResultDidDocument, Error;
}
file: rpc-impls/did.rs
OR didethresolver
repository
use jsonrpsee::*;
use didethresolver::resolve_did;
pub struct DidRegistryMethods;
impl DidRegistryServer for DidRegistryMethods {
async fn resolveDid(&self, publicKey: String) -> ResultDidDocument, Error {
resolve_did(publicKey)
}
}
file: rpc-definitions/postal_service.rs
OR postal_service
repository
[rpc(server, client, namespace = "postalService")]
pub trait PostalService {
#[method(name = "sendMessage")]
async fn send_message(&self, message: String) -> Result(), Error;
}
file: rpc-impls/postal_service.rs
OR postal_service
repository
use postal_service::send_message;
pub struct PostalServiceMethods;
impl PostalServiceServer for PostalServiceMethods {
async fn send_message(&self, message: String) -> Result(), Error {
send_message(message);
}
}
in own separate crate, or within each gateway crate as needed or until complexity requires otherwise
use jsonrpsee::[RpcModule, Server](RpcModule, Server);
use crate::rpc_impls::*;
// could automatically enumerate methods on the server at runtime, depending on configuration.
pub async fn run_server_both() {
let rpc = RpcModule::new()
rpc.merge(DidRegistryMethods);
rpc.merge(PostalServiceMethods);
let server = Server::builder().build("127.0.0.1:8000").await?;
let addr = server.local_addr()?;
let handle = server.start(rpc);
}
pub async fn run_did() {
let rpc = RpcModule::new();
rpc.merge(DidRegistryMethods);
let server = Server::builder().build("127.0.0.1:8000").await?;
let addr = server.local_addr()?;
let handle = server.start(rpc);
}
pub async fn run_postal_service() {
let rpc = RpcModule::new();
rpc.merge(PostalServiceMethods);
let server = Server::builder().build("127.0.0.1:8000").await?;
let addr = server.local_addr()?;
let handle = server.start(rpc);
}