diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 837c758..d47249e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,19 +1,25 @@ name: Coverage on: [pull_request, push] jobs: - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Install Rust - run: rustup toolchain install nightly --component llvm-tools-preview - - name: Install cargo-llvm-cov - run: curl -LsSf https://github.com/taiki-e/cargo-llvm-cov/releases/latest/download/cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C ~/.cargo/bin - - name: Generate code coverage - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: lcov.info - fail_ci_if_error: true + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Rust + run: rustup toolchain install nightly --component llvm-tools-preview + - name: Install anvil + uses: baptiste0928/cargo-install@v2 + with: + crate: anvil + git: https://github.com/foundry-rs/foundry + commit: nightly + - name: Install cargo-llvm-cov + run: curl -LsSf https://github.com/taiki-e/cargo-llvm-cov/releases/latest/download/cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C ~/.cargo/bin + - name: Generate code coverage + run: RUST_LOG=xps_gateway=info,registry=info,inbox=info,messaging=info,gateway_types=info cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: lcov.info + fail_ci_if_error: true diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index d4cb3c9..3236bd2 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -1,22 +1,18 @@ name: GitHub Pages - on: push: branches: ["main"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: - # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write - # Allow one concurrent deployment concurrency: group: "github-pages" cancel-in-progress: true - jobs: build: runs-on: ubuntu-latest @@ -25,25 +21,22 @@ jobs: uses: actions/checkout@v3 with: submodules: recursive - - name: Setup Pages uses: actions/configure-pages@v2 - uses: actions/setup-python@v2 - - name: Install rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true - - name: Run tests env: - CARGO_INCREMENTAL: 0 + CARGO_INCREMENTAL: 0 run: | - cargo test + cargo test --docs + cargo test --lib id: test - - name: Invoke cargo doc run: | rm -rf ./_site @@ -55,10 +48,8 @@ jobs: echo "Taking care of pedantic permissions requirements required by GitHub Pages" chmod -R +rX _site id: docgen - - name: Upload artifact uses: actions/upload-pages-artifact@v2 - # Deployment job deploy: environment: diff --git a/Cargo.lock b/Cargo.lock index af6aa54..9f65520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.11" @@ -386,11 +401,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", ] [[package]] @@ -1086,6 +1106,7 @@ dependencies = [ "const-hex", "enr", "ethers-core", + "futures-channel", "futures-core", "futures-timer", "futures-util", @@ -1385,6 +1406,14 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gateway-types" +version = "0.1.0" +dependencies = [ + "lib-didethresolver", + "serde", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1620,6 +1649,29 @@ dependencies = [ "tokio-rustls 0.24.1", ] +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1999,6 +2051,33 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lib-didethresolver" +version = "0.1.0" +source = "git+https://github.com/xmtp/didethresolver?branch=main#4bf3d4b539b6090fdd46367d3a76863f3c449e7f" +dependencies = [ + "async-trait", + "base64 0.21.7", + "bs58", + "chrono", + "ethers", + "hex", + "jsonrpsee", + "log", + "peg", + "percent-encoding", + "rand", + "rustc-hex", + "serde", + "sha3", + "smart-default", + "thiserror", + "tiny-keccak", + "tokio", + "tracing", + "url", +] + [[package]] name = "libc" version = "0.2.152" @@ -2335,6 +2414,33 @@ dependencies = [ "hmac", ] +[[package]] +name = "peg" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "400bcab7d219c38abf8bd7cc2054eb9bbbd4312d66f6a5557d572a203f646f61" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e61cce859b76d19090f62da50a9fe92bab7c2a5f09e183763559a2ac392c90" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bae92c60fa2398ce4678b98b2c4b5a7c61099961ca1fa305aec04a9ad28922" + [[package]] name = "pem" version = "1.1.1" @@ -2423,18 +2529,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", @@ -2682,7 +2788,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.4", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -2697,9 +2803,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -2727,6 +2833,17 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "registry" version = "0.1.0" +dependencies = [ + "async-trait", + "ethers", + "gateway-types", + "lib-didethresolver", + "log", + "rustc-hex", + "thiserror", + "tokio", + "tracing", +] [[package]] name = "reqwest" @@ -3279,6 +3396,17 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "socket2" version = "0.5.5" @@ -3396,9 +3524,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "svm-rs" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20689c7d03b6461b502d0b95d6c24874c7d24dea2688af80486a130a06af3b07" +checksum = "7ce290b5536ab2a42a61c9c6f22d6bfa8f26339c602aa62db4c978c95d1afc47" dependencies = [ "dirs", "fs2", @@ -3902,6 +4030,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4085,6 +4214,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4274,10 +4412,15 @@ dependencies = [ "ctor", "ethers", "futures", + "gateway-types", + "hex", "jsonrpsee", + "lib-didethresolver", "log", "rand", + "registry", "serde", + "serde_json", "thiserror", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index dae845e..756e93c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [workspace] members = [ -"xps-gateway", -"messaging", -"inbox", -"registry", + "xps-gateway", + "messaging", + "inbox", + "registry", + "gateway-types", ] exclude = [ ] @@ -12,3 +13,21 @@ exclude = [ ] # Make the feature resolver explicit. # See https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html#details resolver = "2" + +[workspace.dependencies] +log = "0.4" +tracing = "0.1" +tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] } +serde = "1.0" +serde_json = "1.0" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +async-trait = "0.1" +jsonrpsee = { version = "0.21", features = ["macros", "server", "client-core"] } +anyhow = "1.0" +thiserror = "1.0" +ethers = { version = "2", features = ["abigen"] } +ctor = "0.2" +lib-didethresolver = { git = "https://github.com/xmtp/didethresolver", package = "lib-didethresolver", branch = "main" } +gateway-types = { path = "./gateway-types" } +rustc-hex = "2.1" +hex = "0.4" diff --git a/Dockerfile b/Dockerfile index 4d0ff54..dcc012b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ ENV USER=xmtp RUN sudo apt update && sudo apt install -y pkg-config openssl libssl-dev +COPY --from=ghcr.io/xmtp/foundry:latest /usr/local/bin/anvil /usr/local/bin/anvil + ARG PROJECT=xps-gateway WORKDIR /workspaces/${PROJECT} COPY --chown=xmtp:xmtp . . diff --git a/gateway-types/Cargo.toml b/gateway-types/Cargo.toml new file mode 100644 index 0000000..5aea0be --- /dev/null +++ b/gateway-types/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "gateway-types" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde.workspace = true +lib-didethresolver.workspace = true diff --git a/gateway-types/src/lib.rs b/gateway-types/src/lib.rs new file mode 100644 index 0000000..b3a4208 --- /dev/null +++ b/gateway-types/src/lib.rs @@ -0,0 +1,22 @@ +//! Shared types between XPS Gateawy and client (libxmtp) + +use serde::{Deserialize, Serialize}; + +/// Address of the did:ethr Registry on Sepolia +pub const DID_ETH_REGISTRY: &str = "0xd1D374DDE031075157fDb64536eF5cC13Ae75000"; + +/// A message sent to a conversation +#[derive(Serialize, Deserialize)] +pub struct Message { + // Unique identifier for a conversation + #[serde(rename = "conversationId")] + pub conversation_id: Vec, + /// message content in bytes + pub payload: Vec, + /// Signature of V + pub v: Vec, + /// Signature of R + pub r: Vec, + /// Signature of S + pub s: Vec, +} diff --git a/inbox/src/lib.rs b/inbox/src/lib.rs index 7d12d9a..8b13789 100644 --- a/inbox/src/lib.rs +++ b/inbox/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/registry/Cargo.toml b/registry/Cargo.toml index 89c58fc..0fe1573 100644 --- a/registry/Cargo.toml +++ b/registry/Cargo.toml @@ -6,3 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +log.workspace = true +tracing.workspace = true +tokio.workspace = true +async-trait.workspace = true +ethers = { workspace = true, features = ["ws"] } +gateway-types.workspace = true +lib-didethresolver.workspace = true +rustc-hex.workspace = true +thiserror.workspace = true diff --git a/registry/src/error.rs b/registry/src/error.rs new file mode 100644 index 0000000..5d19b2b --- /dev/null +++ b/registry/src/error.rs @@ -0,0 +1,21 @@ +//! Error variants for Registry + +use std::num::TryFromIntError; + +use ethers::{ + contract::ContractError, + providers::{Middleware, ProviderError}, +}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContactOperationError { + #[error("Invalid address {0}")] + BadAddress(#[from] rustc_hex::FromHexError), + #[error(transparent)] + ContractError(#[from] ContractError), + #[error(transparent)] + ProviderError(#[from] ProviderError), + #[error("Error converting from int: {0}")] + IntConversion(#[from] TryFromIntError), +} diff --git a/registry/src/lib.rs b/registry/src/lib.rs index 7d12d9a..d66f9a7 100644 --- a/registry/src/lib.rs +++ b/registry/src/lib.rs @@ -1,14 +1,57 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +pub mod error; + +use std::str::FromStr; + +use ethers::{core::types::Signature, providers::Middleware, types::Address}; +use lib_didethresolver::{ + did_registry::DIDRegistry, + types::{Attribute, XmtpAttribute}, +}; + +use error::ContactOperationError; + +pub struct ContactOperations { + registry: DIDRegistry, } -#[cfg(test)] -mod tests { - use super::*; +impl ContactOperations +where + M: Middleware + 'static, +{ + /// Creates a new ContactOperations instance + pub fn new(registry: DIDRegistry) -> Self { + Self { registry } + } + + pub async fn revoke_installation( + &self, + did: String, + name: XmtpAttribute, + 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 attribute: [u8; 32] = Attribute::from(name).into(); + log::debug!( + "Revoking attribute {:#?}", + String::from_utf8_lossy(&attribute) + ); + + self.registry + .revoke_attribute_signed( + address, + signature.v.try_into()?, + signature.r.into(), + signature.s.into(), + attribute, + value.into(), + ) + .send() + .await? + .await?; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + Ok(()) } } diff --git a/xps-gateway/Cargo.toml b/xps-gateway/Cargo.toml index 890b394..7f777b8 100644 --- a/xps-gateway/Cargo.toml +++ b/xps-gateway/Cargo.toml @@ -7,22 +7,28 @@ resolver = "2" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -log = "0.4" -tracing = "0.1" -tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] } -serde = "1" -tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } -tokio-stream = { version = "0.1", features = ["net"] } -async-trait = "0.1" -jsonrpsee = { version = "0.21", features = ["macros", "server", "client-core"] } -anyhow = "1" -thiserror = "1" -ctor = "0.2" +log.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +serde.workspace = true +tokio.workspace = true +async-trait.workspace = true +jsonrpsee.workspace = true +anyhow.workspace = true +thiserror.workspace = true +ethers = { workspace = true, features = ["ws"] } +ctor.workspace = true +lib-didethresolver.workspace = true +gateway-types.workspace = true +hex.workspace = true clap = { version = "4.4.18", features = ["derive"] } -ethers = "2.0.11" rand = "0.8.5" +tokio-stream = { version = "0.1", features = ["net"] } +registry = { path = "../registry" } [dev-dependencies] -jsonrpsee = { version = "0.21", features = ["macros", "server", "client"] } +jsonrpsee = { workspace = true, features = ["macros", "server", "client"]} +tokio = { workspace = true, features = ["macros", "rt", "time"]} futures = "0.3" -tokio = { version = "1.34", features = ["macros", "rt", "time"] } +serde_json.workspace = true + diff --git a/xps-gateway/src/lib.rs b/xps-gateway/src/lib.rs index fa40437..c6b3e8a 100644 --- a/xps-gateway/src/lib.rs +++ b/xps-gateway/src/lib.rs @@ -3,9 +3,16 @@ pub mod types; mod util; use anyhow::Result; +use ethers::{ + abi::Address, + providers::{Provider, Ws}, +}; +use gateway_types::DID_ETH_REGISTRY; use jsonrpsee::server::Server; +use std::str::FromStr; pub use crate::rpc::{XpsMethods, XpsServer}; +use crate::types::GatewayContext; /// Entrypoint for the xps Gateway pub async fn run(host: String, port: u16) -> Result<()> { @@ -16,7 +23,15 @@ pub async fn run(host: String, port: u16) -> Result<()> { // a port of 0 allows the OS to choose an open port let server = Server::builder().build(server_addr).await?; let addr = server.local_addr()?; - let handle = server.start(XpsMethods::new().into_rpc()); + + let registry_contract = Address::from_str(DID_ETH_REGISTRY)?; + let provider = Provider::::connect("wss://ethereum-sepolia.publicnode.com") + .await + .unwrap(); + + let context = GatewayContext::new(registry_contract, provider).await?; + let xps_methods = rpc::XpsMethods::new(&context); + let handle = server.start(xps_methods.into_rpc()); log::info!("Server Started at {addr}"); handle.stopped().await; diff --git a/xps-gateway/src/rpc/api.rs b/xps-gateway/src/rpc/api.rs index da4f3aa..14055f0 100644 --- a/xps-gateway/src/rpc/api.rs +++ b/xps-gateway/src/rpc/api.rs @@ -1,9 +1,11 @@ //! Trait Interface Definitions for XPS JSON-RPC +use ethers::core::types::Signature; +use ethers::prelude::*; use jsonrpsee::{proc_macros::rpc, types::ErrorObjectOwned}; -use crate::types::Message; -use ethers::prelude::*; +use gateway_types::Message; +use lib_didethresolver::types::XmtpAttribute; /// XPS JSON-RPC Interface Methods #[rpc(server, client, namespace = "xps")] @@ -12,6 +14,116 @@ pub trait Xps { #[method(name = "sendMessage")] async fn send_message(&self, _message: Message) -> Result<(), ErrorObjectOwned>; + /// # Documentation for JSON RPC Endpoint: `revoke_installation` + /// + /// ## JSON RPC Endpoint Specification + /// + /// #### Request: + /// + /// - **Method:** `POST` + /// - **URL:** `/rpc/v1/revokeInstallation` + /// - **Headers:** + /// - `Content-Type: application/json` + /// - **Body:** + /// - **JSON Object:** + /// - `jsonrpc`: `"2.0"` + /// - `method`: `"revokeInstallation"` + /// - `params`: Array (optional parameters as required) + /// - `id`: Request identifier (integer or string) + /// + /// ### Endpoint: `revokeInstallation` + /// + /// #### Description + /// The `revokeInstallation` endpoint is responsible for removing the contact bundle for the XMTP device installation. The request must be made to a valid did with an XMTP profile. + /// + /// #### Request + /// The request for this endpoint should contain the necessary information to authenticate and validate the installation request including the wallet signed payload + /// + /// ##### Parameters: + /// - `DID` (string): Unique XMTP identifier for the user requesting the revocation. + /// - `name` (string): Unique identifier naming bundled contents variant. + /// - `value` (bytes): Installation bundle bytes payload + /// - `V` (int): signature V + /// - `R` (bytes): signature R + /// - `S` (bytes): signature S + /// + /// ##### Example Request: + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "method": "revokeInstallation", + /// "params": { + /// "did": "12345", + /// "name": "xmtp/contact_installation", + /// "value": "#######", + /// "signature": { + /// "V": "12345", + /// "R": "valueR", + /// "S": "valueS" + /// } + /// }, + /// "id": 1 + /// } + /// ``` + /// + /// #### Response + /// The response will indicate whether the installation is revoked and may include additional information or instructions. + /// + /// ##### Result Fields: + /// - `status` (string): The status of the request, e.g., 'completed'. + /// - `message` (string, optional): Additional information or reason for the decision. + /// - `tx` (string, optional): transaction receipt + /// + /// ##### Example Response: + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "result": { + /// "status": "completed", + /// "message": "Installation revoked.", + /// "tx": "" + /// }, + /// "id": 1 + /// } + /// ``` + /// + /// #### Error Handling + /// In case of an error, the response will include an error object with details. + /// + /// ##### Error Object Fields: + /// - `code` (integer): Numeric code representing the error type. + /// - `message` (string): Description of the error. + /// + /// ##### Example Error Response: + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "error": { + /// "code": 403, + /// "message": "User not authorized for installation." + /// }, + /// "id": 1 + /// } + /// ``` + + /// removes the contact bundle for the XMTP device installation. Request must be made to a + /// valid DID with an XMTP profile. + /// + /// # Arguments + /// + /// * `did` - the DID of the XMTP device installation + /// * `name` - the name of the contact bundle variant + /// * `value` - the value of the contact bundle + /// * `signature` - the signature of the contact bundle + #[method(name = "revokeInstallation")] + async fn revoke_installation( + &self, + did: String, + name: XmtpAttribute, + value: Vec, + signature: Signature, + ) -> Result<(), ErrorObjectOwned>; + /// # Documentation for JSON RPC Endpoint: `status` /// ## Overview diff --git a/xps-gateway/src/rpc/methods.rs b/xps-gateway/src/rpc/methods.rs index a5d2a6a..b44f46c 100644 --- a/xps-gateway/src/rpc/methods.rs +++ b/xps-gateway/src/rpc/methods.rs @@ -1,37 +1,38 @@ //! Interface Implementations for XPS JSON-RPC +use crate::types::{GatewayContext, GatewaySigner}; + use super::api::*; use jsonrpsee::types::error::ErrorCode; use async_trait::async_trait; use ethers::prelude::*; +use ethers::{core::types::Signature, providers::Middleware}; use jsonrpsee::types::ErrorObjectOwned; +use lib_didethresolver::types::XmtpAttribute; use rand::{rngs::StdRng, SeedableRng}; +use thiserror::Error; -use crate::types::Message; +use gateway_types::Message; +use registry::{error::ContactOperationError, ContactOperations}; /// Gateway Methods for XPS -pub struct XpsMethods { +pub struct XpsMethods { + contact_operations: ContactOperations>, pub wallet: LocalWallet, } -impl XpsMethods { - /// Create a new instance of the XpsMethods struct - pub fn new() -> Self { +impl XpsMethods

