diff --git a/bindings_node/Cargo.toml b/bindings_node/Cargo.toml index 8cc4ee49e..aa1038961 100644 --- a/bindings_node/Cargo.toml +++ b/bindings_node/Cargo.toml @@ -8,6 +8,7 @@ crate-type = ["cdylib"] [dependencies] # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix +futures.workspace = true hex.workspace = true napi = { version = "2.12.2", default-features = false, features = [ "napi4", @@ -17,14 +18,18 @@ napi = { version = "2.12.2", default-features = false, features = [ napi-derive = "2.12.2" prost.workspace = true tokio = { workspace = true, features = ["sync"] } -tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json", "chrono"] } +tracing-subscriber = { workspace = true, features = [ + "env-filter", + "fmt", + "json", + "chrono", +] } tracing.workspace = true xmtp_api_grpc = { path = "../xmtp_api_grpc" } xmtp_cryptography = { path = "../xmtp_cryptography" } xmtp_id = { path = "../xmtp_id" } xmtp_mls = { path = "../xmtp_mls" } xmtp_proto = { path = "../xmtp_proto", features = ["proto_full"] } -futures.workspace = true [build-dependencies] napi-build = "2.0.1" diff --git a/bindings_node/package.json b/bindings_node/package.json index 3710b3740..55165b85f 100644 --- a/bindings_node/package.json +++ b/bindings_node/package.json @@ -1,6 +1,6 @@ { "name": "@xmtp/node-bindings", - "version": "0.0.25", + "version": "0.0.26", "repository": { "type": "git", "url": "git+https://git@github.com/xmtp/libxmtp.git", diff --git a/bindings_node/src/client.rs b/bindings_node/src/client.rs index d554274f0..9a8e094e7 100644 --- a/bindings_node/src/client.rs +++ b/bindings_node/src/client.rs @@ -14,9 +14,11 @@ pub use xmtp_api_grpc::grpc_api_helper::Client as TonicApiClient; use xmtp_cryptography::signature::ed25519_public_key_to_address; use xmtp_id::associations::builder::SignatureRequest; use xmtp_id::associations::MemberIdentifier; +use xmtp_mls::api::ApiClientWrapper; use xmtp_mls::builder::ClientBuilder; use xmtp_mls::groups::scoped_client::LocalScopedGroupClient; use xmtp_mls::identity::IdentityStrategy; +use xmtp_mls::retry::Retry; use xmtp_mls::storage::{EncryptedMessageStore, EncryptionKey, StorageOption}; use xmtp_mls::Client as MlsClient; use xmtp_proto::xmtp::mls::message_contents::DeviceSyncKind; @@ -131,7 +133,7 @@ pub async fn create_client( log_options: Option, ) -> Result { init_logging(log_options.unwrap_or_default())?; - let api_client = TonicApiClient::create(host.clone(), is_secure) + let api_client = TonicApiClient::create(&host, is_secure) .await .map_err(|_| Error::from_reason("Error creating Tonic API client"))?; @@ -343,3 +345,48 @@ impl Client { Ok(association_state.get(identifier).is_some()) } } + +#[napi] +pub async fn is_installation_authorized( + host: String, + inbox_id: String, + installation_id: Uint8Array, +) -> Result { + is_member_of_association_state( + &host, + &inbox_id, + &MemberIdentifier::Installation(installation_id.to_vec()), + ) + .await +} + +#[napi] +pub async fn is_address_authorized( + host: String, + inbox_id: String, + address: String, +) -> Result { + is_member_of_association_state(&host, &inbox_id, &MemberIdentifier::Address(address)).await +} + +async fn is_member_of_association_state( + host: &str, + inbox_id: &str, + identifier: &MemberIdentifier, +) -> Result { + let api_client = TonicApiClient::create(host, true) + .await + .map_err(ErrorWrapper::from)?; + let api_client = ApiClientWrapper::new(Arc::new(api_client), Retry::default()); + + let is_member = xmtp_mls::identity_updates::is_member_of_association_state( + &api_client, + inbox_id, + identifier, + None, + ) + .await + .map_err(ErrorWrapper::from)?; + + Ok(is_member) +} diff --git a/bindings_node/src/inbox_id.rs b/bindings_node/src/inbox_id.rs index ebe93c6d7..34821514a 100644 --- a/bindings_node/src/inbox_id.rs +++ b/bindings_node/src/inbox_id.rs @@ -14,7 +14,7 @@ pub async fn get_inbox_id_for_address( ) -> Result> { let account_address = account_address.to_lowercase(); let api_client = ApiClientWrapper::new( - TonicApiClient::create(host.clone(), is_secure) + TonicApiClient::create(host, is_secure) .await .map_err(ErrorWrapper::from)? .into(), diff --git a/examples/cli/cli-client.rs b/examples/cli/cli-client.rs index 5598ee25a..709c7a8a6 100755 --- a/examples/cli/cli-client.rs +++ b/examples/cli/cli-client.rs @@ -251,15 +251,13 @@ async fn main() -> color_eyre::eyre::Result<()> { ) .await?, ), - (false, Env::Local) => { - Box::new(ClientV3::create("http://localhost:5556".into(), false).await?) - } + (false, Env::Local) => Box::new(ClientV3::create("http://localhost:5556", false).await?), (false, Env::Dev) => { - Box::new(ClientV3::create("https://grpc.dev.xmtp.network:443".into(), true).await?) + Box::new(ClientV3::create("https://grpc.dev.xmtp.network:443", true).await?) + } + (false, Env::Production) => { + Box::new(ClientV3::create("https://grpc.production.xmtp.network:443", true).await?) } - (false, Env::Production) => Box::new( - ClientV3::create("https://grpc.production.xmtp.network:443".into(), true).await?, - ), (true, Env::Production) => todo!("not supported"), }; diff --git a/xmtp_api_grpc/src/grpc_api_helper.rs b/xmtp_api_grpc/src/grpc_api_helper.rs index 150e0b57c..fa7683de7 100644 --- a/xmtp_api_grpc/src/grpc_api_helper.rs +++ b/xmtp_api_grpc/src/grpc_api_helper.rs @@ -74,7 +74,7 @@ pub struct Client { } impl Client { - pub async fn create(host: String, is_secure: bool) -> Result { + pub async fn create(host: impl ToString, is_secure: bool) -> Result { let host = host.to_string(); let app_version = MetadataValue::try_from(&String::from("0.0.0")) .map_err(|e| Error::new(ErrorKind::MetadataError).with(e))?; diff --git a/xmtp_api_grpc/src/lib.rs b/xmtp_api_grpc/src/lib.rs index d6bfe96e1..6da24430a 100644 --- a/xmtp_api_grpc/src/lib.rs +++ b/xmtp_api_grpc/src/lib.rs @@ -16,13 +16,13 @@ mod utils { #[async_trait::async_trait] impl XmtpTestClient for crate::Client { async fn create_local() -> Self { - crate::Client::create("http://localhost:5556".into(), false) + crate::Client::create("http://localhost:5556", false) .await .unwrap() } async fn create_dev() -> Self { - crate::Client::create("https://grpc.dev.xmtp.network:443".into(), false) + crate::Client::create("https://grpc.dev.xmtp.network:443", false) .await .unwrap() } diff --git a/xmtp_mls/src/identity_updates.rs b/xmtp_mls/src/identity_updates.rs index 53d938a78..7c7220aa9 100644 --- a/xmtp_mls/src/identity_updates.rs +++ b/xmtp_mls/src/identity_updates.rs @@ -1,11 +1,10 @@ -use std::collections::{HashMap, HashSet}; - use crate::{ retry::{Retry, RetryableError}, retry_async, retryable, storage::association_state::StoredAssociationState, }; use futures::future::try_join_all; +use std::collections::{HashMap, HashSet}; use thiserror::Error; use xmtp_cryptography::CredentialSign; use xmtp_id::{ @@ -16,12 +15,13 @@ use xmtp_id::{ unverified::{ UnverifiedIdentityUpdate, UnverifiedInstallationKeySignature, UnverifiedSignature, }, - AssociationError, AssociationState, AssociationStateDiff, IdentityUpdate, + AssociationError, AssociationState, AssociationStateDiff, IdentityAction, IdentityUpdate, InstallationKeyContext, MemberIdentifier, SignatureError, }, - scw_verifier::SmartContractSignatureVerifier, + scw_verifier::{RemoteSignatureVerifier, SmartContractSignatureVerifier}, InboxIdRef, }; +use xmtp_proto::api_client::{ClientWithMetadata, XmtpIdentityClient, XmtpMlsClient}; use crate::{ api::{ApiClientWrapper, GetIdentityUpdatesV2Filter, InboxUpdate}, @@ -284,7 +284,7 @@ where let inbox_id = self.inbox_id(); let builder = SignatureRequestBuilder::new(inbox_id); let installation_public_key = self.identity().installation_keys.verifying_key(); - let new_member_identifier: MemberIdentifier = new_wallet_address.into(); + let new_member_identifier = MemberIdentifier::Address(new_wallet_address); let mut signature_request = builder .add_association(new_member_identifier, installation_public_key.into()) @@ -528,6 +528,53 @@ async fn verify_updates( .await } +/// A static lookup method to verify if an identity is a member of an inbox +pub async fn is_member_of_association_state( + api_client: &ApiClientWrapper, + inbox_id: &str, + identifier: &MemberIdentifier, + scw_verifier: Option>, +) -> Result +where + Client: XmtpMlsClient + XmtpIdentityClient + ClientWithMetadata + Send + Sync, +{ + let filters = vec![GetIdentityUpdatesV2Filter { + inbox_id: inbox_id.to_string(), + sequence_id: None, + }]; + let mut updates = api_client.get_identity_updates_v2(filters).await?; + + let Some(updates) = updates.remove(inbox_id) else { + return Err(ClientError::Generic( + "Unable to find provided inbox_id".to_string(), + )); + }; + let updates: Vec<_> = updates.into_iter().map(|u| u.update).collect(); + + let mut association_state = None; + + let scw_verifier = scw_verifier.unwrap_or_else(|| { + Box::new(RemoteSignatureVerifier::new(api_client.api_client.clone())) + as Box + }); + + let updates: Vec<_> = updates + .iter() + .map(|u| u.to_verified(&scw_verifier)) + .collect(); + let updates = try_join_all(updates).await?; + + for update in updates { + association_state = + Some(update.update_state(association_state, update.client_timestamp_ns)?); + } + let association_state = association_state.ok_or(ClientError::Generic( + "Unable to create association state".to_string(), + ))?; + + Ok(association_state.get(identifier).is_some()) +} + #[cfg(test)] pub(crate) mod tests { #[cfg(target_arch = "wasm32")] @@ -550,7 +597,7 @@ pub(crate) mod tests { Client, XmtpApi, }; - use super::load_identity_updates; + use super::{is_member_of_association_state, load_identity_updates}; async fn get_association_state( client: &Client, @@ -579,6 +626,45 @@ pub(crate) mod tests { .expect("insert should succeed"); } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn test_is_member_of_association_state() { + let wallet = generate_local_wallet(); + let client = ClientBuilder::new_test_client(&wallet).await; + + let wallet2 = generate_local_wallet(); + + let mut request = client + .associate_wallet(wallet2.get_address()) + .await + .unwrap(); + add_wallet_signature(&mut request, &wallet2).await; + client.apply_signature_request(request).await.unwrap(); + + let conn = client.store().conn().unwrap(); + let state = client + .get_latest_association_state(&conn, client.inbox_id()) + .await + .unwrap(); + + // The installation, wallet1 address, and the newly associated wallet2 address + assert_eq!(state.members().len(), 3); + + let api_client = &client.api_client; + + // Check that the second wallet is associated with our new static helper + let is_member = is_member_of_association_state( + api_client, + client.inbox_id(), + &MemberIdentifier::Address(wallet2.get_address()), + None, + ) + .await + .unwrap(); + + assert!(is_member); + } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn create_inbox_round_trip() { diff --git a/xmtp_mls/src/lib.rs b/xmtp_mls/src/lib.rs index 86095c223..6384cc5de 100644 --- a/xmtp_mls/src/lib.rs +++ b/xmtp_mls/src/lib.rs @@ -9,7 +9,7 @@ pub mod configuration; pub mod groups; mod hpke; pub mod identity; -mod identity_updates; +pub mod identity_updates; mod intents; mod mutex_registry; pub mod retry;