diff --git a/Cargo.lock b/Cargo.lock index d780bf0c5..9dd3d106d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1138,7 +1138,6 @@ dependencies = [ "calimero-primitives", "calimero-store", "camino", - "ed25519-dalek 2.1.1", "eyre", "futures-util", "rand 0.8.5", @@ -1157,6 +1156,7 @@ dependencies = [ "bs58 0.5.1", "ed25519-dalek 2.1.1", "either", + "eyre", "near-crypto 0.26.0", "near-jsonrpc-client 0.13.0", "near-jsonrpc-primitives 0.26.0", diff --git a/crates/context/Cargo.toml b/crates/context/Cargo.toml index 86e8d3d17..ebe2494e6 100644 --- a/crates/context/Cargo.toml +++ b/crates/context/Cargo.toml @@ -8,7 +8,6 @@ license.workspace = true [dependencies] camino = { workspace = true, features = ["serde1"] } -ed25519-dalek.workspace = true eyre.workspace = true futures-util.workspace = true rand.workspace = true diff --git a/crates/context/config/Cargo.toml b/crates/context/config/Cargo.toml index 1fbebc8f5..c12185579 100644 --- a/crates/context/config/Cargo.toml +++ b/crates/context/config/Cargo.toml @@ -11,6 +11,7 @@ bs58.workspace = true borsh = { workspace = true, features = ["derive"] } ed25519-dalek.workspace = true either = { workspace = true, optional = true } +eyre = { workspace = true, optional = true } near-crypto = { workspace = true, optional = true } near-jsonrpc-client = { workspace = true, optional = true } near-jsonrpc-primitives = { workspace = true, optional = true } @@ -18,9 +19,9 @@ near-primitives = { workspace = true, optional = true } reqwest = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -starknet.workspace = true -starknet-crypto.workspace = true -starknet-types-core.workspace = true +starknet = { workspace = true, optional = true } +starknet-crypto = { workspace = true, optional = true } +starknet-types-core = { workspace = true, optional = true } thiserror.workspace = true url = { workspace = true, optional = true } @@ -30,10 +31,14 @@ workspace = true [features] client = [ "dep:either", + "dep:eyre", "dep:near-crypto", "dep:near-jsonrpc-client", "dep:near-jsonrpc-primitives", "dep:near-primitives", "reqwest/json", - "dep:url", + "dep:starknet", + "dep:starknet-crypto", + "dep:starknet-types-core", + "url/serde", ] diff --git a/crates/context/config/src/client.rs b/crates/context/config/src/client.rs index c3be2cb40..60bc403fb 100644 --- a/crates/context/config/src/client.rs +++ b/crates/context/config/src/client.rs @@ -1,143 +1,48 @@ -use core::convert::Infallible; -use core::error::Error as CoreError; -use core::marker::PhantomData; -use core::ptr; use std::borrow::Cow; -use std::collections::BTreeMap; -use ed25519_dalek::Signature; use either::Either; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Error as JsonError}; +use env::Method; use thiserror::Error; -use crate::repr::Repr; -use crate::types::{ - self, Application, Capability, ContextId, ContextIdentity, Revision, Signed, SignerId, -}; -use crate::{ContextRequest, ContextRequestKind, Request, RequestKind}; - pub mod config; -pub mod near; +pub mod env; +pub mod protocol; pub mod relayer; -pub mod starknet; - -use config::{ContextConfigClientConfig, ContextConfigClientSelectedSigner, Credentials, Protocol}; - -pub trait Transport { - type Error: CoreError; - - #[expect(async_fn_in_trait, reason = "Should be fine")] - async fn send( - &self, - request: TransportRequest<'_>, - payload: Vec, - ) -> Result, Self::Error>; -} - -impl Transport for Either { - type Error = Either; - - async fn send( - &self, - request: TransportRequest<'_>, - payload: Vec, - ) -> Result, Self::Error> { - match self { - Self::Left(left) => left.send(request, payload).await.map_err(Either::Left), - Self::Right(right) => right.send(request, payload).await.map_err(Either::Right), - } - } -} - -#[derive(Debug)] -#[non_exhaustive] -pub struct TransportRequest<'a> { - pub protocol: Protocol, - pub network_id: Cow<'a, str>, - pub contract_id: Cow<'a, str>, - pub operation: Operation<'a>, -} +pub mod transport; -impl<'a> TransportRequest<'a> { - #[must_use] - pub const fn new( - protocol: Protocol, - network_id: Cow<'a, str>, - contract_id: Cow<'a, str>, - operation: Operation<'a>, - ) -> Self { - Self { - protocol, - network_id, - contract_id, - operation, - } - } -} +use config::{ClientConfig, ClientSelectedSigner, Credentials}; +use protocol::{near, starknet, Protocol}; +use transport::{Both, Transport, TransportRequest}; -#[derive(Debug, Serialize, Deserialize)] -#[expect(clippy::exhaustive_enums, reason = "Considered to be exhaustive")] -pub enum Operation<'a> { - Read { method: Cow<'a, str> }, - Write { method: Cow<'a, str> }, -} +pub type AnyTransport = Either< + relayer::RelayerTransport, + Both, starknet::StarknetTransport<'static>>, +>; #[derive(Clone, Debug)] -pub struct ContextConfigClient { +pub struct Client { transport: T, } -impl ContextConfigClient { +impl Client { pub const fn new(transport: T) -> Self { Self { transport } } } -pub type AnyTransport = Either< - relayer::RelayerTransport, - BothTransport, starknet::StarknetTransport<'static>>, ->; - -#[expect(clippy::exhaustive_structs, reason = "this is exhaustive")] -#[derive(Debug, Clone)] -pub struct BothTransport { - pub near: L, - pub starknet: R, -} - -impl Transport for BothTransport { - type Error = Either; - - async fn send( - &self, - request: TransportRequest<'_>, - payload: Vec, - ) -> Result, Self::Error> { - match request.protocol { - Protocol::Near => self.near.send(request, payload).await.map_err(Either::Left), - Protocol::Starknet => self - .starknet - .send(request, payload) - .await - .map_err(Either::Right), - } - } -} - -impl ContextConfigClient { +impl Client { #[must_use] - pub fn from_config(config: &ContextConfigClientConfig) -> Self { + pub fn from_config(config: &ClientConfig) -> Self { let transport = match config.signer.selected { - ContextConfigClientSelectedSigner::Relayer => { + ClientSelectedSigner::Relayer => { // If the selected signer is Relayer, use the Left variant. Either::Left(relayer::RelayerTransport::new(&relayer::RelayerConfig { url: config.signer.relayer.url.clone(), })) } - ContextConfigClientSelectedSigner::Local => Either::Right(BothTransport { - near: near::NearTransport::new(&near::NearConfig { + ClientSelectedSigner::Local => Either::Right(Both { + left: near::NearTransport::new(&near::NearConfig { networks: config .signer .local @@ -164,7 +69,7 @@ impl ContextConfigClient { }) .collect(), }), - starknet: starknet::StarknetTransport::new(&starknet::StarknetConfig { + right: starknet::StarknetTransport::new(&starknet::StarknetConfig { networks: config .signer .local @@ -197,332 +102,105 @@ impl ContextConfigClient { } } -impl ContextConfigClient { - pub const fn query<'a>( +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum ClientError { + #[error("transport error: {0}")] + Transport(T::Error), + #[error("codec error: {0}")] + Codec(#[from] eyre::Report), + #[error("unsupported protocol: {0}")] + UnsupportedProtocol(String), +} + +impl Client { + async fn send( + &self, + request: TransportRequest<'_>, + payload: Vec, + ) -> Result, T::Error> { + self.transport.send(request, payload).await + } + + pub fn query<'a, E: Environment<'a, T>>( &'a self, - protocol: Protocol, + protocol: Cow<'a, str>, network_id: Cow<'a, str>, contract_id: Cow<'a, str>, - ) -> ContextConfigQueryClient<'a, T> { - ContextConfigQueryClient { + ) -> E::Query { + E::query(CallClient { protocol, network_id, contract_id, - transport: &self.transport, - } + client: self, + }) } - pub const fn mutate<'a>( + pub fn mutate<'a, E: Environment<'a, T>>( &'a self, - protocol: Protocol, + protocol: Cow<'a, str>, network_id: Cow<'a, str>, contract_id: Cow<'a, str>, - signer_id: SignerId, - ) -> ContextConfigMutateClient<'a, T> { - ContextConfigMutateClient { + ) -> E::Mutate { + E::mutate(CallClient { protocol, network_id, contract_id, - signer_id, - transport: &self.transport, - } + client: self, + }) } } -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum ConfigError { - #[error("transport error: {0}")] - Transport(T::Error), - #[error(transparent)] - Other(#[from] types::ConfigError), +#[derive(Debug)] +pub struct CallClient<'a, T> { + protocol: Cow<'a, str>, + network_id: Cow<'a, str>, + contract_id: Cow<'a, str>, + client: &'a Client, } #[derive(Debug)] -pub struct Response { - bytes: Vec, - _priv: PhantomData, +pub enum Operation { + Read(M), + Write(M), } -impl Response { - const fn new(bytes: Vec) -> Self { - Self { - bytes, - _priv: PhantomData, - } - } - - pub fn parse<'a>(&'a self) -> Result +impl<'a, T: Transport> CallClient<'a, T> { + async fn send>( + &self, + params: Operation, + ) -> Result> where - T: Deserialize<'a>, + P: Protocol, { - serde_json::from_slice(&self.bytes) - } -} + let method = Cow::Borrowed(M::METHOD); -#[derive(Debug)] -pub struct ContextConfigQueryClient<'a, T> { - protocol: Protocol, - network_id: Cow<'a, str>, - contract_id: Cow<'a, str>, - transport: &'a T, -} - -impl<'a, T: Transport> ContextConfigQueryClient<'a, T> { - async fn read( - &self, - method: &str, - body: I, - ) -> Result, ConfigError> { - let payload = serde_json::to_vec(&body).map_err(|err| ConfigError::Other(err.into()))?; + let (operation, payload) = match params { + Operation::Read(params) => (transport::Operation::Read { method }, params.encode()?), + Operation::Write(params) => (transport::Operation::Write { method }, params.encode()?), + }; let request = TransportRequest { - protocol: self.protocol, + protocol: Cow::Borrowed(&self.protocol), network_id: Cow::Borrowed(&self.network_id), contract_id: Cow::Borrowed(&self.contract_id), - operation: Operation::Read { - method: Cow::Borrowed(method), - }, + operation, }; let response = self - .transport - .send(request, payload) - .await - .map_err(ConfigError::Transport)?; - - Ok(Response::new(response)) - } - - pub async fn application( - &self, - context_id: ContextId, - ) -> Result>, ConfigError> { - self.read( - "application", - json!({ - "context_id": Repr::new(context_id), - }), - ) - .await - } - - pub async fn application_revision( - &self, - context_id: ContextId, - ) -> Result, ConfigError> { - self.read( - "application_revision", - json!({ - "context_id": Repr::new(context_id), - }), - ) - .await - } - - pub async fn members( - &self, - context_id: ContextId, - offset: usize, - length: usize, - ) -> Result>>, ConfigError> { - self.read( - "members", - json!({ - "context_id": Repr::new(context_id), - "offset": offset, - "length": length, - }), - ) - .await - } - - pub async fn members_revision( - &self, - context_id: ContextId, - ) -> Result, ConfigError> { - self.read( - "members_revision", - json!({ - "context_id": Repr::new(context_id), - }), - ) - .await - } - - pub async fn privileges( - &self, - context_id: ContextId, - identities: &[ContextIdentity], - ) -> Result, Vec>>, ConfigError> { - let identities = unsafe { - &*(ptr::from_ref::<[ContextIdentity]>(identities) as *const [Repr]) - }; - - self.read( - "privileges", - json!({ - "context_id": Repr::new(context_id), - "identities": identities, - }), - ) - .await - } -} - -#[derive(Debug)] -pub struct ContextConfigMutateClient<'a, T> { - protocol: Protocol, - network_id: Cow<'a, str>, - contract_id: Cow<'a, str>, - signer_id: SignerId, - transport: &'a T, -} - -#[derive(Debug)] -pub struct ClientRequest<'a, 'b, T> { - client: &'a ContextConfigMutateClient<'a, T>, - kind: RequestKind<'b>, -} - -impl ClientRequest<'_, '_, T> { - pub async fn send Signature>(self, sign: F) -> Result<(), ConfigError> { - let signed = Signed::new(&Request::new(self.client.signer_id, self.kind), sign)?; - - let request = TransportRequest { - protocol: self.client.protocol, - network_id: Cow::Borrowed(&self.client.network_id), - contract_id: Cow::Borrowed(&self.client.contract_id), - operation: Operation::Write { - method: Cow::Borrowed("mutate"), - }, - }; - - let payload = serde_json::to_vec(&signed).map_err(|err| ConfigError::Other(err.into()))?; - - let _unused = self .client - .transport .send(request, payload) .await - .map_err(ConfigError::Transport)?; + .map_err(ClientError::Transport)?; - Ok(()) + M::decode(response).map_err(ClientError::Codec) } } -impl ContextConfigMutateClient<'_, T> { - #[must_use] - pub const fn add_context<'a>( - &self, - context_id: ContextId, - author_id: ContextIdentity, - application: Application<'a>, - ) -> ClientRequest<'_, 'a, T> { - let kind = RequestKind::Context(ContextRequest { - context_id: Repr::new(context_id), - kind: ContextRequestKind::Add { - author_id: Repr::new(author_id), - application, - }, - }); - - ClientRequest { client: self, kind } - } - - #[must_use] - pub const fn update_application<'a>( - &self, - context_id: ContextId, - application: Application<'a>, - ) -> ClientRequest<'_, 'a, T> { - let kind = RequestKind::Context(ContextRequest { - context_id: Repr::new(context_id), - kind: ContextRequestKind::UpdateApplication { application }, - }); - - ClientRequest { client: self, kind } - } - - #[must_use] - pub const fn add_members( - &self, - context_id: ContextId, - members: &[ContextIdentity], - ) -> ClientRequest<'_, 'static, T> { - let members = unsafe { - &*(ptr::from_ref::<[ContextIdentity]>(members) as *const [Repr]) - }; - - let kind = RequestKind::Context(ContextRequest { - context_id: Repr::new(context_id), - kind: ContextRequestKind::AddMembers { - members: Cow::Borrowed(members), - }, - }); +pub trait Environment<'a, T> { + type Query; + type Mutate; - ClientRequest { client: self, kind } - } - - #[must_use] - pub const fn remove_members( - &self, - context_id: ContextId, - members: &[ContextIdentity], - ) -> ClientRequest<'_, 'static, T> { - let members = unsafe { - &*(ptr::from_ref::<[ContextIdentity]>(members) as *const [Repr]) - }; - - let kind = RequestKind::Context(ContextRequest { - context_id: Repr::new(context_id), - kind: ContextRequestKind::RemoveMembers { - members: Cow::Borrowed(members), - }, - }); - - ClientRequest { client: self, kind } - } - - #[must_use] - pub const fn grant( - &self, - context_id: ContextId, - capabilities: &[(ContextIdentity, Capability)], - ) -> ClientRequest<'_, 'static, T> { - let capabilities = unsafe { - &*(ptr::from_ref::<[(ContextIdentity, Capability)]>(capabilities) - as *const [(Repr, Capability)]) - }; - - let kind = RequestKind::Context(ContextRequest { - context_id: Repr::new(context_id), - kind: ContextRequestKind::Grant { - capabilities: Cow::Borrowed(capabilities), - }, - }); - - ClientRequest { client: self, kind } - } - - #[must_use] - pub const fn revoke( - &self, - context_id: ContextId, - capabilities: &[(ContextIdentity, Capability)], - ) -> ClientRequest<'_, 'static, T> { - let capabilities = unsafe { - &*(ptr::from_ref::<[(ContextIdentity, Capability)]>(capabilities) - as *const [(Repr, Capability)]) - }; - - let kind = RequestKind::Context(ContextRequest { - context_id: Repr::new(context_id), - kind: ContextRequestKind::Revoke { - capabilities: Cow::Borrowed(capabilities), - }, - }); - - ClientRequest { client: self, kind } - } + fn query(client: CallClient<'a, T>) -> Self::Query; + fn mutate(client: CallClient<'a, T>) -> Self::Mutate; } diff --git a/crates/context/config/src/client/config.rs b/crates/context/config/src/client/config.rs index 3f9d539e9..1cbe3f580 100644 --- a/crates/context/config/src/client/config.rs +++ b/crates/context/config/src/client/config.rs @@ -1,73 +1,36 @@ #![allow(clippy::exhaustive_structs, reason = "TODO: Allowed until reviewed")] -use core::str::FromStr; use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; -use thiserror::Error; use url::Url; -use super::{near, starknet}; +use crate::client::protocol::near::Credentials as NearCredentials; +use crate::client::protocol::starknet::Credentials as StarknetCredentials; #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ContextConfigClientConfig { - pub new: ContextConfigClientNew, - pub signer: ContextConfigClientSigner, -} - -#[non_exhaustive] -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum Protocol { - Near, - Starknet, -} - -impl Protocol { - #[must_use] - pub const fn as_str(&self) -> &'static str { - match self { - Self::Near => "near", - Self::Starknet => "starknet", - } - } -} - -#[derive(Debug, Error, Copy, Clone)] -#[error("Failed to parse protocol")] -pub struct ProtocolParseError { - _priv: (), -} - -impl FromStr for Protocol { - type Err = ProtocolParseError; - - fn from_str(input: &str) -> Result { - match input.to_lowercase().as_str() { - "near" => Ok(Self::Near), - "starknet" => Ok(Self::Starknet), - _ => Err(ProtocolParseError { _priv: () }), - } - } +pub struct ClientConfig { + pub new: ClientNew, + pub signer: ClientSigner, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ContextConfigClientNew { - pub protocol: Protocol, +pub struct ClientNew { + pub protocol: String, pub network: String, pub contract_id: String, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct LocalConfig { - pub near: BTreeMap, - pub starknet: BTreeMap, + pub near: BTreeMap, + pub starknet: BTreeMap, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ContextConfigClientSigner { +pub struct ClientSigner { #[serde(rename = "use")] - pub selected: ContextConfigClientSelectedSigner, - pub relayer: ContextConfigClientRelayerSigner, + pub selected: ClientSelectedSigner, + pub relayer: ClientRelayerSigner, #[serde(rename = "self")] pub local: LocalConfig, } @@ -75,19 +38,19 @@ pub struct ContextConfigClientSigner { #[derive(Copy, Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] #[non_exhaustive] -pub enum ContextConfigClientSelectedSigner { +pub enum ClientSelectedSigner { Relayer, #[serde(rename = "self")] Local, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ContextConfigClientRelayerSigner { +pub struct ClientRelayerSigner { pub url: Url, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ContextConfigClientLocalSigner { +pub struct ClientLocalSigner { pub rpc_url: Url, #[serde(flatten)] pub credentials: Credentials, @@ -97,6 +60,6 @@ pub struct ContextConfigClientLocalSigner { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum Credentials { - Near(near::Credentials), - Starknet(starknet::Credentials), + Near(NearCredentials), + Starknet(StarknetCredentials), } diff --git a/crates/context/config/src/client/env.rs b/crates/context/config/src/client/env.rs new file mode 100644 index 000000000..9c61da3d3 --- /dev/null +++ b/crates/context/config/src/client/env.rs @@ -0,0 +1,42 @@ +use super::protocol::Protocol; + +pub mod config; +// pub mod proxy; + +pub trait Method { + type Returns; + + const METHOD: &'static str; + + fn encode(self) -> eyre::Result>; + fn decode(response: Vec) -> eyre::Result; +} + +mod utils { + #![expect(clippy::type_repetition_in_bounds, reason = "Useful for clarity")] + + use super::Method; + use crate::client::protocol::near::Near; + use crate::client::protocol::starknet::Starknet; + use crate::client::protocol::Protocol; + use crate::client::transport::Transport; + use crate::client::{CallClient, ClientError, Operation}; + + // todo! when crates are broken up, appropriately locate this + pub(super) async fn send_near_or_starknet( + client: &CallClient<'_, T>, + params: Operation, + ) -> Result> + where + M: Method, + M: Method, + { + match &*client.protocol { + Near::PROTOCOL => client.send::(params).await, + Starknet::PROTOCOL => client.send::(params).await, + unsupported_protocol => Err(ClientError::UnsupportedProtocol( + unsupported_protocol.to_owned(), + )), + } + } +} diff --git a/crates/context/config/src/client/env/config.rs b/crates/context/config/src/client/env/config.rs new file mode 100644 index 000000000..04c95f9e6 --- /dev/null +++ b/crates/context/config/src/client/env/config.rs @@ -0,0 +1,23 @@ +use crate::client::{CallClient, Environment}; + +mod mutate; +mod query; + +use mutate::ContextConfigMutate; +use query::ContextConfigQuery; + +#[derive(Copy, Clone, Debug)] +pub enum ContextConfig {} + +impl<'a, T: 'a> Environment<'a, T> for ContextConfig { + type Query = ContextConfigQuery<'a, T>; + type Mutate = ContextConfigMutate<'a, T>; + + fn query(client: CallClient<'a, T>) -> Self::Query { + ContextConfigQuery { client } + } + + fn mutate(client: CallClient<'a, T>) -> Self::Mutate { + ContextConfigMutate { client } + } +} diff --git a/crates/context/config/src/client/env/config/mutate.rs b/crates/context/config/src/client/env/config/mutate.rs new file mode 100644 index 000000000..f45f9ba6e --- /dev/null +++ b/crates/context/config/src/client/env/config/mutate.rs @@ -0,0 +1,85 @@ +use ed25519_dalek::{Signer, SigningKey}; + +use crate::client::env::{utils, Method}; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::client::transport::Transport; +use crate::client::{CallClient, ClientError, Operation}; +use crate::repr::ReprTransmute; +use crate::types::Signed; +use crate::{Request, RequestKind}; + +pub mod methods; + +#[derive(Debug)] +pub struct ContextConfigMutate<'a, T> { + pub client: CallClient<'a, T>, +} + +#[derive(Debug)] +pub struct ContextConfigMutateRequest<'a, T> { + client: CallClient<'a, T>, + kind: RequestKind<'a>, +} + +#[derive(Debug)] +struct Mutate<'a> { + pub(crate) signing_key: [u8; 32], + pub(crate) kind: RequestKind<'a>, +} + +impl<'a> Method for Mutate<'a> { + const METHOD: &'static str = "mutate"; + + type Returns = (); + + fn encode(self) -> eyre::Result> { + let signer_sk = SigningKey::from_bytes(&self.signing_key); + + let request = Request::new(signer_sk.verifying_key().rt()?, self.kind); + + let signed = Signed::new(&request, |b| signer_sk.sign(b))?; + + let encoded = serde_json::to_vec(&signed)?; + + Ok(encoded) + } + + fn decode(response: Vec) -> eyre::Result { + serde_json::from_slice(&response).map_err(Into::into) + } +} + +impl<'a> Method for Mutate<'a> { + type Returns = (); + + const METHOD: &'static str = "mutate"; + + fn encode(self) -> eyre::Result> { + // sign the params, encode it and return + // since you will have a `Vec` here, you can + // `Vec::with_capacity(32 * calldata.len())` and then + // extend the `Vec` with each `Felt::to_bytes_le()` + // when this `Vec` makes it to `StarknetTransport`, + // reconstruct the `Vec` from it + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} + +impl<'a, T: Transport> ContextConfigMutateRequest<'a, T> { + pub async fn send(self, signing_key: [u8; 32]) -> Result<(), ClientError> { + let request = Mutate { + signing_key, + // todo! when nonces are implemented in context + // todo! config contract, we fetch it here first + // nonce: _, + kind: self.kind, + }; + + utils::send_near_or_starknet(&self.client, Operation::Write(request)).await + } +} diff --git a/crates/context/config/src/client/env/config/mutate/methods.rs b/crates/context/config/src/client/env/config/mutate/methods.rs new file mode 100644 index 000000000..836339084 --- /dev/null +++ b/crates/context/config/src/client/env/config/mutate/methods.rs @@ -0,0 +1,122 @@ +use core::ptr; + +use super::{ContextConfigMutate, ContextConfigMutateRequest}; +use crate::repr::Repr; +use crate::types::{Application, Capability, ContextId, ContextIdentity}; +use crate::{ContextRequest, ContextRequestKind, RequestKind}; + +impl<'a, T> ContextConfigMutate<'a, T> { + pub fn add_context( + self, + context_id: ContextId, + author_id: ContextIdentity, + application: Application<'a>, + ) -> ContextConfigMutateRequest<'a, T> { + ContextConfigMutateRequest { + client: self.client, + kind: RequestKind::Context(ContextRequest { + context_id: Repr::new(context_id), + kind: ContextRequestKind::Add { + author_id: Repr::new(author_id), + application, + }, + }), + } + } + + pub fn update_application( + self, + context_id: ContextId, + application: Application<'a>, + ) -> ContextConfigMutateRequest<'a, T> { + ContextConfigMutateRequest { + client: self.client, + kind: RequestKind::Context(ContextRequest { + context_id: Repr::new(context_id), + kind: ContextRequestKind::UpdateApplication { application }, + }), + } + } + + pub fn add_members( + self, + context_id: ContextId, + members: &[ContextIdentity], + ) -> ContextConfigMutateRequest<'a, T> { + let members = unsafe { + &*(ptr::from_ref::<[ContextIdentity]>(members) as *const [Repr]) + }; + + ContextConfigMutateRequest { + client: self.client, + kind: RequestKind::Context(ContextRequest { + context_id: Repr::new(context_id), + kind: ContextRequestKind::AddMembers { + members: members.into(), + }, + }), + } + } + + pub fn remove_members( + self, + context_id: ContextId, + members: &[ContextIdentity], + ) -> ContextConfigMutateRequest<'a, T> { + let members = unsafe { + &*(ptr::from_ref::<[ContextIdentity]>(members) as *const [Repr]) + }; + + ContextConfigMutateRequest { + client: self.client, + kind: RequestKind::Context(ContextRequest { + context_id: Repr::new(context_id), + kind: ContextRequestKind::RemoveMembers { + members: members.into(), + }, + }), + } + } + + pub fn grant( + self, + context_id: ContextId, + capabilities: &[(ContextIdentity, Capability)], + ) -> ContextConfigMutateRequest<'a, T> { + let capabilities = unsafe { + &*(ptr::from_ref::<[(ContextIdentity, Capability)]>(capabilities) + as *const [(Repr, Capability)]) + }; + + ContextConfigMutateRequest { + client: self.client, + kind: RequestKind::Context(ContextRequest { + context_id: Repr::new(context_id), + kind: ContextRequestKind::Grant { + capabilities: capabilities.into(), + }, + }), + } + } + + pub fn revoke( + self, + context_id: ContextId, + capabilities: &[(ContextIdentity, Capability)], + ) -> ContextConfigMutateRequest<'a, T> { + let capabilities = unsafe { + &*(ptr::from_ref::<[(ContextIdentity, Capability)]>(capabilities) + as *const [(Repr, Capability)]) + }; + + ContextConfigMutateRequest { + client: self.client, + kind: RequestKind::Context(ContextRequest { + context_id: Repr::new(context_id), + kind: ContextRequestKind::Revoke { + capabilities: capabilities.into(), + }, + }), + } + } +} diff --git a/crates/context/config/src/client/env/config/query.rs b/crates/context/config/src/client/env/config/query.rs new file mode 100644 index 000000000..fd216d3cc --- /dev/null +++ b/crates/context/config/src/client/env/config/query.rs @@ -0,0 +1,92 @@ +use std::collections::BTreeMap; + +use crate::client::env::utils; +use crate::client::transport::Transport; +use crate::client::{CallClient, ClientError, Operation}; +use crate::repr::Repr; +use crate::types::{Application, Capability, ContextId, ContextIdentity, Revision, SignerId}; + +pub mod application; +pub mod application_revision; +pub mod has_member; +pub mod members; +pub mod members_revision; +pub mod privileges; + +#[derive(Debug)] +pub struct ContextConfigQuery<'a, T> { + pub client: CallClient<'a, T>, +} + +impl<'a, T: Transport> ContextConfigQuery<'a, T> { + pub async fn application( + &self, + context_id: ContextId, + ) -> Result, ClientError> { + let params = application::ApplicationRequest { + context_id: Repr::new(context_id), + }; + + utils::send_near_or_starknet(&self.client, Operation::Read(params)).await + } + + pub async fn application_revision( + &self, + context_id: ContextId, + ) -> Result> { + let params = application_revision::ApplicationRevisionRequest { + context_id: Repr::new(context_id), + }; + + utils::send_near_or_starknet(&self.client, Operation::Read(params)).await + } + + pub async fn members( + &self, + context_id: ContextId, + offset: usize, + length: usize, + ) -> Result, ClientError> { + let params = members::MembersRequest { + context_id: Repr::new(context_id), + offset, + length, + }; + + utils::send_near_or_starknet(&self.client, Operation::Read(params)).await + } + + pub async fn has_member( + &self, + context_id: ContextId, + identity: ContextIdentity, + ) -> Result> { + let params = has_member::HasMemberRequest { + context_id: Repr::new(context_id), + identity: Repr::new(identity), + }; + + utils::send_near_or_starknet(&self.client, Operation::Read(params)).await + } + + pub async fn members_revision( + &self, + context_id: ContextId, + ) -> Result> { + let params = members_revision::MembersRevisionRequest { + context_id: Repr::new(context_id), + }; + + utils::send_near_or_starknet(&self.client, Operation::Read(params)).await + } + + pub async fn privileges( + &self, + context_id: ContextId, + identities: &[ContextIdentity], + ) -> Result>, ClientError> { + let params = privileges::PrivilegesRequest::new(context_id, identities); + + utils::send_near_or_starknet(&self.client, Operation::Read(params)).await + } +} diff --git a/crates/context/config/src/client/env/config/query/application.rs b/crates/context/config/src/client/env/config/query/application.rs new file mode 100644 index 000000000..51873f281 --- /dev/null +++ b/crates/context/config/src/client/env/config/query/application.rs @@ -0,0 +1,50 @@ +use serde::Serialize; + +use crate::client::env::Method; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::repr::Repr; +use crate::types::{Application, ApplicationMetadata, ApplicationSource, ContextId}; + +#[derive(Copy, Clone, Debug, Serialize)] +pub(super) struct ApplicationRequest { + pub(super) context_id: Repr, +} + +impl Method for ApplicationRequest { + const METHOD: &'static str = "application"; + + type Returns = Application<'static>; + + fn encode(self) -> eyre::Result> { + serde_json::to_vec(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let application: Application<'_> = serde_json::from_slice(&response)?; + + Ok(Application::new( + application.id, + application.blob, + application.size, + ApplicationSource(application.source.0.into_owned().into()), + ApplicationMetadata(Repr::new( + application.metadata.0.into_inner().into_owned().into(), + )), + )) + } +} + +impl Method for ApplicationRequest { + type Returns = Application<'static>; + + const METHOD: &'static str = "application"; + + fn encode(self) -> eyre::Result> { + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} diff --git a/crates/context/config/src/client/env/config/query/application_revision.rs b/crates/context/config/src/client/env/config/query/application_revision.rs new file mode 100644 index 000000000..2312e0824 --- /dev/null +++ b/crates/context/config/src/client/env/config/query/application_revision.rs @@ -0,0 +1,40 @@ +use serde::Serialize; + +use crate::client::env::Method; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::repr::Repr; +use crate::types::{ContextId, Revision}; + +#[derive(Copy, Clone, Debug, Serialize)] +pub(super) struct ApplicationRevisionRequest { + pub(super) context_id: Repr, +} + +impl Method for ApplicationRevisionRequest { + const METHOD: &'static str = "application_revision"; + + type Returns = Revision; + + fn encode(self) -> eyre::Result> { + serde_json::to_vec(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + serde_json::from_slice(&response).map_err(Into::into) + } +} + +impl Method for ApplicationRevisionRequest { + type Returns = Revision; + + const METHOD: &'static str = "application_revision"; + + fn encode(self) -> eyre::Result> { + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} diff --git a/crates/context/config/src/client/env/config/query/has_member.rs b/crates/context/config/src/client/env/config/query/has_member.rs new file mode 100644 index 000000000..2e9c91bad --- /dev/null +++ b/crates/context/config/src/client/env/config/query/has_member.rs @@ -0,0 +1,41 @@ +use serde::Serialize; + +use crate::client::env::Method; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::repr::Repr; +use crate::types::{ContextId, ContextIdentity}; + +#[derive(Copy, Clone, Debug, Serialize)] +pub(super) struct HasMemberRequest { + pub(super) context_id: Repr, + pub(super) identity: Repr, +} + +impl Method for HasMemberRequest { + const METHOD: &'static str = "has_member"; + + type Returns = bool; + + fn encode(self) -> eyre::Result> { + serde_json::to_vec(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + serde_json::from_slice(&response).map_err(Into::into) + } +} + +impl Method for HasMemberRequest { + type Returns = bool; + + const METHOD: &'static str = "has_member"; + + fn encode(self) -> eyre::Result> { + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} diff --git a/crates/context/config/src/client/env/config/query/members.rs b/crates/context/config/src/client/env/config/query/members.rs new file mode 100644 index 000000000..6d18930f2 --- /dev/null +++ b/crates/context/config/src/client/env/config/query/members.rs @@ -0,0 +1,54 @@ +use core::mem; + +use serde::Serialize; + +use crate::client::env::Method; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::repr::Repr; +use crate::types::{ContextId, ContextIdentity}; + +#[derive(Copy, Clone, Debug, Serialize)] +pub(super) struct MembersRequest { + pub(super) context_id: Repr, + pub(super) offset: usize, + pub(super) length: usize, +} + +impl Method for MembersRequest { + const METHOD: &'static str = "members"; + + type Returns = Vec; + + fn encode(self) -> eyre::Result> { + serde_json::to_vec(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let members: Vec> = serde_json::from_slice(&response)?; + + // safety: `Repr` is a transparent wrapper around `T` + #[expect( + clippy::transmute_undefined_repr, + reason = "Repr is a transparent wrapper around T" + )] + let members = + unsafe { mem::transmute::>, Vec>(members) }; + + Ok(members) + } +} + +impl Method for MembersRequest { + type Returns = Vec; + + const METHOD: &'static str = "members"; + + fn encode(self) -> eyre::Result> { + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} diff --git a/crates/context/config/src/client/env/config/query/members_revision.rs b/crates/context/config/src/client/env/config/query/members_revision.rs new file mode 100644 index 000000000..df4191ad8 --- /dev/null +++ b/crates/context/config/src/client/env/config/query/members_revision.rs @@ -0,0 +1,40 @@ +use serde::Serialize; + +use crate::client::env::Method; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::repr::Repr; +use crate::types::{ContextId, Revision}; + +#[derive(Copy, Clone, Debug, Serialize)] +pub(super) struct MembersRevisionRequest { + pub(super) context_id: Repr, +} + +impl Method for MembersRevisionRequest { + const METHOD: &'static str = "members_revision"; + + type Returns = Revision; + + fn encode(self) -> eyre::Result> { + serde_json::to_vec(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + serde_json::from_slice(&response).map_err(Into::into) + } +} + +impl Method for MembersRevisionRequest { + type Returns = Revision; + + const METHOD: &'static str = "members_revision"; + + fn encode(self) -> eyre::Result> { + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} diff --git a/crates/context/config/src/client/env/config/query/privileges.rs b/crates/context/config/src/client/env/config/query/privileges.rs new file mode 100644 index 000000000..61e7b923b --- /dev/null +++ b/crates/context/config/src/client/env/config/query/privileges.rs @@ -0,0 +1,72 @@ +use core::{mem, ptr}; +use std::collections::BTreeMap; + +use serde::Serialize; + +use crate::client::env::Method; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::repr::Repr; +use crate::types::{Capability, ContextId, ContextIdentity, SignerId}; + +#[derive(Copy, Clone, Debug, Serialize)] +pub(super) struct PrivilegesRequest<'a> { + pub(super) context_id: Repr, + pub(super) identities: &'a [Repr], +} + +impl<'a> PrivilegesRequest<'a> { + pub const fn new(context_id: ContextId, identities: &'a [ContextIdentity]) -> Self { + let identities = unsafe { + &*(ptr::from_ref::<[ContextIdentity]>(identities) as *const [Repr]) + }; + + Self { + context_id: Repr::new(context_id), + identities, + } + } +} + +impl<'a> Method for PrivilegesRequest<'a> { + const METHOD: &'static str = "privileges"; + + type Returns = BTreeMap>; + + fn encode(self) -> eyre::Result> { + serde_json::to_vec(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let privileges: BTreeMap, Vec> = + serde_json::from_slice(&response)?; + + // safety: `Repr` is a transparent wrapper around `T` + let privileges = unsafe { + #[expect( + clippy::transmute_undefined_repr, + reason = "Repr is a transparent wrapper around T" + )] + mem::transmute::< + BTreeMap, Vec>, + BTreeMap>, + >(privileges) + }; + + Ok(privileges) + } +} + +impl<'a> Method for PrivilegesRequest<'a> { + type Returns = BTreeMap>; + + const METHOD: &'static str = "privileges"; + + fn encode(self) -> eyre::Result> { + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} diff --git a/crates/context/config/src/client/env/proxy.rs b/crates/context/config/src/client/env/proxy.rs new file mode 100644 index 000000000..684c6f583 --- /dev/null +++ b/crates/context/config/src/client/env/proxy.rs @@ -0,0 +1,23 @@ +use crate::client::{CallClient, Environment}; + +mod mutate; +mod query; + +use mutate::ContextProxyMutate; +use query::ContextProxyQuery; + +#[derive(Copy, Clone, Debug)] +pub enum ContextProxy {} + +impl<'a, T: 'a> Environment<'a, T> for ContextProxy { + type Query = ContextProxyQuery<'a, T>; + type Mutate = ContextProxyMutate<'a, T>; + + fn query(_client: CallClient<'a, T>) -> Self::Query { + todo!() + } + + fn mutate(_client: CallClient<'a, T>) -> Self::Mutate { + todo!() + } +} diff --git a/crates/context/config/src/client/env/proxy/mutate.rs b/crates/context/config/src/client/env/proxy/mutate.rs new file mode 100644 index 000000000..3d86f6a4d --- /dev/null +++ b/crates/context/config/src/client/env/proxy/mutate.rs @@ -0,0 +1,8 @@ +use crate::client::CallClient; + +mod propose; + +#[derive(Debug)] +pub struct ContextProxyMutate<'a, T> { + client: CallClient<'a, T>, +} diff --git a/crates/context/config/src/client/env/proxy/mutate/propose.rs b/crates/context/config/src/client/env/proxy/mutate/propose.rs new file mode 100644 index 000000000..794e24d38 --- /dev/null +++ b/crates/context/config/src/client/env/proxy/mutate/propose.rs @@ -0,0 +1,73 @@ +use super::ContextProxyMutate; +use crate::client::env::{utils, Method}; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::client::transport::Transport; +use crate::client::{CallClient, Error, Operation}; + +// todo! this should be replaced with primitives lib +#[derive(Debug)] +pub enum Proposal { + Transfer { recipient: String, amount: u64 }, + // __ +} + +impl<'a, T> ContextProxyMutate<'a, T> { + pub fn propose(self, proposal: Proposal) -> ContextProxyProposeRequest<'a, T> { + ContextProxyProposeRequest { + client: self.client, + proposal, + } + } +} + +#[derive(Debug)] +pub struct ContextProxyProposeRequest<'a, T> { + client: CallClient<'a, T>, + proposal: Proposal, +} + +struct Propose { + signing_key: [u8; 32], + proposal: Proposal, +} + +impl Method for Propose { + const METHOD: &'static str = "propose"; + + type Returns = (); + + fn encode(self) -> eyre::Result> { + // sign the params, encode it and return + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} + +impl Method for Propose { + type Returns = (); + + const METHOD: &'static str = "propose"; + + fn encode(self) -> eyre::Result> { + todo!() + } + + fn decode(_response: Vec) -> eyre::Result { + todo!() + } +} + +impl<'a, T: Transport> ContextProxyProposeRequest<'a, T> { + pub async fn send(self, signing_key: [u8; 32]) -> Result<(), Error> { + let request = Propose { + signing_key, + proposal: self.proposal, + }; + + utils::send_near_or_starknet(&self.client, Operation::Write(request)).await + } +} diff --git a/crates/context/config/src/client/env/proxy/query.rs b/crates/context/config/src/client/env/proxy/query.rs new file mode 100644 index 000000000..090c064be --- /dev/null +++ b/crates/context/config/src/client/env/proxy/query.rs @@ -0,0 +1,19 @@ +use crate::client::transport::Transport; +use crate::client::{CallClient, Error}; + +#[derive(Debug)] +pub struct ContextProxyQuery<'a, T> { + client: CallClient<'a, T>, +} + +type ProposalId = [u8; 32]; + +impl<'a, T: Transport> ContextProxyQuery<'a, T> { + pub async fn proposals( + &self, + _offset: usize, + _length: usize, + ) -> Result, Error> { + todo!() + } +} diff --git a/crates/context/config/src/client/protocol.rs b/crates/context/config/src/client/protocol.rs new file mode 100644 index 000000000..ceb4b50f5 --- /dev/null +++ b/crates/context/config/src/client/protocol.rs @@ -0,0 +1,6 @@ +pub mod near; +pub mod starknet; + +pub trait Protocol { + const PROTOCOL: &'static str; +} diff --git a/crates/context/config/src/client/near.rs b/crates/context/config/src/client/protocol/near.rs similarity index 97% rename from crates/context/config/src/client/near.rs rename to crates/context/config/src/client/protocol/near.rs index 063a970f5..2e480df66 100644 --- a/crates/context/config/src/client/near.rs +++ b/crates/context/config/src/client/protocol/near.rs @@ -1,5 +1,3 @@ -#![allow(clippy::exhaustive_structs, reason = "TODO: Allowed until reviewed")] - use std::borrow::Cow; use std::collections::BTreeMap; use std::time; @@ -26,7 +24,19 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; -use super::{Operation, Transport, TransportRequest}; +use super::Protocol; +use crate::client::transport::{AssociatedTransport, Operation, Transport, TransportRequest}; + +#[derive(Copy, Clone, Debug)] +pub enum Near {} + +impl Protocol for Near { + const PROTOCOL: &'static str = "near"; +} + +impl AssociatedTransport for NearTransport<'_> { + type Protocol = Near; +} #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(try_from = "serde_creds::Credentials")] diff --git a/crates/context/config/src/client/starknet.rs b/crates/context/config/src/client/protocol/starknet.rs similarity index 96% rename from crates/context/config/src/client/starknet.rs rename to crates/context/config/src/client/protocol/starknet.rs index f29746f1e..fb7f31ac3 100644 --- a/crates/context/config/src/client/starknet.rs +++ b/crates/context/config/src/client/protocol/starknet.rs @@ -1,5 +1,3 @@ -#![allow(clippy::exhaustive_structs, reason = "TODO: Allowed until reviewed")] - use core::str::FromStr; use std::borrow::Cow; use std::collections::BTreeMap; @@ -14,7 +12,19 @@ use starknet::providers::{JsonRpcClient, Provider, Url}; use starknet::signers::{LocalWallet, SigningKey}; use thiserror::Error; -use super::{Operation, Transport, TransportRequest}; +use super::Protocol; +use crate::client::transport::{AssociatedTransport, Operation, Transport, TransportRequest}; + +#[derive(Copy, Clone, Debug)] +pub enum Starknet {} + +impl Protocol for Starknet { + const PROTOCOL: &'static str = "starknet"; +} + +impl AssociatedTransport for StarknetTransport<'_> { + type Protocol = Starknet; +} #[derive(Copy, Clone, Debug, Deserialize, Serialize)] #[serde(try_from = "serde_creds::Credentials")] diff --git a/crates/context/config/src/client/relayer.rs b/crates/context/config/src/client/relayer.rs index 91f3431dc..33ae0600c 100644 --- a/crates/context/config/src/client/relayer.rs +++ b/crates/context/config/src/client/relayer.rs @@ -3,8 +3,7 @@ use std::borrow::Cow; use serde::{Deserialize, Serialize}; use url::Url; -use super::config::Protocol; -use super::{Operation, Transport, TransportRequest}; +use super::transport::{Operation, Transport, TransportRequest}; #[derive(Debug)] #[non_exhaustive] @@ -33,7 +32,7 @@ impl RelayerTransport { #[derive(Debug, Serialize, Deserialize)] #[non_exhaustive] pub struct RelayRequest<'a> { - pub protocol: Protocol, + pub protocol: Cow<'a, str>, pub network_id: Cow<'a, str>, pub contract_id: Cow<'a, str>, pub operation: Operation<'a>, diff --git a/crates/context/config/src/client/transport.rs b/crates/context/config/src/client/transport.rs new file mode 100644 index 000000000..af7dbfb38 --- /dev/null +++ b/crates/context/config/src/client/transport.rs @@ -0,0 +1,124 @@ +use core::error::Error; +use std::borrow::Cow; + +use either::Either; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use super::protocol::Protocol; + +pub trait Transport { + type Error: Error; + + #[expect(async_fn_in_trait, reason = "Should be fine")] + async fn send( + &self, + request: TransportRequest<'_>, + payload: Vec, + ) -> Result, Self::Error>; +} + +impl Transport for Either { + type Error = Either; + + async fn send( + &self, + request: TransportRequest<'_>, + payload: Vec, + ) -> Result, Self::Error> { + match self { + Self::Left(left) => left.send(request, payload).await.map_err(Either::Left), + Self::Right(right) => right.send(request, payload).await.map_err(Either::Right), + } + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub struct TransportRequest<'a> { + pub protocol: Cow<'a, str>, + pub network_id: Cow<'a, str>, + pub contract_id: Cow<'a, str>, + pub operation: Operation<'a>, +} + +impl<'a> TransportRequest<'a> { + #[must_use] + pub const fn new( + protocol: Cow<'a, str>, + network_id: Cow<'a, str>, + contract_id: Cow<'a, str>, + operation: Operation<'a>, + ) -> Self { + Self { + protocol, + network_id, + contract_id, + operation, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[expect(clippy::exhaustive_enums, reason = "Considered to be exhaustive")] +pub enum Operation<'a> { + Read { method: Cow<'a, str> }, + Write { method: Cow<'a, str> }, +} + +pub trait AssociatedTransport: Transport { + type Protocol: Protocol; + + #[inline] + #[must_use] + fn protocol() -> &'static str { + Self::Protocol::PROTOCOL + } +} + +#[expect(clippy::exhaustive_structs, reason = "this is exhaustive")] +#[derive(Debug, Clone)] +pub struct Both { + pub left: L, + pub right: R, +} + +#[derive(Debug, Error)] +pub enum BothError { + #[error("left error: {0}")] + Left(L), + #[error("right error: {0}")] + Right(R), + #[error("unsupported protocol: {0}")] + UnsupportedProtocol(String), +} + +impl Transport for Both +where + L: AssociatedTransport, + R: AssociatedTransport, +{ + type Error = BothError; + + async fn send( + &self, + request: TransportRequest<'_>, + payload: Vec, + ) -> Result, Self::Error> { + if request.protocol == L::protocol() { + self.left + .send(request, payload) + .await + .map_err(BothError::Left) + } else if request.protocol == R::protocol() { + self.right + .send(request, payload) + .await + .map_err(BothError::Right) + } else { + return Err(BothError::UnsupportedProtocol( + request.protocol.into_owned(), + )); + } + } +} diff --git a/crates/context/src/config.rs b/crates/context/src/config.rs index e80231db3..2290f5bbf 100644 --- a/crates/context/src/config.rs +++ b/crates/context/src/config.rs @@ -1,10 +1,10 @@ #![allow(clippy::exhaustive_structs, reason = "TODO: Allowed until reviewed")] -use calimero_context_config::client::config::ContextConfigClientConfig; +use calimero_context_config::client::config::ClientConfig; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ContextConfig { #[serde(rename = "config")] - pub client: ContextConfigClientConfig, + pub client: ClientConfig, } diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index 71d55b179..ba43746f4 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -7,8 +7,9 @@ use std::io::Error as IoError; use std::sync::Arc; use calimero_blobstore::{Blob, BlobManager, Size}; -use calimero_context_config::client::config::ContextConfigClientConfig; -use calimero_context_config::client::{AnyTransport, ContextConfigClient}; +use calimero_context_config::client::config::ClientConfig; +use calimero_context_config::client::env::config::ContextConfig as ContextConfigEnv; +use calimero_context_config::client::{AnyTransport, Client as ExternalClient}; use calimero_context_config::repr::{Repr, ReprBytes, ReprTransmute}; use calimero_context_config::types::{ Application as ApplicationConfig, ApplicationMetadata as ApplicationMetadataConfig, @@ -34,13 +35,11 @@ use calimero_store::types::{ }; use calimero_store::Store; use camino::Utf8PathBuf; -use ed25519_dalek::ed25519::signature::SignerMut; -use ed25519_dalek::SigningKey; use eyre::{bail, Result as EyreResult}; use futures_util::{AsyncRead, TryStreamExt}; use rand::rngs::StdRng; use rand::SeedableRng; -use reqwest::{Client, Url}; +use reqwest::{Client as ReqClient, Url}; use tokio::fs::File; use tokio::sync::{oneshot, RwLock}; use tokio_util::compat::TokioAsyncReadCompatExt; @@ -53,8 +52,8 @@ use config::ContextConfig; #[derive(Clone, Debug)] pub struct ContextManager { store: Store, - client_config: ContextConfigClientConfig, - config_client: ContextConfigClient, + client_config: ClientConfig, + config_client: ExternalClient, blob_manager: BlobManager, network_client: NetworkClient, server_sender: ServerSender, @@ -75,7 +74,7 @@ impl ContextManager { network_client: NetworkClient, ) -> EyreResult { let client_config = config.client.clone(); - let config_client = ContextConfigClient::from_config(&client_config); + let config_client = ExternalClient::from_config(&client_config); let this = Self { store, @@ -204,11 +203,10 @@ impl ContextManager { } this.config_client - .mutate( - this.client_config.new.protocol, + .mutate::( + this.client_config.new.protocol.as_str().into(), this.client_config.new.network.as_str().into(), this.client_config.new.contract_id.as_str().into(), - context.id.rt().expect("infallible conversion"), ) .add_context( context.id.rt().expect("infallible conversion"), @@ -224,7 +222,7 @@ impl ContextManager { ApplicationMetadataConfig(Repr::new(application.metadata.into())), ), ) - .send(|b| SigningKey::from_bytes(&context_secret).sign(b)) + .send(context.id.rt().expect("infallible conversion")) .await?; Ok((context.id, identity_secret.public_key())) @@ -316,9 +314,11 @@ impl ContextManager { return Ok(None); } - let client = - self.config_client - .query(protocol.parse()?, network_id.into(), contract_id.into()); + let client = self.config_client.query::( + protocol.into(), + network_id.into(), + contract_id.into(), + ); for (offset, length) in (0..).map(|i| (100_usize.saturating_mul(i), 100)) { let members = client @@ -327,8 +327,7 @@ impl ContextManager { offset, length, ) - .await? - .parse()?; + .await?; if members.is_empty() { break; @@ -349,12 +348,10 @@ impl ContextManager { bail!("unable to join context: not a member, ask for an invite") }; - let response = client + let application = client .application(context_id.rt().expect("infallible conversion")) .await?; - let application = response.parse()?; - let context = Context::new( context_id, application.id.as_bytes().into(), @@ -415,17 +412,16 @@ impl ContextManager { }; self.config_client - .mutate( - context_config.protocol.parse()?, + .mutate::( + context_config.protocol.as_ref().into(), context_config.network.as_ref().into(), context_config.contract.as_ref().into(), - inviter_id.rt().expect("infallible conversion"), ) .add_members( context_id.rt().expect("infallible conversion"), &[invitee_id.rt().expect("infallible conversion")], ) - .send(|b| SigningKey::from_bytes(&requester_secret).sign(b)) + .send(requester_secret) .await?; let invitation_payload = ContextInvitationPayload::new( @@ -773,7 +769,7 @@ impl ContextManager { ) -> EyreResult { let uri = url.as_str().parse()?; - let response = Client::new().get(url).send().await?; + let response = ReqClient::new().get(url).send().await?; let expected_size = response.content_length(); @@ -857,18 +853,16 @@ impl ContextManager { } pub async fn get_latest_application(&self, context_id: ContextId) -> EyreResult { - let client = self.config_client.query( - self.client_config.new.protocol, + let client = self.config_client.query::( + self.client_config.new.protocol.as_str().into(), self.client_config.new.network.as_str().into(), self.client_config.new.contract_id.as_str().into(), ); - let response = client + let application = client .application(context_id.rt().expect("infallible conversion")) .await?; - let application = response.parse()?; - Ok(Application::new( application.id.as_bytes().into(), application.blob.as_bytes().into(), diff --git a/crates/merod/src/cli/config.rs b/crates/merod/src/cli/config.rs index 26d55cab0..303c987b6 100644 --- a/crates/merod/src/cli/config.rs +++ b/crates/merod/src/cli/config.rs @@ -3,7 +3,6 @@ use core::net::IpAddr; use std::fs::{read_to_string, write}; -use calimero_context_config::client::config::Protocol as CoreProtocol; use calimero_network::config::BootstrapNodes; use clap::{Args, Parser, ValueEnum}; use eyre::{eyre, Result as EyreResult}; @@ -14,18 +13,16 @@ use tracing::info; use crate::cli; #[derive(Copy, Clone, Debug, ValueEnum)] -#[value(rename_all = "lowercase")] pub enum ConfigProtocol { Near, Starknet, } -// Implement conversion from CLI Protocol to Core Protocol -impl From for CoreProtocol { - fn from(protocol: ConfigProtocol) -> Self { - match protocol { - ConfigProtocol::Near => Self::Near, - ConfigProtocol::Starknet => Self::Starknet, +impl ConfigProtocol { + pub fn as_str(&self) -> &str { + match self { + ConfigProtocol::Near => "near", + ConfigProtocol::Starknet => "starknet", } } } diff --git a/crates/merod/src/cli/init.rs b/crates/merod/src/cli/init.rs index 76c29c400..255066d10 100644 --- a/crates/merod/src/cli/init.rs +++ b/crates/merod/src/cli/init.rs @@ -7,11 +7,12 @@ use calimero_config::{ }; use calimero_context::config::ContextConfig; use calimero_context_config::client::config::{ - ContextConfigClientConfig, ContextConfigClientLocalSigner, ContextConfigClientNew, - ContextConfigClientRelayerSigner, ContextConfigClientSelectedSigner, ContextConfigClientSigner, - Credentials, LocalConfig, + ClientConfig, ClientLocalSigner, ClientNew, ClientRelayerSigner, ClientSelectedSigner, + ClientSigner, Credentials, LocalConfig, +}; +use calimero_context_config::client::protocol::{ + near as near_protocol, starknet as starknet_protocol, }; -use calimero_context_config::client::{near, starknet as starknetCredentials}; use calimero_network::config::{ BootstrapConfig, BootstrapNodes, CatchupConfig, DiscoveryConfig, RelayConfig, RendezvousConfig, SwarmConfig, @@ -177,10 +178,10 @@ impl InitCommand { StoreConfigFile::new("data".into()), BlobStoreConfig::new("blobs".into()), ContextConfig { - client: ContextConfigClientConfig { - signer: ContextConfigClientSigner { - selected: ContextConfigClientSelectedSigner::Relayer, - relayer: ContextConfigClientRelayerSigner { url: relayer }, + client: ClientConfig { + signer: ClientSigner { + selected: ClientSelectedSigner::Relayer, + relayer: ClientRelayerSigner { url: relayer }, local: LocalConfig { near: [ ( @@ -221,12 +222,12 @@ impl InitCommand { .collect(), }, }, - new: ContextConfigClientNew { + new: ClientNew { network: match self.protocol { ConfigProtocol::Near => "testnet".into(), ConfigProtocol::Starknet => "sepolia".into(), }, - protocol: self.protocol.into(), + protocol: self.protocol.as_str().to_owned(), contract_id: match self.protocol { ConfigProtocol::Near => "calimero-context-config.testnet".parse()?, ConfigProtocol::Starknet => { @@ -278,16 +279,16 @@ impl InitCommand { fn generate_local_signer( rpc_url: Url, config_protocol: ConfigProtocol, -) -> EyreResult { +) -> EyreResult { match config_protocol { ConfigProtocol::Near => { let secret_key = SecretKey::from_random(KeyType::ED25519); let public_key = secret_key.public_key(); let account_id = public_key.unwrap_as_ed25519().0; - Ok(ContextConfigClientLocalSigner { + Ok(ClientLocalSigner { rpc_url, - credentials: Credentials::Near(near::Credentials { + credentials: Credentials::Near(near_protocol::Credentials { account_id: hex::encode(account_id).parse()?, public_key, secret_key, @@ -299,9 +300,9 @@ fn generate_local_signer( let secret_key = SigningKey::secret_scalar(&keypair); let public_key = keypair.verifying_key().scalar(); - Ok(ContextConfigClientLocalSigner { + Ok(ClientLocalSigner { rpc_url, - credentials: Credentials::Starknet(starknetCredentials::Credentials { + credentials: Credentials::Starknet(starknet_protocol::Credentials { account_id: public_key, public_key, secret_key, diff --git a/crates/merod/src/cli/relay.rs b/crates/merod/src/cli/relay.rs index f43b8e5fa..f52fa8805 100644 --- a/crates/merod/src/cli/relay.rs +++ b/crates/merod/src/cli/relay.rs @@ -9,8 +9,9 @@ use axum::routing::post; use axum::{Json, Router}; use calimero_config::ConfigFile; use calimero_context_config::client::config::Credentials; +use calimero_context_config::client::protocol::{near, starknet}; use calimero_context_config::client::relayer::RelayRequest; -use calimero_context_config::client::{near, starknet, BothTransport, Transport, TransportRequest}; +use calimero_context_config::client::transport::{Both, Transport, TransportRequest}; use clap::{Parser, ValueEnum}; use eyre::{bail, Result as EyreResult}; use futures_util::FutureExt; @@ -111,9 +112,9 @@ impl RelayCommand { .collect::>()?, }); - let both_transport = BothTransport { - near: near_transport, - starknet: starknet_transport, + let both_transport = Both { + left: near_transport, + right: starknet_transport, }; let handle = async move { diff --git a/crates/network/src/discovery.rs b/crates/network/src/discovery.rs index a01668dbe..2ab669277 100644 --- a/crates/network/src/discovery.rs +++ b/crates/network/src/discovery.rs @@ -32,8 +32,6 @@ impl Discovery { impl EventLoop { // Sends rendezvous discovery requests to all rendezvous peers which are not throttled. // If rendezvous peer is not connected, it will be dialed which will trigger the discovery during identify exchange. - // TODO: Consider splitting this function up to reduce complexity. - #[expect(clippy::cognitive_complexity, reason = "TODO: Will be refactored")] pub(crate) fn broadcast_rendezvous_discoveries(&mut self) { #[expect(clippy::needless_collect, reason = "Necessary here; false positive")] for peer_id in self @@ -102,8 +100,6 @@ impl EventLoop { // Sends rendezvous registrations request to all rendezvous peers which require registration. // If rendezvous peer is not connected, it will be dialed which will trigger the registration during identify exchange. - // TODO: Consider splitting this function up to reduce complexity. - #[expect(clippy::cognitive_complexity, reason = "TODO: Will be refactored")] pub(crate) fn broadcast_rendezvous_registrations(&mut self) { #[expect(clippy::needless_collect, reason = "Necessary here; false positive")] for peer_id in self