diff --git a/contracts/near/context-config/src/mutate.rs b/contracts/near/context-config/src/mutate.rs index 0b2c2c4d2..c1af1ac96 100644 --- a/contracts/near/context-config/src/mutate.rs +++ b/contracts/near/context-config/src/mutate.rs @@ -213,6 +213,8 @@ impl ContextConfigs { for member in members { env::log_str(&format!("Added `{member}` as a member of `{context_id}`")); + let _ = context.member_nonces.insert(*member, 0); + let _ = ctx_members.insert(*member); } } diff --git a/contracts/near/context-config/src/query.rs b/contracts/near/context-config/src/query.rs index 485782337..81a635fcb 100644 --- a/contracts/near/context-config/src/query.rs +++ b/contracts/near/context-config/src/query.rs @@ -128,10 +128,7 @@ impl ContextConfigs { member_id: Repr, context_id: Repr, ) -> Option<&u64> { - let context = self - .contexts - .get(&context_id) - .expect("context does not exist"); + let context = self.contexts.get(&context_id)?; context.member_nonces.get(&member_id) } } diff --git a/contracts/near/context-config/tests/sandbox.rs b/contracts/near/context-config/tests/sandbox.rs index b022aef80..137a8cfe8 100644 --- a/contracts/near/context-config/tests/sandbox.rs +++ b/contracts/near/context-config/tests/sandbox.rs @@ -264,7 +264,7 @@ async fn main() -> eyre::Result<()> { assert_eq!(res, [alice_cx_id]); let mut nonces: HashMap = - Member::all().iter().map(|&member| (member, 1)).collect(); + Member::all().iter().map(|&member| (member, 0)).collect(); let res = node1 .call(contract.id(), "mutate") @@ -294,12 +294,13 @@ async fn main() -> eyre::Result<()> { bob_cx_id, context_id ),] ); + assert_eq!( fetch_nonce(&contract, context_id, alice_cx_id) .await? .unwrap(), 1, - "sssss" + "Alice: Nonce should be incremented to 1 after request is sent" ); *nonces.entry(Member::Alice).or_insert(0) += 1; @@ -381,6 +382,14 @@ async fn main() -> eyre::Result<()> { ); } + assert_eq!( + fetch_nonce(&contract, context_id, bob_cx_id) + .await? + .unwrap(), + 0, + "Nonce should be 0 after request is reverted" + ); + let res = contract .view("application_revision") .args_json(json!({ "context_id": context_id })) @@ -428,6 +437,15 @@ async fn main() -> eyre::Result<()> { )] ); + assert_eq!( + fetch_nonce(&contract, context_id, alice_cx_id) + .await? + .unwrap(), + 2, + "Alice: Nonce should be incremented to 2 after request is sent" + ); + *nonces.entry(Member::Alice).or_insert(0) += 1; + let res = contract .view("application_revision") .args_json(json!({ "context_id": context_id })) @@ -475,6 +493,15 @@ async fn main() -> eyre::Result<()> { ),] ); + assert_eq!( + fetch_nonce(&contract, context_id, bob_cx_id) + .await? + .unwrap(), + 1, + "Bob: Nonce should be incremented to 1 after request is sent" + ); + *nonces.entry(Member::Bob).or_insert(0) += 1; + let res: Vec> = contract .view("members") .args_json(json!({ @@ -573,6 +600,14 @@ async fn main() -> eyre::Result<()> { ); } + assert_eq!( + fetch_nonce(&contract, context_id, bob_cx_id) + .await? + .unwrap(), + 1, + "Bob: Nonce should be 1 after request is reverted" + ); + let res = contract .view("application") .args_json(json!({ "context_id": context_id })) @@ -638,6 +673,15 @@ async fn main() -> eyre::Result<()> { )] ); + assert_eq!( + fetch_nonce(&contract, context_id, alice_cx_id) + .await? + .unwrap(), + 3, + "Alice: Nonce should be incremented to 3 after request is sent" + ); + *nonces.entry(Member::Alice).or_insert(0) += 1; + let res = contract .view("application") .args_json(json!({ "context_id": context_id })) @@ -697,6 +741,15 @@ async fn main() -> eyre::Result<()> { )] ); + assert_eq!( + fetch_nonce(&contract, context_id, alice_cx_id) + .await? + .unwrap(), + 4, + "Alice: Nonce should be incremented to 4 after request is sent" + ); + *nonces.entry(Member::Alice).or_insert(0) += 1; + let res: BTreeMap, Vec> = contract .view("privileges") .args_json(json!({ @@ -776,6 +829,14 @@ async fn main() -> eyre::Result<()> { |p| alice_cx_sk.sign(p), )?); + assert_eq!( + fetch_nonce(&contract, context_id, alice_cx_id) + .await? + .unwrap(), + 4, + "Alice: Nonce should be 4 after request reverted - expired" + ); + time::sleep(time::Duration::from_secs(5)).await; let res = req diff --git a/crates/context/config/src/client/env/config/mutate.rs b/crates/context/config/src/client/env/config/mutate.rs index 51e68c6a9..9d6efc823 100644 --- a/crates/context/config/src/client/env/config/mutate.rs +++ b/crates/context/config/src/client/env/config/mutate.rs @@ -159,12 +159,10 @@ impl<'a> Method for Mutate<'a> { } impl<'a, T: Transport> ContextConfigMutateRequest<'a, T> { - pub async fn send(self, signing_key: [u8; 32]) -> Result<(), ClientError> { + pub async fn send(self, signing_key: [u8; 32], nonce: u64) -> Result<(), ClientError> { let request = Mutate { signing_key, - // todo! when nonces are implemented in context - // todo! config contract, we fetch it here first - nonce: 0, + nonce, kind: self.kind, }; diff --git a/crates/context/config/src/client/env/config/query.rs b/crates/context/config/src/client/env/config/query.rs index dc9d39f28..5bb54a6b1 100644 --- a/crates/context/config/src/client/env/config/query.rs +++ b/crates/context/config/src/client/env/config/query.rs @@ -8,6 +8,7 @@ use crate::types::{Application, Capability, ContextId, ContextIdentity, Revision pub mod application; pub mod application_revision; +pub mod fetch_nonce; pub mod has_member; pub mod members; pub mod members_revision; @@ -101,4 +102,14 @@ impl<'a, T: Transport> ContextConfigQuery<'a, T> { utils::send(&self.client, Operation::Read(params)).await } + + pub async fn fetch_nonce( + &self, + context_id: ContextId, + member_id: ContextIdentity, + ) -> Result> { + let params = fetch_nonce::FetchNonceRequest::new(context_id, member_id); + + utils::send(&self.client, Operation::Read(params)).await + } } diff --git a/crates/context/config/src/client/env/config/query/fetch_nonce.rs b/crates/context/config/src/client/env/config/query/fetch_nonce.rs new file mode 100644 index 000000000..356457a64 --- /dev/null +++ b/crates/context/config/src/client/env/config/query/fetch_nonce.rs @@ -0,0 +1,102 @@ +use candid::{Decode, Encode}; +use serde::Serialize; +use starknet::core::codec::Encode as StarknetEncode; + +use crate::client::env::config::types::starknet::{ + CallData, ContextId as StarknetContextId, ContextIdentity as StarknetContextIdentity, +}; +use crate::client::env::Method; +use crate::client::protocol::icp::Icp; +use crate::client::protocol::near::Near; +use crate::client::protocol::starknet::Starknet; +use crate::icp::repr::ICRepr; +use crate::repr::Repr; +use crate::types::{ContextId, ContextIdentity}; + +#[derive(Copy, Clone, Debug, Serialize)] +pub(super) struct FetchNonceRequest { + pub(super) context_id: Repr, + pub(super) member: Repr, +} + +impl FetchNonceRequest { + pub const fn new(context_id: ContextId, member: ContextIdentity) -> Self { + Self { + context_id: Repr::new(context_id), + member: Repr::new(member), + } + } +} + +impl Method for FetchNonceRequest { + const METHOD: &'static str = "fetch_nonce"; + + type Returns = u64; + + fn encode(self) -> eyre::Result> { + serde_json::to_vec(&self).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let nonce: u64 = serde_json::from_slice(&response)?; + + Ok(nonce) + } +} + +impl Method for FetchNonceRequest { + type Returns = u64; + + const METHOD: &'static str = "fetch_nonce"; + + fn encode(self) -> eyre::Result> { + let mut call_data = CallData::default(); + + // Dereference Repr and encode context_id + let context_id: StarknetContextId = (*self.context_id).into(); + context_id.encode(&mut call_data)?; + + let member: StarknetContextIdentity = (*self.member).into(); + member.encode(&mut call_data)?; + + Ok(call_data.0) + } + + fn decode(response: Vec) -> eyre::Result { + if response.len() != 8 { + return Err(eyre::eyre!( + "Invalid response length: expected 8 bytes, got {}", + response.len() + )); + } + + let nonce = u64::from_be_bytes( + response + .try_into() + .map_err(|_| eyre::eyre!("Failed to convert response to u64"))?, + ); + + Ok(nonce) + } +} + +impl Method for FetchNonceRequest { + type Returns = u64; + + const METHOD: &'static str = "fetch_nonce"; + + fn encode(self) -> eyre::Result> { + let context_id = ICRepr::new(*self.context_id); + let member = ICRepr::new(*self.member); + + let payload = (context_id, member); + + Encode!(&payload).map_err(Into::into) + } + + fn decode(response: Vec) -> eyre::Result { + let decoded = Decode!(&response, u64)?; + + Ok(decoded) + } +} diff --git a/crates/context/src/lib.rs b/crates/context/src/lib.rs index e706b1ee7..60e4befae 100644 --- a/crates/context/src/lib.rs +++ b/crates/context/src/lib.rs @@ -217,6 +217,22 @@ impl ContextManager { ) } + let nonce: u64 = this + .config_client + .query::( + 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(), + ) + .fetch_nonce( + context.id.rt().expect("infallible conversion"), + identity_secret + .public_key() + .rt() + .expect("infallible conversion"), + ) + .await?; + this.config_client .mutate::( this.client_config.new.protocol.as_str().into(), @@ -237,7 +253,7 @@ impl ContextManager { ApplicationMetadataConfig(Repr::new(application.metadata.into())), ), ) - .send(*context_secret) + .send(*context_secret, nonce) .await?; let proxy_contract = this @@ -411,6 +427,18 @@ impl ContextManager { return Ok(None); }; + let member_id = inviter_id.rt().expect("infallible conversion"); + + let nonce: u64 = self + .config_client + .query::( + context_config.protocol.as_ref().into(), + context_config.network.as_ref().into(), + context_config.contract.as_ref().into(), + ) + .fetch_nonce(context_id.rt().expect("infallible conversion"), member_id) + .await?; + self.config_client .mutate::( context_config.protocol.as_ref().into(), @@ -421,7 +449,7 @@ impl ContextManager { context_id.rt().expect("infallible conversion"), &[invitee_id.rt().expect("infallible conversion")], ) - .send(requester_secret) + .send(requester_secret, nonce) .await?; let invitation_payload = ContextInvitationPayload::new( @@ -928,6 +956,20 @@ impl ContextManager { context_id ); }; + + let nonce: u64 = self + .config_client + .query::( + context_config.protocol.as_ref().into(), + context_config.network.as_ref().into(), + context_config.contract.as_ref().into(), + ) + .fetch_nonce( + context_id.rt().expect("infallible conversion"), + signer_id.rt().expect("infallible conversion"), + ) + .await?; + let _ = self .config_client .mutate::( @@ -945,7 +987,7 @@ impl ContextManager { ApplicationMetadataConfig(Repr::new(application.metadata.into())), ), ) - .send(requester_secret) + .send(requester_secret, nonce) .await?; context_meta.application = ApplicationMetaKey::new(application_id);