diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f4719eb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,44 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'didethresolver'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + ], + "filter": { + "name": "lib-didethresolver", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'resolver'", + "cargo": { + "args": [ + "build", + "--bin=resolver", + "--package=resolver" + ], + "filter": { + "name": "resolver", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9fce387..8d28f45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2587,7 +2587,6 @@ dependencies = [ "smart-default", "surf", "thiserror", - "tiny-keccak", "tokio", "tokio-test", "tracing", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 51c2988..16af6c1 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -38,7 +38,6 @@ smart-default = "0.7" sha3 = "0.10" peg = "0.8" rustc-hex = "2.1" -tiny-keccak = { version = "2.0.2", default-features = false } [dev-dependencies] tracing-subscriber.workspace = true diff --git a/lib/src/error.rs b/lib/src/error.rs index bc9e58a..4994555 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -23,8 +23,6 @@ pub enum ResolverError { pub enum DidError { #[error("Parsing of ethr:did failed, {0}")] Parse(#[from] peg::error::ParseError), - #[error(transparent)] - Url(#[from] url::ParseError), } /// Errors originating during the construction of a ethr:did document [`EthrBuilder`](crate::types::EthrBuilder) diff --git a/lib/src/resolver.rs b/lib/src/resolver.rs index 05f5eac..60c43e2 100644 --- a/lib/src/resolver.rs +++ b/lib/src/resolver.rs @@ -128,7 +128,7 @@ impl Resolver { history: Vec<(DIDRegistryEvents, LogMeta)>, ) -> Result> { let mut base_document = DidDocument::ethr_builder(); - base_document.public_key(&public_key)?; + base_document.account_address(&public_key)?; let current_block = self .signer diff --git a/lib/src/resolver/did_registry.rs b/lib/src/resolver/did_registry.rs index a37f4ae..7680569 100644 --- a/lib/src/resolver/did_registry.rs +++ b/lib/src/resolver/did_registry.rs @@ -8,8 +8,8 @@ use ethers::{ providers::Middleware, signers::{LocalWallet, Signer}, types::{Signature, H256, U256, U64}, + utils::keccak256, }; -use tiny_keccak::{Hasher, Keccak}; pub use self::did_registry::*; @@ -228,19 +228,6 @@ async fn sign_typed_data( Ok(signature) } -/// Compute the Keccak-256 hash of input bytes. -/// -/// Note that strings are interpreted as UTF-8 bytes, -pub fn keccak256>(bytes: T) -> [u8; 32] { - let mut output = [0u8; 32]; - - let mut hasher = Keccak::v256(); - hasher.update(bytes.as_ref()); - hasher.finalize(&mut output); - - output -} - #[cfg(test)] mod test { use super::*; diff --git a/lib/src/types/did_parser.rs b/lib/src/types/did_parser.rs index b79f826..af228da 100644 --- a/lib/src/types/did_parser.rs +++ b/lib/src/types/did_parser.rs @@ -7,16 +7,41 @@ use crate::types::*; pub use did_ethr_attribute_parser::attribute as parse_attribute; pub use did_ethr_delegate_parser::delegate as parse_delegate; pub use did_ethr_parser::ethr_did as parse_ethr_did; +pub use did_ethr_parser::ethr_did_url as parse_ethr_did_url; peg::parser! { grammar did_ethr_parser() for str { + /// parses the `did` with a url path part of a [DID-URL](https://www.w3.org/TR/did-core/#did-syntax) + /// # Example + /// ```rust + /// use lib_didethresolver::types::{DidUrl, Did, Method, Network, Account, parse_ethr_did_url}; + /// use ethers::types::Address; + /// let parsed = parse_ethr_did_url("did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a/abc").unwrap(); + /// assert_eq!( + /// parsed, + /// DidUrl { + /// did: Did { + /// method: Method::Ethr, + /// network: Network::Mainnet, + /// account: Account::Address(Address::from_slice( + /// &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap())), + /// }, + /// path: Some("/abc".to_string()), + /// query: None, + /// fragment: None + /// }); + /// ``` + /// + pub rule ethr_did_url() -> DidUrl + = did:ethr_did() path:did_path() query:did_query() fragment:did_fragment() { DidUrl { did, path, query, fragment } } + /// parses the `did` part of a [DID-URL](https://www.w3.org/TR/did-core/#did-syntax) /// /// # Example /// ```rust /// use lib_didethresolver::types::{Did, Method, Network, Account, parse_ethr_did}; /// use ethers::types::Address; - /// let parsed = parse_ethr_did("ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + /// let parsed = parse_ethr_did("did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); /// assert_eq!( /// parsed, /// Did { @@ -28,30 +53,90 @@ peg::parser! { /// }); /// ``` pub rule ethr_did() -> Did - = method:method() ":" network:network()? account:account() { Did { method, network: network.unwrap_or_default(), account } } + = did() ":" method:method() ":" network:(chain_id() / network())? account:account() { Did { method, network: network.unwrap_or_default(), account } } + + rule did() = "did" / expected!("only `did` is supported") rule method() -> Method = "ethr" { Method::Ethr } / expected!("the only supported method is `ethr`") + rule chain_id() -> Network + = "0" i("x") digits:$(HEXDIG()+) ":" { Network::from(digits) } + rule network() -> Network - // chain id networks - = "0" i("x") digits:$(hex_digit()+) ":" { Network::from(digits) } - // named networks - / "mainnet:" { Network::Mainnet } - / "sepolia:" { Network::Sepolia } - / expected!("the only supported networks are `mainnet`, `sepolia`, and chain id") + = n:$(ALPHA()+) ":" { Network::from(n) } rule account() -> Account - = "0" i("x") digits:$(hex_digit()*<40>) { Account::Address(Address::from_slice(&hex::decode(digits).unwrap())) } - / "0" i("x") digits:$(hex_digit()*<66>) { Account::HexKey(hex::decode(digits).unwrap()) } - - rule hex_digit() -> String - = digits:$(['0'..='9' | 'a'..='f' | 'A'..='F']) { digits.to_string() } + = "0" i("x") digits:$(HEXDIG()*<40>) { Account::Address(Address::from_slice(&hex::decode(digits).unwrap())) } + / digits:$(HEXDIG()*<128>) { Account::HexKey(hex::decode(digits).unwrap()) } // case insensitive rule (see https://github.com/kevinmehall/rust-peg/issues/216) rule i(literal: &'static str) = input:$([_]*<{literal.len()}>) {? if input.eq_ignore_ascii_case(literal) { Ok(()) } else { Err(literal) } } + + // parses a url path according to the RFC 3986 definition + // https://www.rfc-editor.org/rfc/rfc3986#section-3.3 + + rule did_path() -> Option = path:$(path_rootless() / path_abempty() / path_absolute() / path_noscheme() / path_empty()) (&"?" / ![_])? + { if path.is_empty() { None } else { Some(path.to_string()) } } + + rule path_abempty() = ( "/" segment() )* + + rule path_absolute() = "/" ( segment_nz() ( "/" segment() )* )+ + + rule path_rootless() = segment_nz() ( "/" segment() )* + + rule path_noscheme() = segment_nz_nc() ( "/" segment() )* + + rule segment() = pchar()* + + rule segment_nz() = pchar()+ + + rule segment_nz_nc() = ( unreserved() / pct_encoded() / sub_delims() / "@" )+ + + rule pchar() = unreserved() / pct_encoded() / sub_delims() / ":" / "@" + + rule unreserved() = ALPHA() / DIGIT() / "-" / "." / "_" / "~" + + rule pct_encoded() = "%" HEXDIG() HEXDIG() + + rule sub_delims() = "!" / "$" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" / "&" + + rule path_empty() = "" + + rule qchar() = unreserved() / pct_encoded() / ":" / "@" / "/" / "?" + + rule ALPHA() -> char + = ['a'..='z' | 'A'..='Z'] + + rule DIGIT() -> char + = ['0'..='9'] + + rule HEXDIG() -> char + = ['0'..='9' | 'a'..='f' | 'A'..='F'] + + // parses a query according to the RFC 3986 definition + // https://www.rfc-editor.org/rfc/rfc3986#section-3.4 + rule did_query() -> Option> = query()? + + rule query() -> Vec<(String, String)> = "?" q:query_assignment() ** "&" (&"#" / ![_]) { q } + + rule query_assignment() -> (String, String) = n:$(query_name()) "=" v:$(query_value()) { (n.to_string(), v.to_string()) } + + rule query_name() = qchar()+ + + rule query_value() = qchar()* + + // parses a fragment according to the RFC 3986 definition + // https://www.rfc-editor.org/rfc/rfc3986#section-3.5 + rule did_fragment() -> Option = fragment:$(fragment()?) + { + let fragment = fragment.strip_prefix('#').unwrap_or(""); + if fragment.is_empty() { None } else { Some(fragment.to_string()) } + } + + rule fragment() = "#" ( pchar() / "/" / "?" )* ![_] } } @@ -241,10 +326,34 @@ mod tests { ); } + #[test] + fn test_did_ethr_is_required() { + let parsed = parse_ethr_did("did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a"); + assert!(parsed.is_ok()); + let parsed = parse_ethr_did("ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a"); + assert!(parsed.is_err()); + } + + #[test] + fn test_ethr_network_sepolia() { + let parsed = + parse_ethr_did("did:ethr:sepolia:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + assert_eq!( + parsed, + Did { + method: Method::Ethr, + network: Network::Sepolia, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + } + ); + } + #[test] fn test_ethr_method_parser() { let parsed = - parse_ethr_did("ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + parse_ethr_did("did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); assert_eq!( parsed, Did { @@ -257,7 +366,7 @@ mod tests { ); // Mainnet is default - let parsed = parse_ethr_did("ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + let parsed = parse_ethr_did("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); assert_eq!( parsed, Did { @@ -270,7 +379,7 @@ mod tests { ); let parsed = - parse_ethr_did("ethr:0x01:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + parse_ethr_did("did:ethr:0x01:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); assert_eq!( parsed, Did { @@ -303,4 +412,263 @@ mod tests { "error at 1:1: expected the only supported delegate types are `sigAuth` and `veriKey`" ); } + + #[test] + fn test_ethr_did_parse_url() { + let parsed = parse_ethr_did_url( + "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a/location/1/2:/3", + ) + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: Some("/location/1/2:/3".to_string()), + query: None, + fragment: None + } + ); + } + + #[test] + fn test_ethr_did_parse_url_1() { + let parsed = + parse_ethr_did_url("did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a/") + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: Some("/".to_string()), + query: None, + fragment: None + } + ); + } + + #[test] + fn test_ethr_did_parse_url_empty_path() { + let parsed = + parse_ethr_did_url("did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a") + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: None, + query: None, + fragment: None + } + ); + } + + #[test] + fn test_ethr_did_parse_url_query() { + let parsed = parse_ethr_did_url( + "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a?a=????&c=d", + ) + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: None, + query: Some(vec![ + ("a".to_string(), "????".to_string()), + ("c".to_string(), "d".to_string()) + ]), + fragment: None + } + ); + } + + #[test] + fn test_ethr_did_parse_url_fragment() { + let parsed = parse_ethr_did_url( + "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a#section3_5", + ) + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: None, + query: None, + fragment: Some("section3_5".to_string()) + } + ); + } + + #[test] + fn test_ethr_did_parse_url_path_query() { + let parsed = parse_ethr_did_url( + "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a/:1/:2/:3/?a=b&c=d", + ) + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: Some("/:1/:2/:3/".to_string()), + query: Some(vec![ + ("a".to_string(), "b".to_string()), + ("c".to_string(), "d".to_string()) + ]), + fragment: None, + } + ); + } + + #[test] + fn test_ethr_did_parse_url_path_fragment() { + let parsed = parse_ethr_did_url( + "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a/a/b/c#section3_5", + ) + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: Some("/a/b/c".to_string()), + query: None, + fragment: Some("section3_5".to_string()) + } + ); + } + + #[test] + fn test_ethr_did_parse_url_query_fragment() { + let parsed = parse_ethr_did_url( + "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a?a=b&c=d#section3_5", + ) + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: None, + query: Some(vec![ + ("a".to_string(), "b".to_string()), + ("c".to_string(), "d".to_string()) + ]), + fragment: Some("section3_5".to_string()) + } + ); + } + + #[test] + fn test_ethr_did_parse_url_path_query_fragment() { + let parsed = parse_ethr_did_url( + "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a/a/b/c?x=y&z1=#section3_5", + ) + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap() + )) + }, + path: Some("/a/b/c".to_string()), + query: Some(vec![ + ("x".to_string(), "y".to_string()), + ("z1".to_string(), "".to_string()) + ]), + fragment: Some("section3_5".to_string()) + } + ); + } + + #[test] + fn test_ethr_did_parse_url_query_chars() { + let parsed = parse_ethr_did_url( + "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a?a=b&c=d:/?@", + ) + .unwrap(); + assert_eq!( + parsed, + DidUrl { + did: Did { + method: Method::Ethr, + network: Network::Mainnet, + account: Account::Address(Address::from_slice( + &hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap(), + )) + }, + path: None, + query: Some(vec![ + ("a".to_string(), "b".to_string()), + ("c".to_string(), "d:/?@".to_string()) + ]), + fragment: None + } + ); + } + + #[test] + fn test_ethr_did_parse_url_public_key() { + let parsed = parse_ethr_did_url("did:ethr:79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8/a/b/c#tothesection").unwrap(); + assert_eq!(parsed.did.method, Method::Ethr); + assert_eq!(parsed.did.network, Network::Mainnet); + assert_eq!( + Address::from_slice( + &hex::decode(&"0x7e5f4552091a69125d5dfcb7b8c2659029395bdf"[2..]).unwrap() + ), + parsed.did.account.into() + ); + assert_eq!(parsed.path, Some("/a/b/c".to_string())); + assert_eq!(parsed.query, None); + assert_eq!(parsed.fragment, Some("tothesection".to_string())); + } } diff --git a/lib/src/types/did_url.rs b/lib/src/types/did_url.rs index 34f103c..3eca68b 100644 --- a/lib/src/types/did_url.rs +++ b/lib/src/types/did_url.rs @@ -1,20 +1,42 @@ //! Convenience Wrapper around [`Url`] for DID URIs according to the [DID Spec](https://www.w3.org/TR/did-core/#did-syntax) -use ethers::types::Address; use serde::{Deserialize, Serialize, Serializer}; use smart_default::SmartDefault; -use url::Url; -use super::parse_ethr_did; +use ethers::{types::Address, utils::keccak256}; + +use super::parse_ethr_did_url; use crate::error::DidError; +/** + * Converts a public key to an ethereum address. The last 20 bytes + * of the keccak256 hash of the public key is returned + */ +fn public_key_to_address(key: &[u8]) -> String { + let keydigest = keccak256(key); + let address = &keydigest[12..]; + format!("0x{}", hex::encode(address)).to_string() +} + +/** + * Convert an Ethereum address string into a hex string + * + * Returns the hex string without the 0x prefix if it exists + */ +fn as_hex_digits(s: &str) -> &str { + if s.starts_with("0x") { + return s.strip_prefix("0x").unwrap(); + } + s +} + /// A DID URL, based on the did specification, [DID URL Syntax](https://www.w3.org/TR/did-core/#did-url-syntax) /// Currently only supports did:ethr: [did-ethr](https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md) #[derive(Debug, Clone, PartialEq, Eq)] pub struct DidUrl { pub did: Did, - pub path: String, - pub query: Option, + pub path: Option, + pub query: Option>, pub fragment: Option, } @@ -50,11 +72,45 @@ pub enum Account { HexKey(Vec), } +impl From
for Account { + fn from(addr: Address) -> Self { + Account::Address(addr) + } +} + +impl From> for Account { + fn from(key: Vec) -> Self { + Account::HexKey(key) + } +} + +impl From<&str> for Account { + fn from(s: &str) -> Self { + if s.starts_with("0x") { + Account::Address(Address::from_slice(&hex::decode(as_hex_digits(s)).unwrap())) + } else { + Account::HexKey(hex::decode(s).unwrap()) + } + } +} + +impl From for Address { + fn from(account: Account) -> Self { + match account { + Account::Address(addr) => addr, + Account::HexKey(key) => { + let addr = public_key_to_address(&key); + Address::from_slice(&hex::decode(as_hex_digits(&addr)).unwrap()) + } + } + } +} + impl ToString for Account { fn to_string(&self) -> String { match self { Account::Address(addr) => format!("0x{}", hex::encode(addr.as_bytes())), - Account::HexKey(key) => format!("0x{}", hex::encode(key)), + Account::HexKey(key) => hex::encode(key).to_string(), } } } @@ -96,10 +152,8 @@ impl ToString for Network { } } -impl<'a> From<&'a str> for Network { - fn from(chain_id: &'a str) -> Network { - let chain_id = usize::from_str_radix(chain_id, 16).expect("String must be valid Hex"); - +impl From for Network { + fn from(chain_id: usize) -> Network { match chain_id { 1 => Network::Mainnet, #[allow(deprecated)] @@ -110,6 +164,22 @@ impl<'a> From<&'a str> for Network { } } +impl<'a> From<&'a str> for Network { + fn from(network: &'a str) -> Network { + match network.to_lowercase().as_str() { + "mainnet" => Network::Mainnet, + #[allow(deprecated)] + "goerli" => Network::Goerli, + "sepolia" => Network::Sepolia, + _ => { + let chain_id = usize::from_str_radix(as_hex_digits(network), 16) + .expect("String must be valid Hex"); + Network::from(chain_id) + } + } + } +} + impl DidUrl { /// Parses a Decentralized Identifier (DID) URI string. /// @@ -129,7 +199,7 @@ impl DidUrl { /// /// let did_url = "did:not:123"; /// let did_url = DidUrl::parse(did_url).unwrap_err(); - /// assert_eq!(did_url.to_string(), "Parsing of ethr:did failed, error at 1:1: expected one of \"ethr\", the only supported method is `ethr`"); + /// assert_eq!(did_url.to_string(), "Parsing of ethr:did failed, error at 1:5: expected one of \"ethr\", the only supported method is `ethr`"); /// ``` /// ``` /// use lib_didethresolver::types::DidUrl; @@ -143,33 +213,13 @@ impl DidUrl { /// components (method name, method-specific ID) do not conform to the expected DID structure. /// pub fn parse>(input: S) -> Result { - let url = Url::parse(input.as_ref())?; - - // Note that `url.path()` will return an incorrect path from did url - // For regular URL("http://w.a.b/path"), the it only returns the string from the first '/' (/path) - // But for did url, it incorrectly returns the content before the first '/' (ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a/path) - let mut split = url.path().split('/'); - let did = if let Some(did_str) = split.next() { - log::debug!("Parsing did from str: {}", did_str); - Some(parse_ethr_did(did_str)?) - } else { - None - }; - // join the strings in the split with '/' as delimiter - let path = split.fold(String::new(), |mut acc, s| { - acc.push_str(format!("/{}", s).as_str()); - acc - }); - - let query = url.query().map(|query| query.to_owned()); - let fragment = url.fragment().map(|fragment| fragment.to_owned()); + let did_url = parse_ethr_did_url(input.as_ref()); + if let Err(e) = did_url { + return Err(DidError::Parse(e)); + } - Ok(Self { - did: did.unwrap_or_default(), - path, - query, - fragment, - }) + let did_url = did_url.unwrap(); + Ok(Self { ..did_url }) } /// Retrieves the method from the DID URL, as defined in the [did-ethr](https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md)). @@ -228,17 +278,38 @@ impl DidUrl { } /// Retrieves the path part from the DID URL, as defined in the [did-ethr spec](https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md)). - pub fn path(&self) -> &str { - &self.path + pub fn path(&self) -> Option<&str> { + self.path.as_deref() } - pub fn query(&self) -> Option<&str> { + pub fn query(&self) -> Option<&[(String, String)]> { self.query.as_deref() } - pub fn set_query(&mut self, key: &str, value: Option<&str>) { - let query = format!("{}={}", key, value.unwrap_or("")); - self.query = Some(query); + /// Immutable copy constructor that returns the same DID URL without its query + pub fn without_query(&self) -> Self { + let mut minion = self.clone(); + minion.query = None; + minion + } + + /// Immutable copy constructor to add a query parameter to the DID URL. + /// # Examples + /// ```rust + /// use lib_didethresolver::types::DidUrl; + /// let did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + /// let did_url = did_url.with_query("versionId", Some("1")); + /// assert_eq!(did_url.query(), Some(&[("versionId".to_string(), "1".to_string())][..])); + /// ``` + /// **Note**: the parser did not percent-encode this component, but the input may have been percent-encoded already. + pub fn with_query(&self, key: &str, value: Option<&str>) -> Self { + let mut minion = self.clone(); + if minion.query.is_none() { + minion.query = Some(Vec::new()); + } + let query = minion.query.as_mut().unwrap(); + query.push((key.to_string(), value.unwrap_or("").to_string())); + minion } /// Returns this DID's fragment identifier, if any. @@ -260,51 +331,76 @@ impl DidUrl { self.fragment.as_deref() } - /// Change this DID's fragment identifier + /// Immutable copy builder to add a fragment to this did url /// # Examples - /// - /// - /// ``` + /// ```rust /// use lib_didethresolver::types::DidUrl; - /// - /// let mut did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); - /// did_url.set_fragment(Some("controller")); + /// let did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + /// let did_url = did_url.with_fragment(Some(&"controller")); /// assert_eq!(did_url.fragment(), Some("controller")); /// ``` - pub fn set_fragment(&mut self, fragment: Option<&str>) { - // replace the fragment + /// **Note**: the parser did not percent-encode this component, but the input may have been percent-encoded already. + /// + pub fn with_fragment(&self, fragment: Option<&str>) -> Self { + let mut minion = self.clone(); if let Some(fragment) = fragment { - self.fragment = Some(fragment.to_string()); + minion.fragment = Some(fragment.to_string()); } else { - self.fragment = None; + minion.fragment = None; } + minion } - /// Change this DID's path + /// Immutable copy constructor returns an identical DID with the specified path /// /// # Examples /// ``` /// use lib_didethresolver::types::DidUrl; /// - /// let mut did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); - /// did_url.set_path("/path/to/resource"); - /// assert_eq!(did_url.path, "/path/to/resource"); + /// let did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + /// let did_url = did_url.with_path(Some(&"/path/to/resource")); + /// assert_eq!(did_url.path, Some("/path/to/resource".to_string())); /// ``` /// - pub fn set_path(&mut self, path: &str) { - self.path = path.to_string(); + pub fn with_path(&self, path: Option<&str>) -> Self { + let mut minion = self.clone(); + if let Some(path) = path { + minion.path = Some(path.to_string()); + } else { + minion.path = None; + } + minion } - pub fn set_account(&mut self, account: Account) { - self.did.account = account; + /// Immutable copy constructor returns an identical DID with the specified account + /// + /// # Examples + /// ```rust + /// use lib_didethresolver::types::{Account, DidUrl}; + /// use ethers::types::Address; + /// let did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + /// let address = hex::decode("b9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + /// let account = Account::Address(Address::from_slice(address.as_slice())); + /// let did_url = did_url.with_account(account.clone()); + /// assert_eq!(did_url.account(), &account); + /// ``` + pub fn with_account(&self, account: Account) -> Self { + let mut minion = self.clone(); + minion.did.account = account.clone(); + minion } } impl ToString for DidUrl { fn to_string(&self) -> String { - let mut string = format!("{}{}", self.did.to_string(), self.path()); + let mut string = format!("{}{}", self.did.to_string(), self.path().unwrap_or("")); if let Some(query) = self.query() { - string = format!("{}?{}", string, query); + let format_str = query + .iter() + .map(|(key, value)| format!("{}={}", key, value)) + .collect::>() + .join("&"); + string = format!("{}?{}", string, format_str); } if let Some(fragment) = self.fragment() { string = format!("{}#{}", string, fragment); @@ -350,7 +446,7 @@ mod tests { let err = DidUrl::parse("did:pkh:0x7e575682A8E450E33eB0493f9972821aE333cd7F").unwrap_err(); assert_eq!( - "Parsing of ethr:did failed, error at 1:1: expected one of \"ethr\", the only supported method is `ethr`" + "Parsing of ethr:did failed, error at 1:5: expected one of \"ethr\", the only supported method is `ethr`" .to_string(), err.to_string() ); @@ -436,25 +532,189 @@ mod tests { } #[test] - fn test_set_fragment() { - let mut did_url = + fn test_with_fragment() { + let did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#key-1").unwrap(); assert_eq!(did_url.fragment(), Some("key-1")); - - did_url.set_fragment(Some("key-2")); + let last = did_url.clone(); + let did_url = did_url.with_fragment(Some("key-2")); assert_eq!(did_url.fragment(), Some("key-2")); - - did_url.set_fragment(None); + assert_eq!(last.fragment(), Some("key-1")); + let did_url = did_url.with_fragment(None); assert_eq!(did_url.fragment(), None); + assert_eq!(did_url.method(), &Method::Ethr,); + assert_eq!(did_url.network(), &Network::Mainnet,); + assert_eq!( + did_url.account(), + &Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")) + ); + } + + #[test] + fn test_with_path() { + let did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + assert_eq!(did_url.path(), None); + let did_url = did_url.with_path(Some("path-2")); + assert_eq!(did_url.path(), Some("path-2")); + assert_eq!(did_url.method(), &Method::Ethr,); + assert_eq!(did_url.network(), &Network::Mainnet,); + assert_eq!( + did_url.account(), + &Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")) + ); + let did_url = did_url.with_path(None); + assert_eq!(did_url.path(), None); + } + + #[test] + fn test_without_query() { + let did_url = + DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a?versionId=1") + .unwrap() + .without_query(); + assert_eq!(did_url.query(), None); + assert_eq!(did_url.method(), &Method::Ethr,); + assert_eq!(did_url.network(), &Network::Mainnet,); + assert_eq!( + did_url.account(), + &Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")) + ); + } + + #[test] + fn test_with_query() { + let did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + assert_eq!(did_url.query(), None); + let last = did_url.clone(); + let did_url = did_url.with_query("key-1", Some("value-1")); + assert_eq!(last.query(), None); + assert_eq!( + did_url.query(), + Some(&[("key-1".to_string(), "value-1".to_string())][..]) + ); + let did_url = did_url.with_query("key-2", Some("value-2")); + assert_eq!( + did_url.query(), + Some( + &[ + ("key-1".to_string(), "value-1".to_string()), + ("key-2".to_string(), "value-2".to_string()) + ][..] + ) + ); + assert_eq!(did_url.method(), &Method::Ethr,); + assert_eq!(did_url.network(), &Network::Mainnet,); + assert_eq!( + did_url.account(), + &Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")) + ); + } + + #[test] + fn test_with_account() { + let did_url = DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); + assert_eq!( + did_url.account(), + &Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")) + ); + let last = did_url.clone(); + let did_url = did_url.with_account(Account::HexKey(vec![0x01, 0x02, 0x03])); + assert_eq!( + last.account(), + &Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")) + ); + assert_eq!(did_url.account(), &Account::HexKey(vec![0x01, 0x02, 0x03])); + assert_eq!(did_url.method(), &Method::Ethr,); + assert_eq!(did_url.network(), &Network::Mainnet,); + assert_eq!(did_url.to_string(), "did:ethr:mainnet:010203") } #[test] - fn test_set_path() { - let mut did_url = - DidUrl::parse("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a").unwrap(); - assert_eq!(did_url.path(), ""); + fn test_account_from_string() { + let account = Account::from("0xb9c5714089478a327f09197987f16f9e5d936e8a"); + assert_eq!( + account, + Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")) + ); - did_url.set_path("path-2"); - assert_eq!(did_url.path(), "path-2"); + let account = + Account::from("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"); + let eth_address: Address = account.into(); + let expect_address: Address = + Account::Address(address("0x7e5f4552091a69125d5dfcb7b8c2659029395bdf")).into(); + assert_eq!(expect_address, eth_address); + } + + #[test] + fn test_account_from_hex_key() { + let account = Account::from(vec![0x01, 0x02, 0x03]); + assert_eq!(account, Account::HexKey(vec![0x01, 0x02, 0x03])); + } + + #[test] + fn test_account_from_address() { + let account = Account::from(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")); + assert_eq!( + account, + Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")) + ); + } + + #[test] + fn test_account_to_address_conversion() { + let account = Account::Address(address("0xb9c5714089478a327f09197987f16f9e5d936e8a")); + assert_eq!( + Address::from_slice( + &hex::decode(as_hex_digits("0xb9c5714089478a327f09197987f16f9e5d936e8a")).unwrap() + ), + account.into() + ); + + let account = Account::HexKey(hex::decode("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8").unwrap().to_vec()); + assert_eq!( + Address::from_slice( + &hex::decode(as_hex_digits("0x7e5f4552091a69125d5dfcb7b8c2659029395bdf")).unwrap() + ), + account.into() + ); + } + + #[test] + fn test_public_key_hash() { + let pk = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"; + let key = hex::decode(pk).unwrap(); + let address = public_key_to_address(&key); + assert_eq!( + address, + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf".to_string() + ); + } + + #[test] + fn test_as_hex_str() { + let address = "0x7e575682A8E450E33eB0493f9972821aE333cd7F"; + assert_eq!( + as_hex_digits(address), + "7e575682A8E450E33eB0493f9972821aE333cd7F" + ); + let public_key = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"; + assert_eq!( + as_hex_digits(public_key), + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" + ); + } + + #[test] + fn test_network_from_usize() { + assert_eq!(Network::from(1), Network::Mainnet); + assert_eq!(Network::from(11155111), Network::Sepolia); + assert_eq!(Network::from(0x1a1), Network::Other(0x1a1)); + } + + #[test] + fn test_network_from_str() { + assert_eq!(Network::from("mainnet"), Network::Mainnet); + assert_eq!(Network::from("sepolia"), Network::Sepolia); + assert_eq!(Network::from("1a1"), Network::Other(0x1a1)); } } diff --git a/lib/src/types/ethr.rs b/lib/src/types/ethr.rs index 47f2379..09634fd 100644 --- a/lib/src/types/ethr.rs +++ b/lib/src/types/ethr.rs @@ -5,7 +5,7 @@ //! # Examples //! //! let mut builder = EthrBuilder::default(); -//! builder.public_key(&Address::from_str("0x872A62ABAfa278F0E0f02c1C5042D0614c3f38eb")).unwrap(); +//! builder.account_address(&Address::from_str("0x872A62ABAfa278F0E0f02c1C5042D0614c3f38eb")).unwrap(); //! let document = builder.build(); use std::{collections::HashMap, str::FromStr}; @@ -116,8 +116,14 @@ impl EthrBuilder { } /// set the identity of the document - pub fn public_key(&mut self, key: &Address) -> Result<(), EthrBuilderError> { - self.id.set_account(types::Account::Address(*key)); + pub fn account_address(&mut self, key: &Address) -> Result<(), EthrBuilderError> { + self.id = self.id.with_account(types::Account::Address(*key)); + Ok(()) + } + + /// set the identity of the document + pub fn public_key(&mut self, key: &[u8]) -> Result<(), EthrBuilderError> { + self.id = self.id.with_account(types::Account::HexKey(key.to_vec())); Ok(()) } @@ -128,8 +134,7 @@ impl EthrBuilder { /// Set the controller of the document pub fn controller(&mut self, controller: &Address) -> Result<(), EthrBuilderError> { - let mut did = self.id.clone(); - did.set_account(types::Account::Address(*controller)); + let did = self.id.with_account(types::Account::Address(*controller)); self.controller = Some(did); Ok(()) } @@ -254,8 +259,7 @@ impl EthrBuilder { value: V, service: ServiceType, ) -> Result<(), EthrBuilderError> { - let mut did = self.id.clone(); - did.set_fragment(Some(&format!("service-{}", index))); + let did = self.id.with_fragment(Some(&format!("service-{}", index))); let endpoint = Url::parse(&String::from_utf8_lossy(value.as_ref()))?; self.service.push(Service { id: did, @@ -280,9 +284,7 @@ impl EthrBuilder { value: V, key: PublicKey, ) -> Result<(), EthrBuilderError> { - let mut did = self.id.clone(); - did.set_fragment(Some(&format!("delegate-{}", index))); - + let did = self.id.with_fragment(Some(&format!("delegate-{}", index))); let method = VerificationMethod { id: did, controller: self.id.clone(), @@ -332,8 +334,7 @@ impl EthrBuilder { /// /// reference: [spec](https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md) pub fn delegate(&mut self, index: usize, delegate: &Address, purpose: KeyPurpose) { - let mut did = self.id.clone(); - did.set_fragment(Some(&format!("delegate-{}", index))); + let did = self.id.with_fragment(Some(&format!("delegate-{}", index))); // TODO: Handle ChainID let method = VerificationMethod { @@ -360,7 +361,7 @@ impl EthrBuilder { /// Handle controller changes according to [owner changed](https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md#controller-changes-didownerchanged) and [registration](https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md#create-register) fn build_controller(&mut self) { let mut controller = self.controller.clone().unwrap_or(self.id.clone()); - controller.set_fragment(Some("controller")); + controller = controller.with_fragment(Some("controller")); self.verification_method.push(VerificationMethod { id: controller.clone(), @@ -374,8 +375,7 @@ impl EthrBuilder { // if we are resolving for a key that is a public key which matches the id, we need to add // another `controllerKey` verification method if let Account::HexKey(_) = self.id.account() { - let mut controller_key = self.id.clone(); - controller_key.set_fragment(Some("controllerKey")); + let controller_key = self.id.with_fragment(Some("controllerKey")); self.verification_method.push(VerificationMethod { id: controller_key.clone(), controller: self.id.clone(), @@ -471,7 +471,7 @@ pub(crate) mod tests { }; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); builder.attribute_event(event).unwrap(); let doc = builder.build().unwrap(); @@ -490,17 +490,18 @@ pub(crate) mod tests { } ) } + #[test] fn test_attribute_changed_ed25519() { let identity = address("0x7e575682a8e450e33eb0493f9972821ae333cd7f"); let event = DidattributeChangedFilter { - name: *b"did/pub/Secp256k1/veriKey/base58", + name: *b"did/pub/Ed25519/veriKey/base58 ", value: b"b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), ..base_attr_changed(identity, None) }; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); builder.attribute_event(event).unwrap(); let doc = builder.build().unwrap(); @@ -509,7 +510,7 @@ pub(crate) mod tests { VerificationMethod { id: DidUrl::parse("did:ethr:0x7e575682a8e450e33eb0493f9972821ae333cd7f#delegate-0") .unwrap(), - verification_type: KeyType::EcdsaSecp256k1VerificationKey2019, + verification_type: KeyType::Ed25519VerificationKey2020, controller: DidUrl::parse("did:ethr:0x7e575682a8e450e33eb0493f9972821ae333cd7f") .unwrap(), verification_properties: Some(VerificationMethodProperties::PublicKeyBase58 { @@ -529,7 +530,7 @@ pub(crate) mod tests { }; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); builder.attribute_event(event).unwrap(); let doc = builder.build().unwrap(); @@ -559,7 +560,7 @@ pub(crate) mod tests { }; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); builder.attribute_event(event).unwrap(); let doc = builder.build().unwrap(); @@ -607,7 +608,7 @@ pub(crate) mod tests { ]; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); for event in events { @@ -663,7 +664,7 @@ pub(crate) mod tests { ]; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::from(100)); for event in events { builder.attribute_event(event).unwrap(); @@ -691,7 +692,7 @@ pub(crate) mod tests { }; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); builder.owner_event(event).unwrap(); @@ -722,7 +723,7 @@ pub(crate) mod tests { ]; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); for event in events { builder.delegate_event(event).unwrap(); @@ -798,7 +799,7 @@ pub(crate) mod tests { ]; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); for event in &events { builder.delegate_event(event.clone()).unwrap(); @@ -808,7 +809,7 @@ pub(crate) mod tests { assert_eq!(builder.keys.len(), 2); let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::from(75)); for event in &events { builder.delegate_event(event.clone()).unwrap(); @@ -817,7 +818,7 @@ pub(crate) mod tests { assert_eq!(builder.keys.len(), 1); let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::from(125)); for event in &events { builder.delegate_event(event.clone()).unwrap(); @@ -860,7 +861,7 @@ pub(crate) mod tests { ]; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); builder.attribute_event(attributes[0].clone()).unwrap(); @@ -905,7 +906,7 @@ pub(crate) mod tests { ]; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); for event in events { builder.owner_event(event).unwrap(); @@ -936,7 +937,7 @@ pub(crate) mod tests { }; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); builder.attribute_event(event).unwrap(); diff --git a/lib/src/types/xmtp.rs b/lib/src/types/xmtp.rs index dfe8156..baa4d74 100644 --- a/lib/src/types/xmtp.rs +++ b/lib/src/types/xmtp.rs @@ -78,19 +78,17 @@ impl EthrBuilder { value: V, key: XmtpAttribute, ) -> Result<(), EthrBuilderError> { - let mut did = self.id.clone(); - did.set_fragment(Some(&format!("xmtp-{}", index))); - log::debug!("index: {}", index); - let mut method = VerificationMethod { - id: did, + let did_url = self + .id + .with_fragment(Some(&format!("xmtp-{}", index))) + .with_query("meta", Some(&key.purpose.to_string())); + let method = VerificationMethod { + id: did_url, controller: self.id.clone(), verification_type: KeyType::Ed25519VerificationKey2020, verification_properties: Self::encode_attribute_value(value, key.encoding)?, }; - - method.id.set_query("meta", Some(&key.purpose.to_string())); - match key.purpose { XmtpKeyPurpose::Installation => { self.authentication.push(method.id.clone()); @@ -122,7 +120,7 @@ mod test { }]; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::zero()); for attr in attributes { @@ -134,7 +132,7 @@ mod test { assert_eq!(doc.verification_method[1].id.fragment().unwrap(), "xmtp-0"); assert_eq!( doc.verification_method[1].id.query().unwrap(), - "meta=installation" + vec![("meta".to_string(), "installation".to_string())] ); assert_eq!(doc.verification_method.len(), 2); @@ -151,7 +149,7 @@ mod test { }]; let mut builder = EthrBuilder::default(); - builder.public_key(&identity).unwrap(); + builder.account_address(&identity).unwrap(); builder.now(U256::from(100)); for attr in attributes { diff --git a/lib/tests/integration_test.rs b/lib/tests/integration_test.rs index 9820205..62c0586 100644 --- a/lib/tests/integration_test.rs +++ b/lib/tests/integration_test.rs @@ -8,17 +8,21 @@ use ethers::{ types::{Address, U256}, }; use integration_util::{validate_document, with_client}; -use lib_didethresolver::{ - did_registry::RegistrySignerExt, - rpc::DidRegistryClient, - types::{DidUrl, KeyType, VerificationMethodProperties, NULL_ADDRESS}, -}; -//TODO: Add tests for: Errors, formats, entire document asserts +#[cfg(test)] +mod it { + + use lib_didethresolver::{ + did_registry::RegistrySignerExt, + rpc::DidRegistryClient, + types::{DidUrl, KeyType, VerificationMethodProperties, NULL_ADDRESS}, + }; + + use super::*; -#[tokio::test] -pub async fn test_attributes() -> Result<()> { - with_client(None, |client, registry, signer, _| async move { + #[tokio::test] + pub async fn test_attributes() -> Result<()> { + with_client(None, |client, registry, signer, _| async move { let me = signer.address(); let did = registry.set_attribute( me, @@ -93,11 +97,11 @@ pub async fn test_attributes() -> Result<()> { Ok(()) }) .await -} + } -#[tokio::test] -pub async fn test_attributes_versions() -> Result<()> { - with_client(None, |client, registry, signer, _| async move { + #[tokio::test] + pub async fn test_attributes_versions() -> Result<()> { + with_client(None, |client, registry, signer, _| async move { let me = signer.address(); let did = registry.set_attribute( me, @@ -134,315 +138,320 @@ pub async fn test_attributes_versions() -> Result<()> { Ok(()) }) .await -} - -#[tokio::test] -pub async fn test_delegate() -> Result<()> { - with_client(None, |client, registry, signer, anvil| async move { - let me = signer.address(); - let delegate: LocalWallet = anvil.keys()[4].clone().into(); - let did = registry.add_delegate( - me, - *b"sigAuth ", - delegate.address(), - U256::from(604_800), - ); - did.send().await?.await?; - - let did = registry.add_delegate( - me, - *b"veriKey ", - delegate.address(), - U256::from(604_800), - ); - did.send().await?.await?; - - let resolution_response = client.resolve_did(hex::encode(me), None).await?; - validate_document(&resolution_response.document).await; - - assert_eq!( - resolution_response.document.verification_method[1].id, - DidUrl::parse(format!("did:ethr:0x{}#delegate-0", hex::encode(me))).unwrap() - ); - assert_eq!( - resolution_response.document.verification_method[1].controller, - DidUrl::parse(format!("did:ethr:0x{}", hex::encode(me))).unwrap() - ); - assert_eq!( - resolution_response.document.verification_method[1].verification_properties, - Some(VerificationMethodProperties::BlockchainAccountId { - blockchain_account_id: format!("0x{}", hex::encode(delegate.address())) - }) - ); - - assert_eq!( - resolution_response.document.verification_method[2].id, - DidUrl::parse(format!("did:ethr:0x{}#delegate-1", hex::encode(me))).unwrap() - ); - assert_eq!( - resolution_response.document.verification_method[2].controller, - DidUrl::parse(format!("did:ethr:0x{}", hex::encode(me))).unwrap() - ); - assert_eq!( - resolution_response.document.verification_method[2].verification_properties, - Some(VerificationMethodProperties::BlockchainAccountId { - blockchain_account_id: format!("0x{}", hex::encode(delegate.address())) - }) - ); - - Ok(()) - }) - .await -} - -#[tokio::test] -pub async fn test_owner_changed() -> Result<()> { - with_client(None, |client, registry, signer, anvil| async move { - let me = signer.address(); - let new_owner: LocalWallet = anvil.keys()[4].clone().into(); - let did = registry.change_owner(me, new_owner.address()); - did.send().await?.await?; - - let resolution_response = client.resolve_did(hex::encode(me), None).await?; - validate_document(&resolution_response.document).await; - - assert_eq!( - resolution_response.document.controller, - Some( - DidUrl::parse(format!("did:ethr:0x{}", hex::encode(new_owner.address()))).unwrap() - ) - ); - Ok(()) - }) - .await -} - -#[tokio::test] -pub async fn test_attribute_revocation() -> Result<()> { - with_client(None, |client, registry, signer, _| async move { - let me = signer.address(); - let did = registry.set_attribute( - me, - *b"did/pub/Secp256k1/veriKey/hex ", - b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), - U256::from(604_800), - ); - did.send().await?.await?; - - let did = registry.revoke_attribute( - me, - *b"did/pub/Secp256k1/veriKey/hex ", - b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), - ); - did.send().await?.await?; - - let document = client.resolve_did(hex::encode(me), None).await?.document; - validate_document(&document).await; - - assert_eq!( - document.verification_method[0].id, - DidUrl::parse(format!("did:ethr:0x{}#controller", hex::encode(me))).unwrap() - ); - assert_eq!(document.verification_method.len(), 1); - - Ok(()) - }) - .await -} - -#[tokio::test] -pub async fn test_delegate_revocation() -> Result<()> { - with_client(None, |client, registry, signer, anvil| async move { - let me = signer.address(); - let delegate: LocalWallet = anvil.keys()[4].clone().into(); - let did = registry.add_delegate( - me, - *b"sigAuth ", - delegate.address(), - U256::from(604_800), - ); - did.send().await?.await?; - let did = registry.add_delegate( - me, - *b"veriKey ", - delegate.address(), - U256::from(604_800), - ); - did.send().await?.await?; - - let did = - registry.revoke_delegate(me, *b"sigAuth0000000000000000000000000", delegate.address()); - did.send().await?.await?; - - let document = client.resolve_did(hex::encode(me), None).await?.document; - validate_document(&document).await; - - assert_eq!( - document.verification_method[0].id, - DidUrl::parse(format!("did:ethr:0x{}#controller", hex::encode(me))).unwrap() - ); - // delegate 1, veriKey should still be valid after revoking delegate 0 - assert_eq!( - document.verification_method[1].id, - DidUrl::parse(format!("did:ethr:0x{}#delegate-1", hex::encode(me))).unwrap() - ); - assert_eq!(document.verification_method.len(), 2); - - Ok(()) - }) - .await -} - -#[tokio::test] -pub async fn test_owner_revocation() -> Result<()> { - with_client(None, |client, registry, signer, _| async move { - let me = signer.address(); - let null = Address::from_str(NULL_ADDRESS.strip_prefix("0x").unwrap()).unwrap(); - let did = registry.change_owner(me, null); - did.send().await?.await?; - - let resolved = client.resolve_did(hex::encode(me), None).await?; - validate_document(&resolved.document).await; - - assert!(resolved.metadata.deactivated); - - Ok(()) - }) - .await -} - -#[tokio::test] -pub async fn test_xmtp_revocation() -> Result<()> { - with_client(None, |client, registry, signer, _| async move { - let me = signer.address(); - let attribute_name = *b"xmtp/installation/hex "; - let did = registry.set_attribute( - me, - attribute_name, - b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), - U256::from(604_800), - ); - did.send().await?.await?; - - let document = client.resolve_did(hex::encode(me), None).await?.document; - assert_eq!( - document.verification_method[1].id, - DidUrl::parse(format!( - "did:ethr:0x{}?meta=installation#xmtp-0", - hex::encode(me) - )) - .unwrap() - ); - - let did = registry.revoke_attribute( - me, - attribute_name, - b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), - ); - did.send().await?.await?; - - let document = client.resolve_did(hex::encode(me), None).await?.document; - validate_document(&document).await; - assert_eq!( - document.verification_method[0].id, - DidUrl::parse(format!("did:ethr:0x{}#controller", hex::encode(me))).unwrap() - ); - assert_eq!(document.verification_method.len(), 1); - - Ok(()) - }) - .await -} - -#[tokio::test] -pub async fn test_signed_fns() -> Result<()> { - with_client(None, |_, registry, _, anvil| async move { - let me: LocalWallet = anvil.keys()[3].clone().into(); - let name = *b"xmtp/installation/hex "; - let value = b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71"; - let validity = U256::from(604_800); - let signature = me - .sign_attribute(®istry, name, value.to_vec(), validity) - .await?; - - let attr = 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 signature = me - .sign_revoke_attribute(®istry, name, value.to_vec()) - .await?; - registry - .revoke_attribute_signed( + } + + #[tokio::test] + pub async fn test_delegate() -> Result<()> { + with_client(None, |client, registry, signer, anvil| async move { + let me = signer.address(); + let delegate: LocalWallet = anvil.keys()[4].clone().into(); + let did = registry.add_delegate( + me, + *b"sigAuth ", + delegate.address(), + U256::from(604_800), + ); + did.send().await?.await?; + + let did = registry.add_delegate( + me, + *b"veriKey ", + delegate.address(), + U256::from(604_800), + ); + did.send().await?.await?; + + let resolution_response = client.resolve_did(hex::encode(me), None).await?; + validate_document(&resolution_response.document).await; + + assert_eq!( + resolution_response.document.verification_method[1].id, + DidUrl::parse(format!("did:ethr:0x{}#delegate-0", hex::encode(me))).unwrap() + ); + assert_eq!( + resolution_response.document.verification_method[1].controller, + DidUrl::parse(format!("did:ethr:0x{}", hex::encode(me))).unwrap() + ); + assert_eq!( + resolution_response.document.verification_method[1].verification_properties, + Some(VerificationMethodProperties::BlockchainAccountId { + blockchain_account_id: format!("0x{}", hex::encode(delegate.address())) + }) + ); + + assert_eq!( + resolution_response.document.verification_method[2].id, + DidUrl::parse(format!("did:ethr:0x{}#delegate-1", hex::encode(me))).unwrap() + ); + assert_eq!( + resolution_response.document.verification_method[2].controller, + DidUrl::parse(format!("did:ethr:0x{}", hex::encode(me))).unwrap() + ); + assert_eq!( + resolution_response.document.verification_method[2].verification_properties, + Some(VerificationMethodProperties::BlockchainAccountId { + blockchain_account_id: format!("0x{}", hex::encode(delegate.address())) + }) + ); + + Ok(()) + }) + .await + } + + #[tokio::test] + pub async fn test_owner_changed() -> Result<()> { + with_client(None, |client, registry, signer, anvil| async move { + let me = signer.address(); + let new_owner: LocalWallet = anvil.keys()[4].clone().into(); + let did = registry.change_owner(me, new_owner.address()); + did.send().await?.await?; + + let resolution_response = client.resolve_did(hex::encode(me), None).await?; + validate_document(&resolution_response.document).await; + + assert_eq!( + resolution_response.document.controller, + Some( + DidUrl::parse(format!("did:ethr:0x{}", hex::encode(new_owner.address()))) + .unwrap() + ) + ); + Ok(()) + }) + .await + } + + #[tokio::test] + pub async fn test_attribute_revocation() -> Result<()> { + with_client(None, |client, registry, signer, _| async move { + let me = signer.address(); + let did = registry.set_attribute( + me, + *b"did/pub/Secp256k1/veriKey/hex ", + b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), + U256::from(604_800), + ); + did.send().await?.await?; + + let did = registry.revoke_attribute( + me, + *b"did/pub/Secp256k1/veriKey/hex ", + b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), + ); + did.send().await?.await?; + + let document = client.resolve_did(hex::encode(me), None).await?.document; + validate_document(&document).await; + + assert_eq!( + document.verification_method[0].id, + DidUrl::parse(format!("did:ethr:0x{}#controller", hex::encode(me))).unwrap() + ); + assert_eq!(document.verification_method.len(), 1); + + Ok(()) + }) + .await + } + + #[tokio::test] + pub async fn test_delegate_revocation() -> Result<()> { + with_client(None, |client, registry, signer, anvil| async move { + let me = signer.address(); + let delegate: LocalWallet = anvil.keys()[4].clone().into(); + let did = registry.add_delegate( + me, + *b"sigAuth ", + delegate.address(), + U256::from(604_800), + ); + did.send().await?.await?; + let did = registry.add_delegate( + me, + *b"veriKey ", + delegate.address(), + U256::from(604_800), + ); + did.send().await?.await?; + + let did = registry.revoke_delegate( + me, + *b"sigAuth0000000000000000000000000", + delegate.address(), + ); + did.send().await?.await?; + + let document = client.resolve_did(hex::encode(me), None).await?.document; + validate_document(&document).await; + + assert_eq!( + document.verification_method[0].id, + DidUrl::parse(format!("did:ethr:0x{}#controller", hex::encode(me))).unwrap() + ); + // delegate 1, veriKey should still be valid after revoking delegate 0 + assert_eq!( + document.verification_method[1].id, + DidUrl::parse(format!("did:ethr:0x{}#delegate-1", hex::encode(me))).unwrap() + ); + assert_eq!(document.verification_method.len(), 2); + + Ok(()) + }) + .await + } + + #[tokio::test] + pub async fn test_owner_revocation() -> Result<()> { + with_client(None, |client, registry, signer, _| async move { + let me = signer.address(); + let null = Address::from_str(NULL_ADDRESS.strip_prefix("0x").unwrap()).unwrap(); + let did = registry.change_owner(me, null); + did.send().await?.await?; + + let resolved = client.resolve_did(hex::encode(me), None).await?; + validate_document(&resolved.document).await; + + assert!(resolved.metadata.deactivated); + + Ok(()) + }) + .await + } + + #[tokio::test] + pub async fn test_xmtp_revocation() -> Result<()> { + with_client(None, |client, registry, signer, _| async move { + let me = signer.address(); + let attribute_name = *b"xmtp/installation/hex "; + let did = registry.set_attribute( + me, + attribute_name, + b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), + U256::from(604_800), + ); + did.send().await?.await?; + + let document = client.resolve_did(hex::encode(me), None).await?.document; + assert_eq!( + document.verification_method[1].id, + DidUrl::parse(format!( + "did:ethr:0x{}?meta=installation#xmtp-0", + hex::encode(me) + )) + .unwrap() + ); + + let did = registry.revoke_attribute( + me, + attribute_name, + b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71".into(), + ); + did.send().await?.await?; + + let document = client.resolve_did(hex::encode(me), None).await?.document; + validate_document(&document).await; + assert_eq!( + document.verification_method[0].id, + DidUrl::parse(format!("did:ethr:0x{}#controller", hex::encode(me))).unwrap() + ); + assert_eq!(document.verification_method.len(), 1); + + Ok(()) + }) + .await + } + + #[tokio::test] + pub async fn test_signed_fns() -> Result<()> { + with_client(None, |_, registry, _, anvil| async move { + let me: LocalWallet = anvil.keys()[3].clone().into(); + let name = *b"xmtp/installation/hex "; + let value = b"02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71"; + let validity = U256::from(604_800); + let signature = me + .sign_attribute(®istry, name, value.to_vec(), validity) + .await?; + + let attr = registry.set_attribute_signed( me.address(), signature.v.try_into().unwrap(), signature.r.into(), signature.s.into(), name, value.into(), - ) - .send() - .await? - .await?; - - let delegate_type = *b"sigAuth "; - - let signature = me - .sign_delegate(®istry, delegate_type, me.address(), validity) - .await?; - registry - .add_delegate_signed( - me.address(), - signature.v.try_into().unwrap(), - signature.r.into(), - signature.s.into(), - delegate_type, - me.address(), validity, - ) - .send() - .await? - .await?; - - let signature = me - .sign_revoke_delegate(®istry, delegate_type, me.address()) - .await?; - - registry - .revoke_delegate_signed( - me.address(), - signature.v.try_into().unwrap(), - signature.r.into(), - signature.s.into(), - delegate_type, - me.address(), - ) - .send() - .await? - .await?; - - let new_owner = Address::from_str(NULL_ADDRESS.strip_prefix("0x").unwrap()).unwrap(); - let signature = me.sign_owner(®istry, new_owner).await?; - registry - .change_owner_signed( - me.address(), - signature.v.try_into().unwrap(), - signature.r.into(), - signature.s.into(), - new_owner, - ) - .send() - .await? - .await?; - - Ok(()) - }) - .await + ); + attr.send().await?.await?; + + let signature = me + .sign_revoke_attribute(®istry, name, value.to_vec()) + .await?; + registry + .revoke_attribute_signed( + me.address(), + signature.v.try_into().unwrap(), + signature.r.into(), + signature.s.into(), + name, + value.into(), + ) + .send() + .await? + .await?; + + let delegate_type = *b"sigAuth "; + + let signature = me + .sign_delegate(®istry, delegate_type, me.address(), validity) + .await?; + registry + .add_delegate_signed( + me.address(), + signature.v.try_into().unwrap(), + signature.r.into(), + signature.s.into(), + delegate_type, + me.address(), + validity, + ) + .send() + .await? + .await?; + + let signature = me + .sign_revoke_delegate(®istry, delegate_type, me.address()) + .await?; + + registry + .revoke_delegate_signed( + me.address(), + signature.v.try_into().unwrap(), + signature.r.into(), + signature.s.into(), + delegate_type, + me.address(), + ) + .send() + .await? + .await?; + + let new_owner = Address::from_str(NULL_ADDRESS.strip_prefix("0x").unwrap()).unwrap(); + let signature = me.sign_owner(®istry, new_owner).await?; + registry + .change_owner_signed( + me.address(), + signature.v.try_into().unwrap(), + signature.r.into(), + signature.s.into(), + new_owner, + ) + .send() + .await? + .await?; + + Ok(()) + }) + .await + } } diff --git a/resolver/src/argenv.rs b/resolver/src/argenv.rs index a95d801..f812bb7 100644 --- a/resolver/src/argenv.rs +++ b/resolver/src/argenv.rs @@ -100,10 +100,16 @@ mod tests { assert_eq!(args.port, DEFAULT_PORT); assert_eq!(args.rpc_url, DEFAULT_RPC_URL); assert_eq!(args.did_registry, "0x1234567890"); - let args2 = Args::parse_from(&["didethresolver", "--did-registry", "0x0987654321"]); + let args2 = Args::parse_from(&[ + "didethresolver", + "--did-registry", + "0x0987654321", + "--rpc-url", + "http://rpc2.xyz", + ]); assert_eq!(args2.host, DEFAULT_HOST); assert_eq!(args2.port, DEFAULT_PORT); - assert_eq!(args2.rpc_url, DEFAULT_RPC_URL); + assert_eq!(args2.rpc_url, "http://rpc2.xyz"); assert_eq!(args2.did_registry, "0x0987654321"); putback(env) }