diff --git a/bindings/wasm/cypress/integration/browser_test.js b/bindings/wasm/cypress/integration/browser_test.js
index f4e1d899aa..6e38f7db65 100644
--- a/bindings/wasm/cypress/integration/browser_test.js
+++ b/bindings/wasm/cypress/integration/browser_test.js
@@ -7,6 +7,7 @@ import { createVP } from "../../examples/browser/create_vp.js";
import { createDiff } from "../../examples/browser/diff_chain.js";
import { revoke } from "../../examples/browser/revoke_vc.js";
import { merkleKey } from "../../examples/browser/merkle_key.js";
+import { createIdentityPrivateTangle } from "../../examples/browser/private_tangle";
import { resolveHistory } from "../../examples/browser/resolve_history";
// Test that the browser examples do not throw uncaught exceptions twice, including syntax errors etc.
@@ -83,6 +84,18 @@ describe(
await merkleKey(defaultClientConfig(), false);
}
});
+
+ it("private tangle", async function () {
+ try {
+ await createIdentityPrivateTangle(false, false)
+ throw new Error("Did not throw.")
+ } catch (err) {
+ // Example is expected to throw an error because no private Tangle is running
+ expect(err.name).to.eq("ClientError")
+ expect(err.message).to.contain("error sending request")
+ }
+ });
+
it("diff chain", async function () {
try {
await createDiff(defaultClientConfig(), false);
@@ -90,6 +103,7 @@ describe(
await createDiff(defaultClientConfig(), false);
}
});
+
it("resolve history", async function () {
try {
await resolveHistory(defaultClientConfig(), false);
diff --git a/bindings/wasm/examples/README.md b/bindings/wasm/examples/README.md
index ac371fe81a..26a0fc585c 100644
--- a/bindings/wasm/examples/README.md
+++ b/bindings/wasm/examples/README.md
@@ -43,7 +43,7 @@ The following examples are currently available:
| 7 | [revoke_vc](node/revoke_vc.js) | Remove a verification method from the Issuers DID Document, making the Verifiable Credential it signed unable to verify, effectively revoking the VC. |
| 8 | [resolution](node/resolution.js) | Resolves an existing DID to return the latest DID Document. |
| 9 | [merkle_key](node/merkle_key.js) | Adds a MerkleKeyCollection verification method to an Issuers DID Document and signs a Verifiable Credential with the key on index 0. Afterwards the key on index 0 is deactivated, making the Verifiable Credential fail its verification. |
-
+| 10 | [private_tangle](node/private_tangle.js) | Showcases the same procedure as `create_did`, but on a private tangle - a locally running hornet node. |
### Browser Examples
diff --git a/bindings/wasm/examples/browser/index.html b/bindings/wasm/examples/browser/index.html
index 749b344226..87d110d2f9 100644
--- a/bindings/wasm/examples/browser/index.html
+++ b/bindings/wasm/examples/browser/index.html
@@ -47,6 +47,12 @@
Merkle Key
+
+
+
+ Create DID on Private Tangle
+
+
Diff Chain
@@ -55,7 +61,6 @@
Resolve History
-
merkleKey(clientConfig));
+//handle private tangle DID creation on click event
+document
+ .querySelector("#private_tangle_btn")
+ .addEventListener("click", () => createIdentityPrivateTangle());
+
//handle diff chain on click event
document
.querySelector("#diff_chain_btn")
diff --git a/bindings/wasm/examples/browser/private_tangle.js b/bindings/wasm/examples/browser/private_tangle.js
new file mode 100644
index 0000000000..79e8a21588
--- /dev/null
+++ b/bindings/wasm/examples/browser/private_tangle.js
@@ -0,0 +1,64 @@
+// Copyright 2020-2021 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+import { Client, Config, Document, KeyType, Network } from "../../web/identity_wasm.js";
+import {
+ logObjectToScreen,
+ logToScreen,
+} from "./utils.js";
+
+/**
+ This example shows how a DID document can be created on a private tangle.
+ It can be run together with a local hornet node.
+ Refer to https://github.com/iotaledger/one-click-tangle/tree/chrysalis/hornet-private-net
+ for setup instructions.
+**/
+export async function createIdentityPrivateTangle(inBrowser = true, log = true) {
+ if (log) logToScreen("Identity creation started...");
+ if (log) logToScreen("This might take a few seconds to complete proof of work!");
+
+ let restURL
+ let networkName
+
+ if (inBrowser) {
+ // Get the required parameters from the input fields
+ restURL = document.querySelector("#create-private-rest-url").value;
+ networkName = document.querySelector("#create-private-network-name").value;
+ } else {
+ restURL = "http://127.0.0.1:14265/";
+ networkName = "custom";
+ }
+
+ // This is an arbitrarily defined network name
+ const network = Network.from_name(networkName);
+
+ // Create a DID Document (an identity).
+ const { doc, key } = new Document(KeyType.Ed25519, network.toString());
+
+ // Sign the DID Document with the generated key.
+ doc.sign(key);
+
+ // Create a client configuration and set the custom network.
+ const config = new Config();
+ config.setNetwork(network);
+
+ // This URL should point to the REST API of a node.
+ config.setNode(restURL);
+
+ // Create a client instance from the configuration to publish messages to the Tangle.
+ const client = Client.fromConfig(config);
+
+ // Publish the Identity to the IOTA Network, this may take a few seconds to complete Proof-of-Work.
+ const receipt = await client.publishDocument(doc.toJSON());
+
+ if (log) logToScreen("Identity creation done!");
+
+ // Make sure the DID can be resolved on the private tangle
+ const resolved = await client.resolve(doc.id.toString());
+
+ if (log) logToScreen("Resolved DID document:");
+ if (log) logObjectToScreen(resolved);
+
+ // Return the results.
+ return { key, doc, receipt };
+}
diff --git a/bindings/wasm/examples/node/node.js b/bindings/wasm/examples/node/node.js
index 60892d990f..d1e4b39935 100644
--- a/bindings/wasm/examples/node/node.js
+++ b/bindings/wasm/examples/node/node.js
@@ -3,6 +3,7 @@
const {createIdentity} = require("./create_did");
const {manipulateIdentity} = require("./manipulate_did");
+const {createIdentityPrivateTangle} = require("./private_tangle");
const {resolution} = require("./resolution");
const {createVC} = require("./create_vc");
const {createVP} = require("./create_vp");
@@ -35,6 +36,8 @@ async function main() {
return await createVP(CLIENT_CONFIG);
case "merkle_key":
return await merkleKey(CLIENT_CONFIG);
+ case "private_tangle":
+ return await createIdentityPrivateTangle();
case "resolve_history":
return await resolveHistory(CLIENT_CONFIG);
case "diff_chain":
diff --git a/bindings/wasm/examples/node/private_tangle.js b/bindings/wasm/examples/node/private_tangle.js
new file mode 100644
index 0000000000..a148b43095
--- /dev/null
+++ b/bindings/wasm/examples/node/private_tangle.js
@@ -0,0 +1,42 @@
+// Copyright 2020-2021 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+const { Client, Config, Document, KeyType, Network } = require('../../node/identity_wasm')
+
+/**
+ This example shows how a DID document can be created on a private tangle.
+ It can be run together with a local hornet node.
+ Refer to https://github.com/iotaledger/one-click-tangle/tree/chrysalis/hornet-private-net
+ for setup instructions.
+**/
+async function createIdentityPrivateTangle() {
+ // This is an arbitrarily defined network name
+ const network = Network.from_name("custom");
+
+ // Create a DID Document (an identity).
+ const { doc, key } = new Document(KeyType.Ed25519, network.toString());
+
+ // Sign the DID Document with the generated key.
+ doc.sign(key);
+
+ // Create a client configuration and set the custom network.
+ const config = new Config();
+ config.setNetwork(network);
+
+ // This URL points to the REST API of the locally running hornet node.
+ config.setNode("http://127.0.0.1:14265/");
+
+ // Create a client instance from the configuration to publish messages to the Tangle.
+ const client = Client.fromConfig(config);
+
+ // Publish the Identity to the IOTA Network, this may take a few seconds to complete Proof-of-Work.
+ const receipt = await client.publishDocument(doc.toJSON());
+
+ // Make sure the DID can be resolved on the private tangle
+ const resolved = await client.resolve(doc.id.toString());
+
+ // Return the results.
+ return { key, resolved, receipt };
+}
+
+exports.createIdentityPrivateTangle = createIdentityPrivateTangle;
diff --git a/bindings/wasm/examples/node/test.js b/bindings/wasm/examples/node/test.js
index c6b4adf464..e557c6885b 100644
--- a/bindings/wasm/examples/node/test.js
+++ b/bindings/wasm/examples/node/test.js
@@ -8,6 +8,7 @@ const { revokeVC } = require("./revoke_vc");
const { merkleKey } = require("./merkle_key");
const { resolveHistory } = require("./resolve_history");
const { CLIENT_CONFIG } = require("./config");
+const { createIdentityPrivateTangle } = require("./private_tangle");
jest.setTimeout(180000); // 3 minutes to account for spurious network delays, most tests pass in a few seconds
@@ -63,6 +64,16 @@ test.concurrent("Merkle Key", async () => {
await merkleKey(CLIENT_CONFIG);
}
});
+test.concurrent("Private Tangle", async () => {
+ try {
+ await createIdentityPrivateTangle()
+ throw new Error("Did not throw.")
+ } catch (err) {
+ // Example is expected to throw an error because no private Tangle is running
+ expect(err.name).toEqual("ClientError")
+ expect(err.message).toContain("error sending request")
+ }
+});
test.concurrent("Diff Chain", async () => {
try {
await createDiff(CLIENT_CONFIG);
diff --git a/bindings/wasm/src/did/wasm_did.rs b/bindings/wasm/src/did/wasm_did.rs
index 3de1128347..01da6617e2 100644
--- a/bindings/wasm/src/did/wasm_did.rs
+++ b/bindings/wasm/src/did/wasm_did.rs
@@ -42,8 +42,8 @@ impl WasmDID {
/// Returns the IOTA tangle network of the `DID`.
#[wasm_bindgen(getter)]
- pub fn network(&self) -> WasmNetwork {
- self.0.network().into()
+ pub fn network(&self) -> Result {
+ self.0.network().map(Into::into).map_err(wasm_error)
}
/// Returns the IOTA tangle network of the `DID`.
diff --git a/bindings/wasm/src/tangle/config.rs b/bindings/wasm/src/tangle/config.rs
index b6a5561bfd..2e719c21e2 100644
--- a/bindings/wasm/src/tangle/config.rs
+++ b/bindings/wasm/src/tangle/config.rs
@@ -40,7 +40,7 @@ impl Config {
#[wasm_bindgen(js_name = setNetwork)]
pub fn set_network(&mut self, network: &WasmNetwork) -> Result<(), JsValue> {
- self.with_mut(|builder| builder.network((*network).into()))
+ self.with_mut(|builder| builder.network(network.clone().into()))
}
#[wasm_bindgen(js_name = setNode)]
diff --git a/bindings/wasm/src/tangle/network.rs b/bindings/wasm/src/tangle/network.rs
index 417e233a83..516d8d4816 100644
--- a/bindings/wasm/src/tangle/network.rs
+++ b/bindings/wasm/src/tangle/network.rs
@@ -5,12 +5,20 @@ use identity::iota::Network as IotaNetwork;
use wasm_bindgen::prelude::*;
+use crate::error::wasm_error;
+
#[wasm_bindgen(js_name = Network)]
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Debug)]
pub struct WasmNetwork(IotaNetwork);
#[wasm_bindgen(js_class = Network)]
impl WasmNetwork {
+ #[wasm_bindgen]
+ pub fn from_name(string: &str) -> Result {
+ let network = IotaNetwork::from_name(string).map_err(wasm_error)?;
+ Ok(Self(network))
+ }
+
#[wasm_bindgen]
pub fn mainnet() -> WasmNetwork {
Self(IotaNetwork::Mainnet)
@@ -23,26 +31,30 @@ impl WasmNetwork {
/// Returns the node URL of the Tangle network.
#[wasm_bindgen(getter = defaultNodeURL)]
- pub fn default_node_url(&self) -> String {
- self.0.default_node_url().to_string()
+ pub fn default_node_url(&self) -> Option {
+ self.0.default_node_url().map(ToString::to_string)
}
/// Returns the web explorer URL of the Tangle network.
#[wasm_bindgen(getter = explorerURL)]
- pub fn explorer_url(&self) -> String {
- self.0.explorer_url().to_string()
+ pub fn explorer_url(&self) -> Result {
+ self.0.explorer_url().map(|url| url.to_string()).map_err(wasm_error)
}
/// Returns the web explorer URL of the given `message`.
#[wasm_bindgen(js_name = messageURL)]
- pub fn message_url(&self, message_id: &str) -> String {
- self.0.message_url(message_id).to_string()
+ pub fn message_url(&self, message_id: &str) -> Result {
+ self
+ .0
+ .message_url(message_id)
+ .map(|url| url.to_string())
+ .map_err(wasm_error)
}
#[allow(clippy::inherent_to_string, clippy::wrong_self_convention)]
#[wasm_bindgen(js_name = toString)]
pub fn to_string(&self) -> String {
- self.0.as_str().into()
+ self.0.name().into()
}
}
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index 891f0d9f22..323c810625 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -38,6 +38,10 @@ path = "account/signing.rs"
name = "account_stronghold"
path = "account/stronghold.rs"
+[[example]]
+name = "account_private_tangle"
+path = "account/private_tangle.rs"
+
[[example]]
name = "create_did"
path = "low-level-api/create_did.rs"
@@ -70,6 +74,10 @@ path = "low-level-api/merkle_key.rs"
name = "resolution"
path = "low-level-api/resolution.rs"
+[[example]]
+name = "private_tangle"
+path = "low-level-api/private_tangle.rs"
+
[[example]]
name = "revoke_vc"
path = "low-level-api/revoke_vc.rs"
diff --git a/examples/account/private_tangle.rs b/examples/account/private_tangle.rs
new file mode 100644
index 0000000000..84d5a883bc
--- /dev/null
+++ b/examples/account/private_tangle.rs
@@ -0,0 +1,63 @@
+// Copyright 2020-2021 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+//! A basic example that generates, publishes and deletes a DID Document
+//! to and from a private tangle.
+//! It can be run together with a local hornet node.
+//! Refer to https://github.com/iotaledger/one-click-tangle/tree/chrysalis/hornet-private-net
+//! for setup instructions.
+//!
+//! cargo run --example account_private_tangle
+
+use identity::account::Account;
+use identity::account::IdentityCreate;
+use identity::account::IdentitySnapshot;
+use identity::account::Result;
+use identity::iota::IotaDID;
+use identity::iota::IotaDocument;
+use identity::iota::Network;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ pretty_env_logger::init();
+
+ // This is an arbitrarily defined network name
+ let network_name = "custom";
+ let network = Network::from_name(network_name)?;
+
+ // Create a new Account with the default configuration
+ let account: Account = Account::builder()
+ // Configure a client for the private network.
+ // Also set the URL that points to the REST API
+ // of the locally running hornet node.
+ .client(network, |builder| {
+ // unwrap is safe, we provided a valid node URL
+ builder.node("http://127.0.0.1:14265/").unwrap()
+ })
+ .build()
+ .await?;
+
+ let id_create = IdentityCreate::new().network(network_name);
+
+ // Create a new Identity with default settings
+ let snapshot: IdentitySnapshot = account.create_identity(id_create).await?;
+
+ // Retrieve the DID from the newly created Identity state.
+ let document: &IotaDID = snapshot.identity().try_did()?;
+
+ println!("[Example] Local Snapshot = {:#?}", snapshot);
+ println!("[Example] Local Document = {:#?}", snapshot.identity().to_document()?);
+ println!("[Example] Local Document List = {:#?}", account.list_identities().await);
+
+ // Fetch the DID Document from the Tangle
+ //
+ // This is an optional step to ensure DID Document consistency.
+ let resolved: IotaDocument = account.resolve_identity(document).await?;
+
+ println!("[Example] Tangle Document = {:#?}", resolved);
+
+ // Delete the identity and all associated keys
+ account.delete_identity(document).await?;
+
+ Ok(())
+}
diff --git a/examples/low-level-api/create_did.rs b/examples/low-level-api/create_did.rs
index 333ab7e910..6b1fb075df 100644
--- a/examples/low-level-api/create_did.rs
+++ b/examples/low-level-api/create_did.rs
@@ -32,7 +32,7 @@ pub async fn run() -> Result<(IotaDocument, KeyPair, Receipt)> {
println!("Publish Receipt > {:#?}", receipt);
// Display the web explorer url that shows the published message.
- println!("DID Document Transaction > {}", receipt.message_url());
+ println!("DID Document Transaction > {}", receipt.message_url()?);
Ok((document, keypair, receipt))
}
diff --git a/examples/low-level-api/diff_chain.rs b/examples/low-level-api/diff_chain.rs
index b014c170d9..8214c1a502 100644
--- a/examples/low-level-api/diff_chain.rs
+++ b/examples/low-level-api/diff_chain.rs
@@ -48,7 +48,7 @@ async fn main() -> Result<()> {
println!("Diff Update Receipt > {:#?}", update_receipt);
// Display the web explorer url that shows the published diff message.
- println!("Diff Transaction > {}", update_receipt.message_url());
+ println!("Diff Transaction > {}", update_receipt.message_url()?);
Ok(())
}
diff --git a/examples/low-level-api/manipulate_did.rs b/examples/low-level-api/manipulate_did.rs
index 6f20ff9a40..be6603131c 100644
--- a/examples/low-level-api/manipulate_did.rs
+++ b/examples/low-level-api/manipulate_did.rs
@@ -52,7 +52,7 @@ pub async fn run() -> Result<(IotaDocument, KeyPair, KeyPair, Receipt, Receipt)>
println!("Publish Receipt > {:#?}", update_receipt);
// Display the web explorer url that shows the published message.
- println!("DID Document Transaction > {}", update_receipt.message_url());
+ println!("DID Document Transaction > {}", update_receipt.message_url()?);
Ok((document, keypair, new_key, receipt, update_receipt))
}
diff --git a/examples/low-level-api/private_tangle.rs b/examples/low-level-api/private_tangle.rs
new file mode 100644
index 0000000000..29fdb19595
--- /dev/null
+++ b/examples/low-level-api/private_tangle.rs
@@ -0,0 +1,53 @@
+// Copyright 2020-2021 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+//! A basic example that generates and publishes a DID Document
+//! to a private tangle.
+//! It can be run together with a local hornet node.
+//! Refer to https://github.com/iotaledger/one-click-tangle/tree/chrysalis/hornet-private-net
+//! for setup instructions.
+//!
+//! cargo run --example private_tangle
+
+use identity::iota::ClientBuilder;
+use identity::iota::ClientMap;
+use identity::iota::Network;
+use identity::iota::Receipt;
+use identity::prelude::*;
+
+#[tokio::main]
+pub async fn main() -> Result<()> {
+ // This is an arbitrarily defined network name
+ let network_name = "custom";
+ let network = Network::from_name(network_name)?;
+
+ // Set the network and the URL that points to
+ // the REST API of the locally running hornet node.
+ let client = ClientBuilder::new()
+ .network(network)
+ .node("http://127.0.0.1:14265/")?
+ .build()
+ .await?;
+
+ // Create a client instance to send messages to the Tangle.
+ let client: ClientMap = ClientMap::from_client(client);
+
+ // Generate a new ed25519 public/private key pair.
+ let keypair: KeyPair = KeyPair::new_ed25519()?;
+
+ // Create a DID with the explicitly set network.
+ // This will result in a DID prefixed by `did:iota:private-tangle`.
+ let mut document: IotaDocument = IotaDocument::from_keypair_with_network(&keypair, network_name)?;
+
+ // Sign the DID Document with the default authentication key.
+ document.sign(keypair.secret())?;
+
+ println!("DID Document JSON > {:#}", document);
+
+ // Publish the DID Document to the Tangle.
+ let receipt: Receipt = client.publish_document(&document).await?;
+
+ println!("Publish Receipt > {:#?}", receipt);
+
+ Ok(())
+}
diff --git a/examples/low-level-api/revoke_vc.rs b/examples/low-level-api/revoke_vc.rs
index a71d6ec0b1..5ed5e9ee0a 100644
--- a/examples/low-level-api/revoke_vc.rs
+++ b/examples/low-level-api/revoke_vc.rs
@@ -41,7 +41,7 @@ async fn main() -> Result<()> {
let update_receipt = client.publish_document(&issuer_doc).await?;
// Log the resulting Identity update
- println!("Issuer Identity Update > {}", update_receipt.message_url());
+ println!("Issuer Identity Update > {}", update_receipt.message_url()?);
// Check the verifiable credential
let validation: CredentialValidation = common::check_credential(&client, &signed_vc).await?;
diff --git a/identity-account/src/account/builder.rs b/identity-account/src/account/builder.rs
index 70e3d99adc..b9a384e503 100644
--- a/identity-account/src/account/builder.rs
+++ b/identity-account/src/account/builder.rs
@@ -80,7 +80,7 @@ impl AccountBuilder {
self
.clients
.get_or_insert_with(HashMap::new)
- .insert(network, f(ClientBuilder::new().network(network)));
+ .insert(network.clone(), f(ClientBuilder::new().network(network)));
self
}
diff --git a/identity-iota/src/did/url/iota_did.rs b/identity-iota/src/did/url/iota_did.rs
index 109259dc65..984948efb7 100644
--- a/identity-iota/src/did/url/iota_did.rs
+++ b/identity-iota/src/did/url/iota_did.rs
@@ -246,8 +246,8 @@ impl IotaDID {
self.0.query_pairs()
}
- /// Returns the Tangle `network` of the `DID`.
- pub fn network(&self) -> Network {
+ /// Returns the Tangle `network` of the `DID`, if it is valid.
+ pub fn network(&self) -> Result {
Network::from_name(self.network_str())
}
diff --git a/identity-iota/src/error.rs b/identity-iota/src/error.rs
index df787c29ff..7b78d89741 100644
--- a/identity-iota/src/error.rs
+++ b/identity-iota/src/error.rs
@@ -27,8 +27,8 @@ pub enum Error {
InvalidDocumentAuthFragment,
#[error("Invalid Document - Authentication Type Not Supported")]
InvalidDocumentAuthType,
- #[error("Invalid DID Network")]
- InvalidDIDNetwork,
+ #[error("Invalid Network Name: {0}")]
+ InvalidNetworkName(&'static str),
#[error("Invalid Tryte Conversion")]
InvalidTryteConversion,
#[error("Invalid Transaction Bundle")]
@@ -51,4 +51,10 @@ pub enum Error {
CannotRemoveAuthMethod,
#[error("Cannot Revoke Verification Method")]
CannotRevokeMethod,
+ #[error("No Client Nodes Provided")]
+ NoClientNodesProvided,
+ #[error("No Explorer URL Set")]
+ NoExplorerURLSet,
+ #[error("Invalid Explorer Url")]
+ InvalidExplorerURL,
}
diff --git a/identity-iota/src/resolver.rs b/identity-iota/src/resolver.rs
index d911b79fd1..a4070daff3 100644
--- a/identity-iota/src/resolver.rs
+++ b/identity-iota/src/resolver.rs
@@ -49,9 +49,10 @@ impl ResolverMethod for ClientMap {
async fn read(&self, did: &CoreDID, input: InputMetadata) -> Result> {
let did: &IotaDID = IotaDID::try_from_borrowed(did).map_err(|_| Error::MissingResolutionDID)?;
+ let network = did.network().map_err(|_| Error::MissingResolutionDID)?;
self
- .client(did.network())
+ .client(network)
.await
.map_err(|_| Error::MissingResolutionDocument)?
.read(did.as_ref(), input)
diff --git a/identity-iota/src/tangle/client.rs b/identity-iota/src/tangle/client.rs
index 44ace8ce05..57ff96d480 100644
--- a/identity-iota/src/tangle/client.rs
+++ b/identity-iota/src/tangle/client.rs
@@ -13,6 +13,7 @@ use crate::chain::{ChainHistory, DiffChain, DocumentHistory};
use crate::did::DocumentDiff;
use crate::did::IotaDID;
use crate::did::IotaDocument;
+use crate::error::Error;
use crate::error::Result;
use crate::tangle::Message;
use crate::tangle::MessageId;
@@ -51,7 +52,11 @@ impl Client {
let mut client: iota_client::ClientBuilder = builder.builder;
if !builder.nodeset {
- client = client.with_node(builder.network.default_node_url().as_str())?;
+ if let Some(network_url) = builder.network.default_node_url() {
+ client = client.with_node(network_url.as_str())?;
+ } else {
+ return Err(Error::NoClientNodesProvided);
+ }
}
Ok(Self {
@@ -62,7 +67,7 @@ impl Client {
/// Returns the IOTA [`Network`] that the [`Client`] is configured to use.
pub fn network(&self) -> Network {
- self.network
+ self.network.clone()
}
/// Publishes an [`IotaDocument`] to the Tangle.
@@ -86,7 +91,7 @@ impl Client {
.finish()
.await
.map_err(Into::into)
- .map(|message| Receipt::new(self.network, message))
+ .map(|message| Receipt::new(self.network.clone(), message))
}
/// Fetch the [`IotaDocument`] specified by the given [`IotaDID`].
diff --git a/identity-iota/src/tangle/client_builder.rs b/identity-iota/src/tangle/client_builder.rs
index cb6eb8afb1..90f4a60aca 100644
--- a/identity-iota/src/tangle/client_builder.rs
+++ b/identity-iota/src/tangle/client_builder.rs
@@ -31,7 +31,7 @@ impl ClientBuilder {
/// Sets the IOTA Tangle network.
pub fn network(mut self, network: Network) -> Self {
- self.builder = self.builder.with_network(network.as_str());
+ self.builder = self.builder.with_network(&network.name());
self.network = network;
self
}
diff --git a/identity-iota/src/tangle/client_map.rs b/identity-iota/src/tangle/client_map.rs
index 80bd6f93e8..0a311df3bc 100644
--- a/identity-iota/src/tangle/client_map.rs
+++ b/identity-iota/src/tangle/client_map.rs
@@ -31,7 +31,7 @@ impl ClientMap {
pub fn from_client(client: Client) -> Self {
let data: State = State::new();
- data.insert(client.network, Arc::new(client));
+ data.insert(client.network.clone(), Arc::new(client));
Self { data }
}
@@ -49,32 +49,32 @@ impl ClientMap {
}
pub fn insert(&self, client: Client) {
- self.data.insert(client.network, Arc::new(client));
+ self.data.insert(client.network.clone(), Arc::new(client));
}
pub async fn publish_document(&self, document: &IotaDocument) -> Result {
- let network: Network = document.id().network();
+ let network: Network = document.id().network()?;
let client: Arc = self.client(network).await?;
client.publish_document(document).await
}
pub async fn publish_diff(&self, message_id: &MessageId, diff: &DocumentDiff) -> Result {
- let network: Network = diff.id().network();
+ let network: Network = diff.id().network()?;
let client: Arc = self.client(network).await?;
client.publish_diff(message_id, diff).await
}
pub async fn read_document(&self, did: &IotaDID) -> Result {
- let network: Network = did.network();
+ let network: Network = did.network()?;
let client: Arc = self.client(network).await?;
client.read_document(did).await
}
pub async fn read_document_chain(&self, did: &IotaDID) -> Result {
- let network: Network = did.network();
+ let network: Network = did.network()?;
let client: Arc = self.client(network).await?;
client.read_document_chain(did).await
@@ -85,7 +85,7 @@ impl ClientMap {
return Ok(Arc::clone(&client));
}
- let client: Arc = Client::from_network(network).await.map(Arc::new)?;
+ let client: Arc = Client::from_network(network.clone()).await.map(Arc::new)?;
self.data.insert(network, Arc::clone(&client));
diff --git a/identity-iota/src/tangle/network.rs b/identity-iota/src/tangle/network.rs
index 5bfa112afd..43f7679a66 100644
--- a/identity-iota/src/tangle/network.rs
+++ b/identity-iota/src/tangle/network.rs
@@ -1,9 +1,12 @@
// Copyright 2020-2021 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
+use std::borrow::Cow;
+
use identity_core::common::Url;
use crate::did::IotaDID;
+use crate::error::{Error, Result};
const MAIN_NETWORK_NAME: &str = "main";
const TEST_NETWORK_NAME: &str = "test";
@@ -16,65 +19,120 @@ lazy_static! {
}
/// The Tangle network to use ([`Mainnet`][Network::Mainnet] or [`Testnet`][Network::Testnet]).
-#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
+#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
pub enum Network {
#[serde(rename = "main")]
Mainnet,
#[serde(rename = "test")]
Testnet,
+ Other {
+ name: String,
+ explorer_url: Option,
+ },
}
impl Network {
- /// Parses the provided string to a `Network`.
+ /// Parses the provided string to a [Network].
+ ///
+ /// The inputs `"test"` and `"main"` will be mapped to the well-known [Testnet][Network::Testnet]
+ /// and [Mainnet][Network::Mainnet] variants, respectively.
+ /// Other inputs will return an instance of [Other][Network::Other].
///
- /// If the input is `"test"` then `Testnet` is returned, otherwise `Mainnet` is returned.
- pub fn from_name(string: &str) -> Self {
+ /// Note that the empty string is not a valid network name, and that names have to be compliant
+ /// with the IOTA DID Method spec, that is, be at most 6 characters long and only include
+ /// characters `0-9` or `a-z`.
+ pub fn from_name(string: &str) -> Result {
match string {
- TEST_NETWORK_NAME => Self::Testnet,
- _ => Self::Mainnet,
+ "" => Err(Error::InvalidNetworkName("name cannot be the empty string")),
+ TEST_NETWORK_NAME => Ok(Self::Testnet),
+ MAIN_NETWORK_NAME => Ok(Self::Mainnet),
+ other => {
+ Self::check_name_compliance(other)?;
+ Ok(Self::Other {
+ name: other.to_owned(),
+ explorer_url: None,
+ })
+ }
}
}
- /// Returns the `Network` the `IotaDID` is associated with.
- pub fn from_did(did: &IotaDID) -> Self {
+ /// Checks if a string is a spec-compliant network name.
+ fn check_name_compliance(string: &str) -> Result<()> {
+ if string.len() > 6 {
+ return Err(Error::InvalidNetworkName("name cannot exceed 6 characters"));
+ };
+
+ if !string.chars().all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()) {
+ return Err(Error::InvalidNetworkName(
+ "name may only contain characters `0-9` and `a-z`",
+ ));
+ }
+
+ Ok(())
+ }
+
+ /// Sets the explorer url if `self` is an `Other` variant.
+ ///
+ /// The `Url` needs to be a valid base url, i.e. `url.cannot_be_a_base()`
+ /// must be false. An [InvalidExplorerUrl][Error::InvalidExplorerURL] is returned otherwise.
+ pub fn set_explorer_url(&mut self, url: Url) -> Result<()> {
+ if url.cannot_be_a_base() {
+ return Err(Error::InvalidExplorerURL);
+ }
+
+ if let Self::Other { explorer_url, .. } = self {
+ explorer_url.replace(url);
+ }
+
+ Ok(())
+ }
+
+ /// Returns the [Network] the [IotaDID] is associated with, if it is a valid one.
+ pub fn try_from_did(did: &IotaDID) -> Result {
did.network()
}
/// Returns true if this network is the same network as the DID.
pub fn matches_did(self, did: &IotaDID) -> bool {
- did.network_str() == self.as_str()
+ did.network_str() == self.name()
}
/// Returns the default node URL of the Tangle network.
- pub fn default_node_url(self) -> &'static Url {
+ pub fn default_node_url(&self) -> Option<&'static Url> {
match self {
- Self::Mainnet => &*NODE_MAIN,
- Self::Testnet => &*NODE_TEST,
+ Self::Mainnet => Some(&*NODE_MAIN),
+ Self::Testnet => Some(&*NODE_TEST),
+ _ => None,
}
}
/// Returns the web explorer URL of the Tangle network.
- pub fn explorer_url(self) -> &'static Url {
+ pub fn explorer_url(&self) -> Result {
match self {
- Self::Mainnet => &*EXPLORER_MAIN,
- Self::Testnet => &*EXPLORER_TEST,
+ Self::Mainnet => Ok(EXPLORER_MAIN.clone()),
+ Self::Testnet => Ok(EXPLORER_TEST.clone()),
+ Self::Other {
+ explorer_url: Some(url),
+ ..
+ } => Ok(url.clone()),
+ _ => Err(Error::NoExplorerURLSet),
}
}
/// Returns the web explorer URL of the given `message`.
- pub fn message_url(&self, message_id: &str) -> Url {
- let mut url: Url = self.explorer_url().clone();
-
+ pub fn message_url(&self, message_id: &str) -> Result {
+ let mut url = self.explorer_url()?;
// unwrap is safe, the explorer URL is always a valid base URL
url.path_segments_mut().unwrap().push("message").push(message_id);
- url
+ Ok(url)
}
- /// Returns the name of the network as a static `str`.
- pub const fn as_str(self) -> &'static str {
+ /// Returns the name of the network.
+ pub fn name(&self) -> Cow<'static, str> {
match self {
- Self::Mainnet => MAIN_NETWORK_NAME,
- Self::Testnet => TEST_NETWORK_NAME,
+ Self::Mainnet => Cow::Borrowed(MAIN_NETWORK_NAME),
+ Self::Testnet => Cow::Borrowed(TEST_NETWORK_NAME),
+ Self::Other { name, .. } => Cow::Owned(name.clone()),
}
}
}
@@ -92,9 +150,35 @@ mod tests {
#[test]
fn test_from_name() {
- assert_eq!(Network::from_name("test"), Network::Testnet);
- assert_eq!(Network::from_name("main"), Network::Mainnet);
- assert_eq!(Network::from_name("anything"), Network::Mainnet);
+ assert_eq!(Network::from_name("test").unwrap(), Network::Testnet);
+ assert_eq!(Network::from_name("main").unwrap(), Network::Mainnet);
+ assert_eq!(
+ Network::from_name("6chars").unwrap(),
+ Network::Other {
+ name: "6chars".to_owned(),
+ explorer_url: None
+ }
+ );
+
+ assert!(matches!(
+ Network::from_name("7seven7").unwrap_err(),
+ Error::InvalidNetworkName("name cannot exceed 6 characters")
+ ));
+
+ assert!(matches!(
+ Network::from_name("täst").unwrap_err(),
+ Error::InvalidNetworkName("name may only contain characters `0-9` and `a-z`")
+ ));
+
+ assert!(matches!(
+ Network::from_name(" ").unwrap_err(),
+ Error::InvalidNetworkName("name may only contain characters `0-9` and `a-z`")
+ ));
+
+ assert!(matches!(
+ Network::from_name("").unwrap_err(),
+ Error::InvalidNetworkName("name cannot be the empty string")
+ ));
}
#[test]
@@ -107,4 +191,29 @@ mod tests {
assert!(Network::matches_did(Network::Testnet, &did));
assert!(!Network::matches_did(Network::Mainnet, &did));
}
+
+ #[test]
+ fn test_explorer_url() {
+ let testnet = Network::Testnet;
+
+ assert!(testnet.explorer_url().is_ok());
+
+ let mut other = Network::from_name("atoi").unwrap();
+
+ assert!(matches!(other.explorer_url().unwrap_err(), Error::NoExplorerURLSet));
+
+ // Try setting a `cannot_be_a_base` url.
+ assert!(matches!(
+ other
+ .set_explorer_url(Url::parse("data:text/plain,stuff").unwrap())
+ .unwrap_err(),
+ Error::InvalidExplorerURL
+ ));
+
+ let url = Url::parse("https://explorer.iota.org/testnet").unwrap();
+
+ assert!(other.set_explorer_url(url.clone()).is_ok());
+
+ assert_eq!(other.explorer_url().unwrap(), url);
+ }
}
diff --git a/identity-iota/src/tangle/receipt.rs b/identity-iota/src/tangle/receipt.rs
index 6b05fd835e..6b515ee9df 100644
--- a/identity-iota/src/tangle/receipt.rs
+++ b/identity-iota/src/tangle/receipt.rs
@@ -3,6 +3,7 @@
use identity_core::common::Url;
+use crate::error::Result;
use crate::tangle::Message;
use crate::tangle::MessageId;
use crate::tangle::Network;
@@ -29,7 +30,7 @@ impl Receipt {
/// Returns the associated IOTA Tangle `Network`.
pub fn network(&self) -> Network {
- self.network
+ self.network.clone()
}
/// Returns the message `id`.
@@ -48,7 +49,7 @@ impl Receipt {
}
/// Returns the web explorer URL of the associated `message`.
- pub fn message_url(&self) -> Url {
+ pub fn message_url(&self) -> Result {
self.network.message_url(&self.message_id.to_string())
}
}