diff --git a/gateway-types/src/lib.rs b/gateway-types/src/lib.rs index b3a4208..f782625 100644 --- a/gateway-types/src/lib.rs +++ b/gateway-types/src/lib.rs @@ -20,3 +20,24 @@ pub struct Message { /// Signature of S pub s: Vec, } + +/// GrantInstallationResult represents the result of a grant installation operation in the DID registry. +/// +/// This struct encapsulates the outcome of an attempt to grant an installation, +/// providing details about the operation's status, a descriptive message, and the +/// transaction identifier associated with the blockchain transaction. +/// +/// # Fields +/// * `status` - A `String` indicating the outcome status of the operation. Typically, this +/// would be values like "Success" or "Failure". +/// * `message` - A `String` providing more detailed information about the operation. This +/// can be a success message, error description, or any other relevant information. +/// * `transaction` - A `String` representing the unique identifier of the transaction on the +/// blockchain. This can be used to track the transaction in a blockchain explorer. +/// +#[derive(Serialize, Deserialize, Clone)] +pub struct GrantInstallationResult { + pub status: String, + pub message: String, + pub transaction: String, +} diff --git a/registry/src/lib.rs b/registry/src/lib.rs index d66f9a7..5b58f4c 100644 --- a/registry/src/lib.rs +++ b/registry/src/lib.rs @@ -2,14 +2,15 @@ pub mod error; use std::str::FromStr; +use error::ContactOperationError; +use ethers::types::{H160, U256}; use ethers::{core::types::Signature, providers::Middleware, types::Address}; +use gateway_types::GrantInstallationResult; use lib_didethresolver::{ did_registry::DIDRegistry, types::{Attribute, XmtpAttribute}, }; -use error::ContactOperationError; - pub struct ContactOperations { registry: DIDRegistry, } @@ -23,6 +24,49 @@ where Self { registry } } + fn resolve_did_address(&self, did: String) -> Result> { + // for now, we will just assume the DID is a valid ethereum wallet address + // TODO: Parse or resolve the actual DID + let address = Address::from_str(&did)?; + Ok(address) + } + + pub async fn grant_installation( + &self, + did: String, + name: XmtpAttribute, + value: Vec, + signature: Signature, + validity: U256, + ) -> Result> { + let address = self.resolve_did_address(did)?; + let attribute: [u8; 32] = Attribute::from(name).into(); + log::debug!( + "setting attribute {:#?}", + String::from_utf8_lossy(&attribute) + ); + + let transaction_receipt = self + .registry + .set_attribute_signed( + address, + signature.v.try_into()?, + signature.r.into(), + signature.s.into(), + attribute, + value.into(), + validity, + ) + .send() + .await? + .await?; + Ok(GrantInstallationResult { + status: "completed".to_string(), + message: "Installation request complete.".to_string(), + transaction: transaction_receipt.unwrap().transaction_hash.to_string(), + }) + } + pub async fn revoke_installation( &self, did: String, @@ -30,9 +74,7 @@ where value: Vec, signature: Signature, ) -> Result<(), ContactOperationError> { - // for now, we will just assume the DID is a valid ethereum wallet address - // TODO: Parse or resolve the actual DID - let address = Address::from_str(&did)?; + let address = self.resolve_did_address(did)?; let attribute: [u8; 32] = Attribute::from(name).into(); log::debug!( "Revoking attribute {:#?}", diff --git a/xps-gateway/src/rpc/api.rs b/xps-gateway/src/rpc/api.rs index 14055f0..4b180ce 100644 --- a/xps-gateway/src/rpc/api.rs +++ b/xps-gateway/src/rpc/api.rs @@ -4,6 +4,7 @@ use ethers::core::types::Signature; use ethers::prelude::*; use jsonrpsee::{proc_macros::rpc, types::ErrorObjectOwned}; +use gateway_types::GrantInstallationResult; use gateway_types::Message; use lib_didethresolver::types::XmtpAttribute; @@ -14,6 +15,103 @@ pub trait Xps { #[method(name = "sendMessage")] async fn send_message(&self, _message: Message) -> Result<(), ErrorObjectOwned>; + /// # Documentation for JSON RPC Endpoint: `grantInstallation` + /// + /// ## Overview + /// + /// The `grantInstallation` method is used to register an installation on the network and associate the installation with a concrete identity. + /// + /// ## JSON RPC Endpoint Specification + /// + /// ### Method Name + /// `grantInstallation` + /// + /// ### Request Parameters + /// did: string + /// name: String, + /// value: String, + /// signature: Signature, + /// + /// ### Request Format + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "method": "status", + /// "id": 1 + /// } + /// ``` + + /// - `jsonrpc`: Specifies the version of the JSON RPC protocol being used. Always "2.0". + /// - `method`: The name of the method being called. Here it is "grantInstallation". + /// - `id`: A unique identifier established by the client that must be number or string. Used for correlating the response with the request. + + /// ### Response Format + /// The response will typically include the result of the operation or an error if the operation was unsuccessful. + + /// #### Success Response + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "result": "OK", + /// "id": 1 + /// } + /// ``` + /// + /// - `result`: Contains data related to the success of the operation. The nature of this data can vary based on the implementation. + /// + /// #### Error Response + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "error": { + /// "code": , + /// "message": "" + /// }, + /// "id": 1 + /// } + /// ``` + /// + /// - `error`: An object containing details about the error. + /// - `code`: A numeric error code. + /// - `message`: A human-readable string describing the error. + /// + /// ### Example Usage + /// + /// #### Request + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "method": "status", + /// "id": 42 + /// } + /// ``` + /// + /// #### Response + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "result": "OK", + /// "id": 42 + /// } + /// ``` + /// + /// ### Command Line Example + /// ```bash + /// $ $ curl -H "Content-Type: application/json" -d '{"id":7000, "jsonrpc":"2.0", "method":"xps_status"}' http:///localhost:34695 + /// {"jsonrpc":"2.0","result":"OK","id":7000} + /// ``` + /// + /// ### Notes + /// - The system should have proper error handling to deal with invalid requests, unauthorized access, and other potential issues. + #[method(name = "grantInstallation")] + async fn grant_installation( + &self, + did: String, + name: XmtpAttribute, + value: Vec, + signature: Signature, + ) -> Result; + /// # Documentation for JSON RPC Endpoint: `revoke_installation` /// /// ## JSON RPC Endpoint Specification diff --git a/xps-gateway/src/rpc/methods.rs b/xps-gateway/src/rpc/methods.rs index b44f46c..262eb46 100644 --- a/xps-gateway/src/rpc/methods.rs +++ b/xps-gateway/src/rpc/methods.rs @@ -8,9 +8,11 @@ use jsonrpsee::types::error::ErrorCode; use async_trait::async_trait; use ethers::prelude::*; use ethers::{core::types::Signature, providers::Middleware}; +use gateway_types::GrantInstallationResult; use jsonrpsee::types::ErrorObjectOwned; use lib_didethresolver::types::XmtpAttribute; use rand::{rngs::StdRng, SeedableRng}; +use std::sync::Arc; use thiserror::Error; use gateway_types::Message; @@ -20,6 +22,7 @@ use registry::{error::ContactOperationError, ContactOperations}; pub struct XpsMethods { contact_operations: ContactOperations>, pub wallet: LocalWallet, + pub signer: Arc>, } impl XpsMethods

{ @@ -27,6 +30,7 @@ impl XpsMethods

{ Self { contact_operations: ContactOperations::new(context.registry.clone()), wallet: LocalWallet::new(&mut StdRng::from_entropy()), + signer: context.signer.clone(), } } } @@ -44,6 +48,27 @@ impl XpsServer for XpsMethods

{ Ok("OK".to_string()) } + async fn grant_installation( + &self, + did: String, + name: XmtpAttribute, + value: Vec, + signature: Signature, + ) -> Result { + log::debug!("xps_grantInstallation called"); + let block_number = self.signer.get_block_number().await.unwrap(); + let validity_period: U64 = U64::from(60 * 60 * 24 * 365 / 5); // number of round in one year, assuming 5-second round. + let validity = block_number + validity_period; + + let result = self + .contact_operations + .grant_installation(did, name, value, signature, U256::from(validity.as_u64())) + .await + .map_err(RpcError::from)?; + + Ok(result) + } + async fn revoke_installation( &self, did: String, diff --git a/xps-gateway/tests/integration_test.rs b/xps-gateway/tests/integration_test.rs index 58d3f61..c144f2f 100644 --- a/xps-gateway/tests/integration_test.rs +++ b/xps-gateway/tests/integration_test.rs @@ -9,7 +9,8 @@ use lib_didethresolver::{ }; use xps_gateway::rpc::XpsClient; -use ethers::types::{Address, U256}; +use ethers::middleware::Middleware; +use ethers::types::{Address, U256, U64}; use gateway_types::Message; use integration_util::*; @@ -51,6 +52,69 @@ async fn test_wallet_address() -> Result<(), Error> { .await } +#[tokio::test] +async fn test_grant_installation() -> Result<(), Error> { + with_xps_client(None, |client, context, resolver, anvil| async move { + let wallet: LocalWallet = anvil.keys()[3].clone().into(); + let me = get_user(&anvil, 3).await; + let name = *b"xmtp/installation/hex "; + let value = b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71"; + + let attribute = XmtpAttribute { + purpose: XmtpKeyPurpose::Installation, + encoding: KeyEncoding::Hex, + }; + + let block_number = context.signer.get_block_number().await.unwrap(); + let validity_period: U64 = U64::from(60 * 60 * 24 * 365 / 5); // number of round in one year, assuming 5-second round. + let validity = block_number + validity_period; + + let signature = wallet + .sign_attribute( + &context.registry, + name, + value.to_vec(), + U256::from(validity.as_u64()), + ) + .await?; + + client + .grant_installation( + format!("0x{}", hex::encode(me.address())), + attribute, + value.to_vec(), + signature, + ) + .await?; + + let doc = resolver + .resolve_did(me.address(), None) + .await + .unwrap() + .document; + + assert_eq!(doc.verification_method.len(), 2); + assert_eq!( + doc.verification_method[0].id, + DidUrl::parse(format!( + "did:ethr:0x{}#controller", + hex::encode(me.address()) + )) + .unwrap() + ); + assert_eq!( + doc.verification_method[1].id, + DidUrl::parse(format!( + "did:ethr:0x{}?meta=installation#xmtp-0", + hex::encode(me.address()) + )) + .unwrap() + ); + Ok(()) + }) + .await +} + #[tokio::test] async fn test_revoke_installation() -> Result<(), Error> { with_xps_client(None, |client, context, resolver, anvil| async move {