From 401925d18ce2414134ce99e4522c8a8263e3eb27 Mon Sep 17 00:00:00 2001 From: Andrew Plaza Date: Wed, 21 Feb 2024 16:40:52 -0500 Subject: [PATCH] Add nonce JSON-RPC endpoint --- Cargo.lock | 2 + lib-xps/src/rpc/api.rs | 172 ++++++++++++++++++++++++++++-- lib-xps/src/rpc/methods.rs | 10 ++ lib-xps/src/util.rs | 1 - lib-xps/tests/integration_test.rs | 13 +++ registry/Cargo.toml | 4 + registry/src/error.rs | 2 + registry/src/lib.rs | 75 +++++++++++-- registry/src/test.rs | 19 ++++ 9 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 registry/src/test.rs diff --git a/Cargo.lock b/Cargo.lock index fe7a118..0ec1e39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2969,6 +2969,7 @@ name = "registry" version = "0.1.0" dependencies = [ "async-trait", + "ctor", "ethers", "lib-didethresolver", "log", @@ -2976,6 +2977,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "tracing-subscriber", "xps-types", ] diff --git a/lib-xps/src/rpc/api.rs b/lib-xps/src/rpc/api.rs index d099955..8300475 100644 --- a/lib-xps/src/rpc/api.rs +++ b/lib-xps/src/rpc/api.rs @@ -517,14 +517,90 @@ pub trait Xps { #[method(name = "status")] async fn status(&self) -> Result; + /// ### Documentation for JSON RPC Endpoint: `xps_walletAddress` + /// --- + /// #### Endpoint Name: `xps_walletAddress` + /// #### Description: + /// The `xps_walletAddress` endpoint retrieves the current Wallet Address of the internal wallet managed by the server. This endpoint is essential for applications that need to display or monitor the wallet, especially in the context of cryptocurrency transactions or account management. + /// #### Request: + /// - **Method:** `POST` + /// - **URL:** `/rpc/v1/walletAddress` + /// - **Headers:** + /// - `Content-Type: application/json` + /// - **Body:** + /// - **JSON Object:** + /// - `jsonrpc`: `"2.0"` + /// - `method`: `"xps_walletAddress"` + /// - `params`: Array (optional parameters as required) + /// - `id`: Request identifier (integer or string) + /// **Example Request Body:** + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "method": "xps_walletAddress", + /// "params": [], + /// "id": 1 + /// } + /// ``` + /// #### Response: + /// - **Success Status Code:** `200 OK` + /// - **Error Status Codes:** + /// - `400 Bad Request` - Invalid request format or parameters. + /// - `500 Internal Server Error` - Server or wallet-related error. + /// **Success Response Body:** + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "result": "0x0000000000000000000000000000000000000000", + /// "id": 1 + /// } + /// ``` + /// **Error Response Body:** + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "error": { + /// "code": -32602, + /// "message": "Invalid parameters" + /// }, + /// "id": 1 + /// } + /// ``` + /// #### Error Handling: + /// - **Invalid Parameters:** Check if the request body is properly formatted and includes valid parameters. + /// - **Wallet or Server Errors:** Ensure that the server and wallet are operational. Consult server logs for detailed error information. + /// #### Security Considerations: + /// - **Authentication and Authorization:** Implement robust authentication and authorization checks to ensure only authorized users can access wallet balance information. + /// - **Secure Communication:** Utilize HTTPS to encrypt data in transit and prevent eavesdropping. + /// #### Usage Example: + /// ```javascript + /// const requestBody = { + /// jsonrpc: "2.0", + /// method: "xps_walletAddress", + /// params: [], + /// id: 1 + /// }; + /// fetch('https://server.example.com/rpc/v1/walletAddress', { + /// method: 'POST', + /// headers: { + /// 'Content-Type': 'application/json' + /// }, + /// body: JSON.stringify(requestBody) + /// }) + /// .then(response => response.json()) + /// .then(data => console.log('Wallet Balance:', data.result)) + /// .catch(error => console.error('Error:', error)); + /// ``` + /// + /// ``` #[method(name = "walletAddress")] async fn wallet_address(&self) -> Result; - /// ### Documentation for JSON RPC Endpoint: `balance` + /// ### Documentation for JSON RPC Endpoint: `xps_balance` /// --- - /// #### Endpoint Name: `balance` + /// #### Endpoint Name: `xps_balance` /// #### Description: - /// The `balance` endpoint retrieves the current balance of the internal wallet managed by the server. This endpoint is essential for applications that need to display or monitor the wallet's balance, especially in the context of cryptocurrency transactions or account management. + /// The `xps_balance` endpoint retrieves the current balance of the internal wallet managed by the server. This endpoint is essential for applications that need to display or monitor the wallet's balance, especially in the context of cryptocurrency transactions or account management. /// #### Request: /// - **Method:** `POST` /// - **URL:** `/rpc/v1/balance` @@ -533,14 +609,14 @@ pub trait Xps { /// - **Body:** /// - **JSON Object:** /// - `jsonrpc`: `"2.0"` - /// - `method`: `"balance"` + /// - `method`: `"xps_balance"` /// - `params`: Array (optional parameters as required) /// - `id`: Request identifier (integer or string) /// **Example Request Body:** /// ```json /// { /// "jsonrpc": "2.0", - /// "method": "balance", + /// "method": "xps_balance", /// "params": [], /// "id": 1 /// } @@ -582,7 +658,7 @@ pub trait Xps { /// ```javascript /// const requestBody = { /// jsonrpc: "2.0", - /// method: "balance", + /// method: "xps_balance", /// params: [], /// id: 1 /// }; @@ -601,4 +677,88 @@ pub trait Xps { /// ``` #[method(name = "balance")] async fn balance(&self) -> Result; + + /// ### Documentation for JSON RPC Endpoint: `xps_nonce` + /// --- + /// #### Endpoint Name: `nonce` + /// #### Description: + /// The `xps_nonce` endpoint retrieves the nonce for `address` in the [`DIDRegistry`]. This is + /// needed for signing payloads for DID Registry meta transactions. + + /// #### Request: + /// - **Method:** `POST` + /// - **URL:** `/rpc/v1/nonce` + /// - **Headers:** + /// - `Content-Type: application/json` + /// - **Body:** + /// - **JSON Object:** + /// - `jsonrpc`: `"2.0"` + /// - `method`: `"xps_nonce"` + /// - `params`: Array (optional parameters as required) + /// - `id`: Request identifier (integer or string) + /// **Example Request Body:** + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "method": "xps_nonce", + /// "params": ["0x0000000000000000000000000000000000000000"], + /// "id": 1 + /// } + /// ``` + /// #### Response: + /// - **Success Status Code:** `200 OK` + /// - **Error Status Codes:** + /// - `400 Bad Request` - Invalid request format or parameters. + /// - `500 Internal Server Error` - Server or wallet-related error. + /// **Success Response Body:** + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "result": { + /// "nonce": 0 + /// }, + /// "id": 1 + /// } + /// ``` + /// **Error Response Body:** + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "error": { + /// "code": -32602, + /// "message": "Invalid parameters" + /// }, + /// "id": 1 + /// } + /// ``` + /// #### Error Handling: + /// - **Invalid Parameters:** Check if the request body is properly formatted and includes valid parameters. + /// - **Wallet or Server Errors:** Ensure that the server and wallet are operational. Consult server logs for detailed error information. + /// #### Security Considerations: + /// - **Authentication and Authorization:** Implement robust authentication and authorization checks to ensure only authorized users can access wallet balance information. + /// - **Secure Communication:** Utilize HTTPS to encrypt data in transit and prevent eavesdropping. + /// #### Usage Example: + /// ```javascript + /// const requestBody = { + /// jsonrpc: "2.0", + /// method: "xps_nonce", + /// params: ["0x0000000000000000000000000000000000000000"], + /// id: 1 + /// }; + /// fetch('https://server.example.com/rpc/v1/nonce', { + /// method: 'POST', + /// headers: { + /// 'Content-Type': 'application/json' + /// }, + /// body: JSON.stringify(requestBody) + /// }) + /// .then(response => response.json()) + /// .then(data => console.log('Wallet Balance:', data.result)) + /// .catch(error => console.error('Error:', error)); + /// ``` + /// + /// ``` + + #[method(name = "nonce")] + async fn nonce(&self, did: String) -> Result; } diff --git a/lib-xps/src/rpc/methods.rs b/lib-xps/src/rpc/methods.rs index efab4a6..a34d64b 100644 --- a/lib-xps/src/rpc/methods.rs +++ b/lib-xps/src/rpc/methods.rs @@ -145,6 +145,16 @@ impl XpsServer for XpsMethods

{ .map_err(RpcError::from)?; Ok(result) } + + async fn nonce(&self, did: String) -> Result { + log::debug!("xps_nonce called"); + let result = self + .contact_operations + .nonce(did) + .await + .map_err(RpcError::from)?; + Ok(result) + } } /// Error types for DID Registry JSON-RPC diff --git a/lib-xps/src/util.rs b/lib-xps/src/util.rs index f1b0d3c..4fdbe75 100644 --- a/lib-xps/src/util.rs +++ b/lib-xps/src/util.rs @@ -1,5 +1,4 @@ //! Internal Utility functions for use in crate - #[cfg(test)] use std::sync::Once; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; diff --git a/lib-xps/tests/integration_test.rs b/lib-xps/tests/integration_test.rs index 3ab3dd0..7716293 100644 --- a/lib-xps/tests/integration_test.rs +++ b/lib-xps/tests/integration_test.rs @@ -582,3 +582,16 @@ async fn test_did_deactivation() -> Result<(), Error> { }) .await } + +#[tokio::test] +async fn test_nonce() -> Result<(), Error> { + with_xps_client(None, None, |client, _, _, anvil| async move { + let me: LocalWallet = anvil.keys()[3].clone().into(); + + let nonce = client.nonce(hex::encode(me.address())).await?; + assert_eq!(U256::from(0), nonce); + + Ok(()) + }) + .await +} diff --git a/registry/Cargo.toml b/registry/Cargo.toml index 74feccb..ea089b7 100644 --- a/registry/Cargo.toml +++ b/registry/Cargo.toml @@ -15,3 +15,7 @@ xps-types.workspace = true lib-didethresolver.workspace = true rustc-hex.workspace = true thiserror.workspace = true + +[dev-dependencies] +tracing-subscriber.workspace = true +ctor.workspace = true diff --git a/registry/src/error.rs b/registry/src/error.rs index b643878..683e9f3 100644 --- a/registry/src/error.rs +++ b/registry/src/error.rs @@ -24,4 +24,6 @@ pub enum ContactOperationError { DIDDeactivated, #[error("Type failed to convert")] Type(#[from] lib_didethresolver::error::TypeError), + #[error("Error parsing hex bytes: {0}")] + Bytes(#[from] ethers::types::ParseBytesError), } diff --git a/registry/src/lib.rs b/registry/src/lib.rs index a41ce88..c77b358 100644 --- a/registry/src/lib.rs +++ b/registry/src/lib.rs @@ -1,13 +1,19 @@ pub mod error; +#[cfg(test)] +mod test; use std::str::FromStr; use error::ContactOperationError; -use ethers::types::{H160, U256}; -use ethers::{core::types::Signature, providers::Middleware, types::Address}; -use lib_didethresolver::types::VerificationMethodProperties; -use lib_didethresolver::Resolver; -use lib_didethresolver::{did_registry::DIDRegistry, types::XmtpAttribute}; +use ethers::{ + providers::Middleware, + types::{Address, Bytes, Signature, H160, U256}, +}; +use lib_didethresolver::{ + did_registry::DIDRegistry, + types::{VerificationMethodProperties, XmtpAttribute}, + Resolver, +}; use xps_types::{GrantInstallationResult, KeyPackageResult, Status}; pub struct ContactOperations { @@ -29,7 +35,8 @@ where 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)?; + + let address = Address::from_slice(Bytes::from_str(did.as_ref())?.to_vec().as_slice()); Ok(address) } @@ -165,4 +172,60 @@ where Ok(()) } + + pub async fn nonce(&self, did: String) -> Result> { + let address = self.resolve_did_address(did)?; + let nonce = self.registry.nonce(address).await?; + Ok(nonce) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ethers::{ + abi::AbiEncode, + providers::{MockProvider, Provider}, + }; + use lib_didethresolver::did_registry::NonceReturn; + + impl ContactOperations> { + pub fn mocked() -> (Self, MockProvider) { + let (mock_provider, mock) = Provider::mocked(); + let registry = DIDRegistry::new(H160::zero(), mock_provider.into()); + + (ContactOperations::new(registry), mock) + } + } + + #[test] + fn test_resolve_address_from_hexstr() { + let addr = "0x0000000000000000000000000000000000000000"; + let (ops, _) = ContactOperations::mocked(); + assert_eq!( + ops.resolve_did_address(addr.to_string()).unwrap(), + H160::zero() + ); + + let addr = "0000000000000000000000000000000000000000"; + assert_eq!( + ops.resolve_did_address(addr.to_string()).unwrap(), + H160::zero() + ); + } + + #[tokio::test] + async fn test_nonce() { + let (ops, mock) = ContactOperations::mocked(); + + mock.push::(NonceReturn(U256::from(212)).encode_hex()) + .unwrap(); + + let nonce = ops + .nonce("0x1111111111111111111111111111111111111111".to_string()) + .await + .unwrap(); + + assert_eq!(nonce, U256::from(212)); + } } diff --git a/registry/src/test.rs b/registry/src/test.rs new file mode 100644 index 0000000..406224a --- /dev/null +++ b/registry/src/test.rs @@ -0,0 +1,19 @@ +//! Internal Utility functions for use in crate +use std::sync::Once; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; + +static INIT: Once = Once::new(); + +#[ctor::ctor] +fn __init_test_logging() { + INIT.call_once(|| { + let fmt = fmt::layer().compact(); + Registry::default().with(env()).with(fmt).init() + }) +} + +/// Try to get the logging environment from the `RUST_LOG` environment variable. +/// If it is not set, use the default of `info`. +fn env() -> EnvFilter { + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")) +}