{ + pub fn new(context: &GatewayContext

) -> Self { Self { + contact_operations: ContactOperations::new(context.registry.clone()), wallet: LocalWallet::new(&mut StdRng::from_entropy()), } } } -impl Default for XpsMethods { - fn default() -> Self { - Self::new() - } -} - #[async_trait] -impl XpsServer for XpsMethods { +impl XpsServer for XpsMethods

{ async fn send_message(&self, _message: Message) -> Result<(), ErrorObjectOwned> { //TODO: Stub for sendMessage, ref: [discussion](https://github.com/xmtp/xps-gateway/discussions/11) log::debug!("xps_sendMessage called"); @@ -43,7 +44,41 @@ impl XpsServer for XpsMethods { Ok("OK".to_string()) } + async fn revoke_installation( + &self, + did: String, + name: XmtpAttribute, + value: Vec, + signature: Signature, + ) -> Result<(), ErrorObjectOwned> { + log::debug!("xps_revokeInstallation called"); + self.contact_operations + .revoke_installation(did, name, value, signature) + .await + .map_err(RpcError::from)?; + + Ok(()) + } + async fn wallet_address(&self) -> Result { Ok(self.wallet.address()) } } + +/// Error types for DID Registry JSON-RPC +#[derive(Debug, Error)] +enum RpcError { + /// A public key parameter was invalid + #[error(transparent)] + ContactOperation(#[from] ContactOperationError), +} + +impl From> for ErrorObjectOwned { + fn from(error: RpcError) -> Self { + match error { + RpcError::ContactOperation(c) => { + ErrorObjectOwned::owned(-31999, c.to_string(), None::<()>) + } + } + } +} diff --git a/xps-gateway/src/types.rs b/xps-gateway/src/types.rs index 49a96d3..7d8eac2 100644 --- a/xps-gateway/src/types.rs +++ b/xps-gateway/src/types.rs @@ -1,17 +1,49 @@ -use serde::{Deserialize, Serialize}; - -/// A message sent to a conversation -#[derive(Serialize, Deserialize)] -pub struct Message { - // Unique identifier for a conversation - #[serde(rename = "conversationId")] - pub conversation_id: Vec, - /// message content in bytes - pub payload: Vec, - /// Signature of V - pub v: Vec, - /// Signature of R - pub r: Vec, - /// Signature of S - pub s: Vec, +use std::sync::Arc; + +use anyhow::Error; +use ethers::{ + middleware::SignerMiddleware, providers::Middleware, signers::LocalWallet, types::Address, +}; +use lib_didethresolver::did_registry::DIDRegistry; +use rand::{rngs::StdRng, SeedableRng}; + +pub type GatewaySigner

= SignerMiddleware; + +pub struct GatewayContext { + pub registry: DIDRegistry>, + pub signer: Arc>, +} + +impl GatewayContext

{ + pub async fn new(registry: Address, provider: P) -> Result { + let wallet = LocalWallet::new(&mut StdRng::from_entropy()); + let signer = + Arc::new(SignerMiddleware::new_with_provider_chain(provider, wallet.clone()).await?); + let registry = DIDRegistry::new(registry, signer.clone()); + Ok(Self { registry, signer }) + } +} + +#[cfg(test)] +mod tests { + use ethers::{providers::Provider, types::U64}; + use std::str::FromStr; + + use super::*; + + #[tokio::test] + async fn test_gateway_constructor() { + let (provider, mock) = Provider::mocked(); + mock.push(U64::from(2)).unwrap(); + + let gateway = GatewayContext::new( + Address::from_str("0x0000000000000000000000000000000000000000").unwrap(), + provider, + ) + .await + .unwrap(); + + assert!(gateway.registry.address().is_zero()); + assert!(gateway.signer.is_signer().await); + } } diff --git a/xps-gateway/tests/integration_test.rs b/xps-gateway/tests/integration_test.rs index a29e8fc..58d3f61 100644 --- a/xps-gateway/tests/integration_test.rs +++ b/xps-gateway/tests/integration_test.rs @@ -1,82 +1,130 @@ +mod integration_util; + use anyhow::Error; -use jsonrpsee::{ - server::Server, - ws_client::{WsClient, WsClientBuilder}, + +use ethers::signers::LocalWallet; +use lib_didethresolver::{ + did_registry::RegistrySignerExt, + types::{DidUrl, KeyEncoding, XmtpAttribute, XmtpKeyPurpose}, }; +use xps_gateway::rpc::XpsClient; + +use ethers::types::{Address, U256}; +use gateway_types::Message; -use futures::future::FutureExt; -use std::{future::Future, time::Duration}; -use tokio::time::timeout as timeout_tokio; - -use xps_gateway::{rpc::XpsClient, types::Message, XpsMethods, XpsServer}; - -const TEST_TIMEOUT: Duration = Duration::from_secs(10); -pub const SERVER_HOST: &str = "127.0.0.1"; - -#[cfg(test)] -mod it { - use ethers::abi::Address; - - use super::*; - - #[tokio::test] - async fn test_say_hello() -> Result<(), Error> { - with_xps_client(None, |client| async move { - let result = client.status().await?; - assert_eq!(result, "OK"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_fail_send_message() -> Result<(), Error> { - with_xps_client(None, |client| async move { - let message = Message { - conversation_id: b"abcdefg".iter().map(|c| *c as u8).collect(), - payload: b"Hello World".iter().map(|c| *c as u8).collect(), - v: vec![], - r: vec![], - s: vec![], - }; - let result = client.send_message(message).await; - assert!(result.is_err()); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_wallet_address() -> Result<(), Error> { - with_xps_client(None, |client| async move { - let result = client.wallet_address().await?; - assert_ne!(result, Address::zero()); - Ok(()) - }) - .await - } +use integration_util::*; + +#[tokio::test] +async fn test_say_hello() -> Result<(), Error> { + with_xps_client(None, |client, _, _, _| async move { + let result = client.status().await?; + assert_eq!(result, "OK"); + Ok(()) + }) + .await } -async fn with_xps_client(timeout: Option, f: F) -> Result -where - F: FnOnce(WsClient) -> R + 'static + Send, - R: Future> + FutureExt + Send + 'static, -{ - let server_addr = format!("{}:{}", SERVER_HOST, 0); - let server = Server::builder().build(server_addr).await.unwrap(); - let addr = server.local_addr().unwrap(); - let handle = server.start(XpsMethods::new().into_rpc()); - let client = WsClientBuilder::default() - .build(&format!("ws://{addr}")) - .await - .unwrap(); - let result = timeout_tokio(timeout.unwrap_or(TEST_TIMEOUT), f(client)).await; - - handle.stop().unwrap(); - handle.stopped().await; - - match result { - Ok(v) => v, - Err(_) => panic!("Test timed out"), - } +#[tokio::test] +async fn test_fail_send_message() -> Result<(), Error> { + with_xps_client(None, |client, _, _, _| async move { + let message = Message { + conversation_id: (b"abcdefg").to_vec(), + payload: (b"Hello World").to_vec(), + v: vec![], + r: vec![], + s: vec![], + }; + let result = client.send_message(message).await; + assert!(result.is_err()); + Ok(()) + }) + .await +} + +#[tokio::test] +async fn test_wallet_address() -> Result<(), Error> { + with_xps_client(None, |client, _, _, _| async move { + let result = client.wallet_address().await?; + assert_ne!(result, Address::zero()); + Ok(()) + }) + .await +} + +#[tokio::test] +async fn test_revoke_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 validity = U256::from(604_800); + let signature = wallet + .sign_attribute(&context.registry, name, value.to_vec(), validity) + .await?; + + let attr = context.registry.set_attribute_signed( + me.address(), + signature.v.try_into().unwrap(), + signature.r.into(), + signature.s.into(), + name, + value.into(), + validity, + ); + attr.send().await?.await?; + + let doc = resolver + .resolve_did(me.address(), None) + .await + .unwrap() + .document; + assert_eq!( + doc.verification_method[1].id, + DidUrl::parse(format!( + "did:ethr:0x{}?meta=installation#xmtp-0", + hex::encode(me.address()) + )) + .unwrap() + ); + + let signature = wallet + .sign_revoke_attribute(&context.registry, name, value.to_vec()) + .await?; + + let attribute = XmtpAttribute { + purpose: XmtpKeyPurpose::Installation, + encoding: KeyEncoding::Hex, + }; + + client + .revoke_installation( + format!("0x{}", hex::encode(me.address())), + attribute, + value.to_vec(), + signature, + ) + .await?; + + let doc = resolver + .resolve_did(me.address(), None) + .await + .unwrap() + .document; + + log::debug!("{}", serde_json::to_string_pretty(&doc).unwrap()); + + assert_eq!( + doc.verification_method[0].id, + DidUrl::parse(format!( + "did:ethr:0x{}#controller", + hex::encode(me.address()) + )) + .unwrap() + ); + assert_eq!(doc.verification_method.len(), 1); + + Ok(()) + }) + .await } diff --git a/xps-gateway/tests/integration_util/mod.rs b/xps-gateway/tests/integration_util/mod.rs new file mode 100644 index 0000000..492be3f --- /dev/null +++ b/xps-gateway/tests/integration_util/mod.rs @@ -0,0 +1,147 @@ +use anyhow::Error; +use jsonrpsee::{ + server::Server, + ws_client::{WsClient, WsClientBuilder}, +}; + +use ethers::{ + abi::Address, + core::{types::TransactionRequest, utils::Anvil}, + middleware::Middleware, + middleware::SignerMiddleware, + providers::{Provider, Ws}, + signers::{LocalWallet, Signer as _}, + utils::AnvilInstance, +}; +use futures::future::FutureExt; +use lib_didethresolver::{did_registry::DIDRegistry, Resolver}; +use std::{ + future::Future, + sync::{Arc, Once}, + time::Duration, +}; +use tokio::time::timeout as timeout_tokio; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; + +use xps_gateway::{ + types::{GatewayContext, GatewaySigner}, + XpsMethods, XpsServer, +}; + +const TEST_TIMEOUT: Duration = Duration::from_secs(20); +pub const SERVER_HOST: &str = "127.0.0.1"; + +pub async fn with_xps_client(timeout: Option, f: F) -> Result +where + F: FnOnce( + WsClient, + GatewayContext>, + Resolver>>>, + Arc, + ) -> R + + 'static + + Send, + R: Future> + FutureExt + Send + 'static, +{ + init_test_logging(); + let anvil = Anvil::new().args(vec!["--base-fee", "100"]).spawn(); + log::debug!("Anvil spawned at {}", anvil.ws_endpoint()); + let registry_address = deploy_to_anvil(&anvil).await; + log::debug!("Contract deployed at {}", registry_address); + let provider = Provider::::connect(anvil.ws_endpoint()) + .await + .unwrap() + .interval(std::time::Duration::from_millis(10u64)); + + let context = GatewayContext::new(registry_address, provider).await?; + + let accounts = context.signer.get_accounts().await?; + let from = accounts[0]; + let tx = TransactionRequest::new() + .to(context.signer.address()) + .value(5_000_000_000_000_000_000_000_u128) + .from(from); + context.signer.send_transaction(tx, None).await?.await?; + let balance = context + .signer + .get_balance(context.signer.address(), None) + .await?; + log::debug!("Gateway Balance is {}", balance); + + let resolver = Resolver::new(context.signer.clone(), registry_address) + .await + .unwrap(); + + let server = Server::builder() + .build(SERVER_HOST.to_string() + ":0") + .await + .unwrap(); + let addr = server.local_addr().unwrap(); + let handle = server.start(XpsMethods::new(&context).into_rpc()); + let client = WsClientBuilder::default() + .build(&format!("ws://{addr}")) + .await + .unwrap(); + let anvil = Arc::new(anvil); + let result = timeout_tokio( + timeout.unwrap_or(TEST_TIMEOUT), + f(client, context, resolver, anvil.clone()), + ) + .await; + + handle.stop().unwrap(); + handle.stopped().await; + + match result { + Ok(v) => v, + Err(_) => panic!("Test timed out"), + } +} + +async fn deploy_to_anvil(anvil: &AnvilInstance) -> Address { + let wallet: LocalWallet = anvil.keys()[0].clone().into(); + let client = client(&anvil, wallet).await; + + let registry = DIDRegistry::deploy(client.clone(), ()) + .unwrap() + .gas_price(100) + .send() + .await + .unwrap(); + + registry.address() +} + +async fn client( + anvil: &AnvilInstance, + wallet: LocalWallet, +) -> Arc, LocalWallet>> { + let provider = Provider::::connect(anvil.ws_endpoint()) + .await + .unwrap() + .interval(std::time::Duration::from_millis(10u64)); + Arc::new(SignerMiddleware::new( + provider, + wallet.with_chain_id(anvil.chain_id()), + )) +} + +pub async fn get_user( + anvil: &AnvilInstance, + index: usize, +) -> Arc, LocalWallet>> { + let wallet: LocalWallet = anvil.keys()[index].clone().into(); + client(&anvil, wallet).await +} + +static INIT: Once = Once::new(); + +fn init_test_logging() { + INIT.call_once(|| { + let fmt = fmt::layer().compact(); + Registry::default() + .with(EnvFilter::from_default_env()) + .with(fmt) + .init() + }) +}