diff --git a/.gitignore b/.gitignore index 52f081d501..e039fea6dc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ index.html !/bindings/wasm/static/index.html docs +# ignore IOTA build artifacts & package locks +build +identity_iota_core/packages/*/Move.lock + diff --git a/Cargo.toml b/Cargo.toml index 7dfcbcadd1..bbf08bd310 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,11 @@ +[workspace.package] +authors = ["IOTA Stiftung"] +edition = "2021" +homepage = "https://www.iota.org" +license = "Apache-2.0" +repository = "https://github.com/iotaledger/identity.rs" +rust-version = "1.65" + [workspace] resolver = "2" members = [ @@ -22,19 +30,11 @@ exclude = ["bindings/wasm", "bindings/grpc"] [workspace.dependencies] bls12_381_plus = { version = "0.8.17" } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } -thiserror = { version = "1.0", default-features = false } -strum = { version = "0.25", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0", default-features = false } +strum = { version = "0.25", default-features = false, features = ["std", "derive"] } +thiserror = { version = "1.0", default-features = false } json-proof-token = { version = "0.3.5" } zkryptium = { version = "0.2.2", default-features = false, features = ["bbsplus"] } -[workspace.package] -authors = ["IOTA Stiftung"] -edition = "2021" -homepage = "https://www.iota.org" -license = "Apache-2.0" -repository = "https://github.com/iotaledger/identity.rs" -rust-version = "1.65" - [workspace.lints.clippy] result_large_err = "allow" diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index 2b542712db..71f382926c 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -17,27 +17,32 @@ name = "identity-grpc" path = "src/main.rs" [dependencies] -anyhow = "1.0.75" +anyhow = "1.0" futures = { version = "0.3" } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" } identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "domain-linkage", "domain-linkage-fetch", "status-list-2021"] } +identity_jose = { path = "../../identity_jose" } +identity_storage = { path = "../../identity_storage", features = ["memstore"] } identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] } -iota-sdk = { version = "1.1.5", features = ["stronghold"] } -openssl = { version = "0.10", features = ["vendored"] } -prost = "0.12" +identity_sui_name_tbd = { path = "../../identity_sui_name_tbd" } +iota-sdk = { version = "1.1.2", features = ["stronghold"] } +iota-sdk-move = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk" } +prost = "0.13" rand = "0.8.5" -serde = { version = "1.0.193", features = ["derive", "alloc"] } -serde_json = { version = "1.0.108", features = ["alloc"] } -thiserror = "1.0.50" +serde = { version = "1.0", features = ["derive", "alloc"] } +serde_json = { version = "1.0", features = ["alloc"] } +thiserror = "1.0" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } tokio-stream = { version = "0.1.14", features = ["net"] } -tonic = "0.10" +tonic = "0.12" tracing = { version = "0.1.40", features = ["async-await"] } tracing-subscriber = "0.3.18" url = { version = "2.5", default-features = false } [dev-dependencies] +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto" } identity_storage = { path = "../../identity_storage", features = ["memstore"] } +jsonpath-rust = "0.7" [build-dependencies] -tonic-build = "0.10" +tonic-build = "0.12" diff --git a/bindings/grpc/proto/document.proto b/bindings/grpc/proto/document.proto index d25558c243..49f86af94d 100644 --- a/bindings/grpc/proto/document.proto +++ b/bindings/grpc/proto/document.proto @@ -5,8 +5,8 @@ syntax = "proto3"; package document; message CreateDIDRequest { - // An IOTA's bech32 encoded address. - string bech32_address = 1; + // KeyID for getting the public key from stronghold. + string key_id = 1; } message CreateDIDResponse { diff --git a/bindings/grpc/src/main.rs b/bindings/grpc/src/main.rs index 04927b1c9c..82650cabfa 100644 --- a/bindings/grpc/src/main.rs +++ b/bindings/grpc/src/main.rs @@ -1,11 +1,15 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::str::FromStr; + use anyhow::Context; use identity_grpc::server::GRpcServer; use identity_stronghold::StrongholdStorage; use iota_sdk::client::stronghold::StrongholdAdapter; -use iota_sdk::client::Client; + +use identity_sui_name_tbd::client::IdentityClientReadOnly; +use iota_sdk_move::types::base_types::ObjectID; #[tokio::main] #[tracing::instrument(err)] @@ -15,15 +19,19 @@ async fn main() -> anyhow::Result<()> { let api_endpoint = std::env::var("API_ENDPOINT")?; - let client: Client = Client::builder() - .with_primary_node(&api_endpoint, None)? - .finish() - .await?; + let identity_iota_pkg_id = std::env::var("IDENTITY_IOTA_PKG_ID")?; + + let identity_pkg_id = ObjectID::from_str(&identity_iota_pkg_id)?; + + let iota_client = iota_sdk_move::IotaClientBuilder::default().build(api_endpoint).await?; + + let read_only_client = IdentityClientReadOnly::new(iota_client, identity_pkg_id).await?; + let stronghold = init_stronghold()?; let addr = "0.0.0.0:50051".parse()?; tracing::info!("gRPC server listening on {}", addr); - GRpcServer::new(client, stronghold).serve(addr).await?; + GRpcServer::new(read_only_client, stronghold).serve(addr).await?; Ok(()) } diff --git a/bindings/grpc/src/server.rs b/bindings/grpc/src/server.rs index c7fa5b527c..976d00c15a 100644 --- a/bindings/grpc/src/server.rs +++ b/bindings/grpc/src/server.rs @@ -4,7 +4,7 @@ use std::net::SocketAddr; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::Client; +use identity_sui_name_tbd::client::IdentityClientReadOnly; use tonic::transport::server::Router; use tonic::transport::server::Server; @@ -17,7 +17,7 @@ pub struct GRpcServer { } impl GRpcServer { - pub fn new(client: Client, stronghold: StrongholdStorage) -> Self { + pub fn new(client: IdentityClientReadOnly, stronghold: StrongholdStorage) -> Self { let router = Server::builder().add_routes(services::routes(&client, &stronghold)); Self { router, stronghold } } diff --git a/bindings/grpc/src/services/credential/jwt.rs b/bindings/grpc/src/services/credential/jwt.rs index 6cfb3368e6..ad2012469f 100644 --- a/bindings/grpc/src/services/credential/jwt.rs +++ b/bindings/grpc/src/services/credential/jwt.rs @@ -12,7 +12,7 @@ use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwsSignatureOptions; use identity_iota::storage::Storage; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::Client; +use identity_sui_name_tbd::client::IdentityClientReadOnly; use tonic::Request; use tonic::Response; use tonic::Status; @@ -31,9 +31,9 @@ pub struct JwtService { } impl JwtService { - pub fn new(client: &Client, stronghold: &StrongholdStorage) -> Self { + pub fn new(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> Self { let mut resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler(client.clone()); Self { resolver, storage: Storage::new(stronghold.clone(), stronghold.clone()), @@ -80,6 +80,6 @@ impl JwtSvc for JwtService { } } -pub fn service(client: &Client, stronghold: &StrongholdStorage) -> JwtServer { +pub fn service(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> JwtServer { JwtServer::new(JwtService::new(client, stronghold)) } diff --git a/bindings/grpc/src/services/credential/mod.rs b/bindings/grpc/src/services/credential/mod.rs index 8d71ccacee..d2621fae2c 100644 --- a/bindings/grpc/src/services/credential/mod.rs +++ b/bindings/grpc/src/services/credential/mod.rs @@ -6,10 +6,10 @@ pub mod revocation; pub mod validation; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::Client; -use tonic::transport::server::RoutesBuilder; +use identity_sui_name_tbd::client::IdentityClientReadOnly; +use tonic::service::RoutesBuilder; -pub fn init_services(routes: &mut RoutesBuilder, client: &Client, stronghold: &StrongholdStorage) { +pub fn init_services(routes: &mut RoutesBuilder, client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) { routes.add_service(revocation::service(client)); routes.add_service(jwt::service(client, stronghold)); routes.add_service(validation::service(client)); diff --git a/bindings/grpc/src/services/credential/revocation.rs b/bindings/grpc/src/services/credential/revocation.rs index d637bce22e..320081779a 100644 --- a/bindings/grpc/src/services/credential/revocation.rs +++ b/bindings/grpc/src/services/credential/revocation.rs @@ -12,7 +12,7 @@ use identity_iota::credential::RevocationBitmapStatus; use identity_iota::credential::{self}; use identity_iota::prelude::IotaDocument; use identity_iota::prelude::Resolver; -use iota_sdk::client::Client; +use identity_sui_name_tbd::client::IdentityClientReadOnly; use prost::bytes::Bytes; use serde::Deserialize; use serde::Serialize; @@ -107,9 +107,9 @@ pub struct CredentialVerifier { } impl CredentialVerifier { - pub fn new(client: &Client) -> Self { + pub fn new(client: &IdentityClientReadOnly) -> Self { let mut resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler(client.clone()); Self { resolver } } } @@ -156,6 +156,6 @@ impl CredentialRevocation for CredentialVerifier { } } -pub fn service(client: &Client) -> CredentialRevocationServer { +pub fn service(client: &IdentityClientReadOnly) -> CredentialRevocationServer { CredentialRevocationServer::new(CredentialVerifier::new(client)) } diff --git a/bindings/grpc/src/services/credential/validation.rs b/bindings/grpc/src/services/credential/validation.rs index fb218b727b..fd8cfd92bc 100644 --- a/bindings/grpc/src/services/credential/validation.rs +++ b/bindings/grpc/src/services/credential/validation.rs @@ -16,7 +16,7 @@ use identity_iota::credential::StatusCheck; use identity_iota::iota::IotaDID; use identity_iota::resolver; use identity_iota::resolver::Resolver; -use iota_sdk::client::Client; +use identity_sui_name_tbd::client::IdentityClientReadOnly; use _credentials::vc_validation_server::VcValidation; use _credentials::vc_validation_server::VcValidationServer; @@ -63,9 +63,9 @@ pub struct VcValidator { } impl VcValidator { - pub fn new(client: &Client) -> Self { + pub fn new(client: &IdentityClientReadOnly) -> Self { let mut resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler(client.clone()); Self { resolver } } } @@ -130,6 +130,6 @@ impl VcValidation for VcValidator { } } -pub fn service(client: &Client) -> VcValidationServer { +pub fn service(client: &IdentityClientReadOnly) -> VcValidationServer { VcValidationServer::new(VcValidator::new(client)) } diff --git a/bindings/grpc/src/services/document.rs b/bindings/grpc/src/services/document.rs index 0ed1298637..31b57f9aab 100644 --- a/bindings/grpc/src/services/document.rs +++ b/bindings/grpc/src/services/document.rs @@ -6,19 +6,23 @@ use _document::document_service_server::DocumentServiceServer; use _document::CreateDidRequest; use _document::CreateDidResponse; use identity_iota::core::ToJson; -use identity_iota::iota::IotaClientExt; +use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::iota::StateMetadataDocument; +use identity_iota::iota::StateMetadataEncoding; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkStorageDocumentError; use identity_iota::storage::Storage; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; +use identity_storage::KeyId; +use identity_storage::KeyStorageErrorKind; +use identity_storage::StorageSigner; use identity_stronghold::StrongholdStorage; use identity_stronghold::ED25519_KEY_TYPE; -use iota_sdk::client::Client; -use iota_sdk::types::block::address::Address; -use std::error::Error as _; +use identity_sui_name_tbd::client::IdentityClient; +use identity_sui_name_tbd::client::IdentityClientReadOnly; +use identity_sui_name_tbd::transaction::Transaction; use tonic::Code; use tonic::Request; use tonic::Response; @@ -30,31 +34,31 @@ mod _document { #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("The provided address is not a valid bech32 encoded address")] - InvalidAddress, #[error(transparent)] IotaClientError(identity_iota::iota::Error), #[error(transparent)] StorageError(JwkStorageDocumentError), + #[error(transparent)] + StrongholdError(identity_iota::core::SingleStructError), + #[error(transparent)] + IdentityClientError(identity_sui_name_tbd::Error), + #[error("did error : {0}")] + DIDError(String), } impl From for Status { fn from(value: Error) -> Self { - let code = match &value { - Error::InvalidAddress => Code::InvalidArgument, - _ => Code::Internal, - }; - Status::new(code, value.to_string()) + Status::new(Code::Internal, value.to_string()) } } pub struct DocumentSvc { storage: Storage, - client: Client, + client: IdentityClientReadOnly, } impl DocumentSvc { - pub fn new(client: &Client, stronghold: &StrongholdStorage) -> Self { + pub fn new(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> Self { Self { storage: Storage::new(stronghold.clone(), stronghold.clone()), client: client.clone(), @@ -72,35 +76,60 @@ impl DocumentService for DocumentSvc { err, )] async fn create(&self, req: Request) -> Result, Status> { - let CreateDidRequest { bech32_address } = req.into_inner(); - let address = Address::try_from_bech32(&bech32_address).map_err(|_| Error::InvalidAddress)?; - let network_name = self.client.network_name().await.map_err(Error::IotaClientError)?; + let CreateDidRequest { key_id } = req.into_inner(); + + let key_id = KeyId::new(key_id); + let pub_key = self + .storage + .key_id_storage() + .get_public_key(&key_id) + .await + .map_err(Error::StrongholdError)?; + + let network_name = self.client.network(); + + let storage = StorageSigner::new(&self.storage, key_id, pub_key); + + let identity_client = IdentityClient::new(self.client.clone(), storage) + .await + .map_err(Error::IdentityClientError)?; - let mut document = IotaDocument::new(&network_name); + let iota_doc = { + let doc = IotaDocument::new(network_name); + + let iota_doc_md = StateMetadataDocument::from(doc); + + iota_doc_md.pack(StateMetadataEncoding::Json).expect("shouldn't fail") + }; + + let mut created_identity = identity_client + .create_identity(&iota_doc) + .finish() + .execute(&identity_client) + .await + .map_err(Error::IdentityClientError)?; + + let did = + IotaDID::parse(format!("did:iota:{}", created_identity.id())).map_err(|e| Error::DIDError(e.to_string()))?; + + let mut document = IotaDocument::new_with_id(did.clone()); let fragment = document .generate_method( &self.storage, ED25519_KEY_TYPE.clone(), JwsAlgorithm::EdDSA, - None, + Some(identity_client.signer().key_id().as_str()), MethodScope::VerificationMethod, ) .await .map_err(Error::StorageError)?; - let alias_output = self - .client - .new_did_output(address, document, None) - .await - .map_err(Error::IotaClientError)?; - - let document = self - .client - .publish_did_output(self.storage.key_storage().as_secret_manager(), alias_output) + created_identity + .update_did_document(document.clone()) + .finish() + .execute(&identity_client) .await - .map_err(Error::IotaClientError) - .inspect_err(|e| tracing::error!("{:?}", e.source()))?; - let did = document.id(); + .map_err(Error::IdentityClientError)?; Ok(Response::new(CreateDidResponse { document_json: document.to_json().unwrap(), @@ -110,6 +139,6 @@ impl DocumentService for DocumentSvc { } } -pub fn service(client: &Client, stronghold: &StrongholdStorage) -> DocumentServiceServer { +pub fn service(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> DocumentServiceServer { DocumentServiceServer::new(DocumentSvc::new(client, stronghold)) } diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index bb8b214982..560495c628 100644 --- a/bindings/grpc/src/services/domain_linkage.rs +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -29,7 +29,7 @@ use identity_iota::did::CoreDID; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; use identity_iota::resolver::Resolver; -use iota_sdk::client::Client; +use identity_sui_name_tbd::client::IdentityClientReadOnly; use serde::Deserialize; use serde::Serialize; use thiserror::Error; @@ -109,9 +109,9 @@ pub struct DomainLinkageService { } impl DomainLinkageService { - pub fn new(client: &Client) -> Self { + pub fn new(client: &IdentityClientReadOnly) -> Self { let mut resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler(client.clone()); Self { resolver } } @@ -421,6 +421,6 @@ impl DomainLinkage for DomainLinkageService { } } -pub fn service(client: &Client) -> DomainLinkageServer { +pub fn service(client: &IdentityClientReadOnly) -> DomainLinkageServer { DomainLinkageServer::new(DomainLinkageService::new(client)) } diff --git a/bindings/grpc/src/services/mod.rs b/bindings/grpc/src/services/mod.rs index 00abe17ce1..d352b8f858 100644 --- a/bindings/grpc/src/services/mod.rs +++ b/bindings/grpc/src/services/mod.rs @@ -10,11 +10,11 @@ pub mod status_list_2021; pub mod utils; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::Client; -use tonic::transport::server::Routes; -use tonic::transport::server::RoutesBuilder; +use identity_sui_name_tbd::client::IdentityClientReadOnly; +use tonic::service::Routes; +use tonic::service::RoutesBuilder; -pub fn routes(client: &Client, stronghold: &StrongholdStorage) -> Routes { +pub fn routes(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> Routes { let mut routes = RoutesBuilder::default(); routes.add_service(health_check::service()); credential::init_services(&mut routes, client, stronghold); diff --git a/bindings/grpc/src/services/sd_jwt.rs b/bindings/grpc/src/services/sd_jwt.rs index af792e51f6..95263f8c87 100644 --- a/bindings/grpc/src/services/sd_jwt.rs +++ b/bindings/grpc/src/services/sd_jwt.rs @@ -20,7 +20,7 @@ use identity_iota::iota::IotaDocument; use identity_iota::resolver::Resolver; use identity_iota::sd_jwt_payload::SdJwt; use identity_iota::sd_jwt_payload::SdObjectDecoder; -use iota_sdk::client::Client; +use identity_sui_name_tbd::client::IdentityClientReadOnly; use serde::Deserialize; use serde::Serialize; use thiserror::Error; @@ -91,9 +91,9 @@ pub struct SdJwtService { } impl SdJwtService { - pub fn new(client: &Client) -> Self { + pub fn new(client: &IdentityClientReadOnly) -> Self { let mut resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler(client.clone()); Self { resolver } } } @@ -159,6 +159,6 @@ impl Verification for SdJwtService { } } -pub fn service(client: &Client) -> VerificationServer { +pub fn service(client: &IdentityClientReadOnly) -> VerificationServer { VerificationServer::new(SdJwtService::new(client)) } diff --git a/bindings/grpc/tests/api/credential_revocation_check.rs b/bindings/grpc/tests/api/credential_revocation_check.rs index 9e92197c72..a2dc1722d7 100644 --- a/bindings/grpc/tests/api/credential_revocation_check.rs +++ b/bindings/grpc/tests/api/credential_revocation_check.rs @@ -7,9 +7,11 @@ use identity_iota::credential::RevocationBitmap; use identity_iota::credential::RevocationBitmapStatus; use identity_iota::credential::{self}; use identity_iota::did::DID; +use identity_stronghold::StrongholdStorage; use serde_json::json; use crate::credential_revocation_check::credentials::RevocationCheckRequest; +use crate::helpers::make_stronghold; use crate::helpers::Entity; use crate::helpers::TestServer; @@ -21,7 +23,10 @@ mod credentials { async fn checking_status_of_credential_works() -> anyhow::Result<()> { let server = TestServer::new().await; let client = server.client(); - let mut issuer = Entity::new(); + + let stronghold = StrongholdStorage::new(make_stronghold()); + + let mut issuer = Entity::new_with_stronghold(stronghold); issuer.create_did(client).await?; let mut subject = Entity::new(); @@ -61,7 +66,7 @@ async fn checking_status_of_credential_works() -> anyhow::Result<()> { // Revoke credential issuer - .update_document(&client, |mut doc| { + .update_document(client, |mut doc| { doc.revoke_credentials("my-revocation-service", &[3]).ok().map(|_| doc) }) .await?; diff --git a/bindings/grpc/tests/api/credential_validation.rs b/bindings/grpc/tests/api/credential_validation.rs index f1bfedf100..dbb8bb1700 100644 --- a/bindings/grpc/tests/api/credential_validation.rs +++ b/bindings/grpc/tests/api/credential_validation.rs @@ -62,8 +62,8 @@ async fn credential_validation() -> anyhow::Result<()> { .unwrap() .create_credential_jwt( &credential, - &issuer.storage(), - &issuer.fragment().unwrap(), + issuer.storage(), + issuer.fragment().unwrap(), &JwsSignatureOptions::default(), None, ) @@ -128,8 +128,8 @@ async fn revoked_credential_validation() -> anyhow::Result<()> { .unwrap() .create_credential_jwt( &credential, - &issuer.storage(), - &issuer.fragment().unwrap(), + issuer.storage(), + issuer.fragment().unwrap(), &JwsSignatureOptions::default(), None, ) diff --git a/bindings/grpc/tests/api/did_document_creation.rs b/bindings/grpc/tests/api/did_document_creation.rs index 394217e7a3..9a7d140343 100644 --- a/bindings/grpc/tests/api/did_document_creation.rs +++ b/bindings/grpc/tests/api/did_document_creation.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use identity_stronghold::StrongholdStorage; -use iota_sdk::types::block::address::ToBech32Ext; +use identity_sui_name_tbd::utils::request_funds; + use tonic::Request; -use crate::helpers::get_address_with_funds; +use crate::helpers::get_address; use crate::helpers::make_stronghold; use crate::helpers::Entity; use crate::helpers::TestServer; -use crate::helpers::FAUCET_ENDPOINT; + use _document::document_service_client::DocumentServiceClient; use _document::CreateDidRequest; @@ -21,21 +22,16 @@ mod _document { async fn did_document_creation() -> anyhow::Result<()> { let stronghold = StrongholdStorage::new(make_stronghold()); let server = TestServer::new_with_stronghold(stronghold.clone()).await; - let api_client = server.client(); - let hrp = api_client.get_bech32_hrp().await?; let user = Entity::new_with_stronghold(stronghold); - let user_address = get_address_with_funds( - api_client, - user.storage().key_storage().as_secret_manager(), - FAUCET_ENDPOINT, - ) - .await?; + let (user_address, key_id, _) = get_address(user.storage()).await?; + + request_funds(&user_address).await?; let mut grpc_client = DocumentServiceClient::connect(server.endpoint()).await?; grpc_client .create(Request::new(CreateDidRequest { - bech32_address: user_address.to_bech32(hrp).to_string(), + key_id: key_id.as_str().to_string(), })) .await?; diff --git a/bindings/grpc/tests/api/domain_linkage.rs b/bindings/grpc/tests/api/domain_linkage.rs index 4870c74a8d..05819710df 100644 --- a/bindings/grpc/tests/api/domain_linkage.rs +++ b/bindings/grpc/tests/api/domain_linkage.rs @@ -69,7 +69,7 @@ async fn prepare_test() -> anyhow::Result<(TestServer, Url, Url, String, Jwt)> { let service_url: DIDUrl = did.clone().join("#domain-linkage")?; let linked_domain_service: LinkedDomainService = LinkedDomainService::new(service_url, domains, Object::new())?; issuer - .update_document(&api_client, |mut doc| { + .update_document(api_client, |mut doc| { doc.insert_service(linked_domain_service.into()).ok().map(|_| doc) }) .await?; @@ -101,8 +101,8 @@ async fn prepare_test() -> anyhow::Result<(TestServer, Url, Url, String, Jwt)> { let jwt: Jwt = updated_did_document .create_credential_jwt( &domain_linkage_credential, - &issuer.storage(), - &issuer + issuer.storage(), + issuer .fragment() .ok_or_else(|| anyhow::anyhow!("no fragment for issuer"))?, &JwsSignatureOptions::default(), diff --git a/bindings/grpc/tests/api/health_check.rs b/bindings/grpc/tests/api/health_check.rs index d8ea486269..ef3a703c55 100644 --- a/bindings/grpc/tests/api/health_check.rs +++ b/bindings/grpc/tests/api/health_check.rs @@ -1,13 +1,13 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use health_check::health_check_client::HealthCheckClient; -use health_check::HealthCheckRequest; -use health_check::HealthCheckResponse; +use _health_check::health_check_client::HealthCheckClient; +use _health_check::HealthCheckRequest; +use _health_check::HealthCheckResponse; use crate::helpers::TestServer; -mod health_check { +mod _health_check { tonic::include_proto!("health_check"); } diff --git a/bindings/grpc/tests/api/helpers.rs b/bindings/grpc/tests/api/helpers.rs index c307213db7..3d742cb779 100644 --- a/bindings/grpc/tests/api/helpers.rs +++ b/bindings/grpc/tests/api/helpers.rs @@ -1,49 +1,67 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use anyhow::anyhow; use anyhow::Context; -use identity_iota::iota::IotaClientExt; +use fastcrypto::ed25519::Ed25519PublicKey; +use fastcrypto::traits::ToFromBytes; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::iota::NetworkName; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; +use identity_jose::jwk::Jwk; use identity_storage::key_id_storage::KeyIdMemstore; use identity_storage::key_storage::JwkMemStore; use identity_storage::JwkDocumentExt; use identity_storage::JwkStorage; +use identity_storage::KeyId; use identity_storage::KeyIdStorage; +use identity_storage::KeyType; use identity_storage::Storage; +use identity_storage::StorageSigner; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::api::GetAddressesOptions; -use iota_sdk::client::node_api::indexer::query_parameters::QueryParameter; +use identity_sui_name_tbd::client::IdentityClient; +use identity_sui_name_tbd::client::IdentityClientReadOnly; +use identity_sui_name_tbd::transaction::Transaction; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; use iota_sdk::client::stronghold::StrongholdAdapter; -use iota_sdk::client::Client; use iota_sdk::client::Password; -use iota_sdk::crypto::keys::bip39; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::Bech32Address; -use iota_sdk::types::block::address::Hrp; -use iota_sdk::types::block::output::AliasOutputBuilder; +use iota_sdk_move::types::base_types::IotaAddress; +use iota_sdk_move::types::base_types::ObjectID; +use iota_sdk_move::IotaClient; +use iota_sdk_move::IotaClientBuilder; +use jsonpath_rust::JsonPathQuery; use rand::distributions::Alphanumeric; use rand::distributions::DistString; use rand::thread_rng; +use serde_json::Value; +use std::io::Write; use std::net::SocketAddr; use std::path::PathBuf; + use tokio::net::TcpListener; +use tokio::process::Command; +use tokio::sync::OnceCell; use tokio::task::JoinHandle; use tonic::transport::Uri; +const TEST_GAS_BUDGET: u64 = 50_000_000; + +type MemStorage = Storage; -pub type MemStorage = Storage; +const FAUCET_ENDPOINT: &str = "http://localhost:9123/gas"; -pub const API_ENDPOINT: &str = "http://localhost"; -pub const FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue"; +static PACKAGE_ID: OnceCell = OnceCell::const_new(); + +const SCRIPT_DIR: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../", + "identity_sui_name_tbd", + "/scripts" +); +const CACHED_PKG_ID: &str = "/tmp/identity_iota_pkg_id.txt"; -#[derive(Debug)] pub struct TestServer { - client: Client, + client: IdentityClientReadOnly, addr: SocketAddr, _handle: JoinHandle>, } @@ -67,20 +85,28 @@ impl TestServer { .expect("Failed to bind to random OS's port"); let addr = listener.local_addr().unwrap(); - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None) - .unwrap() - .finish() + let iota_client = IotaClientBuilder::default() + .build_localnet() .await .expect("Failed to connect to API's endpoint"); - let server = identity_grpc::server::GRpcServer::new(client.clone(), stronghold) + let identity_pkg_id = PACKAGE_ID + .get_or_try_init(|| init(&iota_client)) + .await + .copied() + .expect("failed to publish package ID"); + + let identity_client = IdentityClientReadOnly::new(iota_client, identity_pkg_id) + .await + .expect("Failed to build Identity client"); + + let server = identity_grpc::server::GRpcServer::new(identity_client.clone(), stronghold) .into_router() .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)); TestServer { _handle: tokio::spawn(server), addr, - client, + client: identity_client, } } @@ -90,31 +116,37 @@ impl TestServer { .expect("Failed to parse server's URI") } - pub fn client(&self) -> &Client { + pub fn client(&self) -> &IdentityClientReadOnly { &self.client } } pub async fn create_did( - client: &Client, - secret_manager: &mut SecretManager, + client: &IdentityClientReadOnly, storage: &Storage, -) -> anyhow::Result<(Address, IotaDocument, String)> +) -> anyhow::Result<(IotaAddress, IotaDocument, KeyId, String)> where K: JwkStorage, I: KeyIdStorage, { - let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT) - .await - .context("failed to get address with funds")?; + let (address, key_id, pub_key_jwk) = get_address(storage).await.context("failed to get address with funds")?; - let network_name = client.network_name().await?; - let (document, fragment): (IotaDocument, String) = create_did_document(&network_name, storage).await?; - let alias_output = client.new_did_output(address, document, None).await?; + // Fund the account + request_faucet_funds(address, FAUCET_ENDPOINT).await?; - let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; + let signer = StorageSigner::new(storage, key_id.clone(), pub_key_jwk); - Ok((address, document, fragment)) + let identity_client = IdentityClient::new(client.clone(), signer).await?; + + let network_name = client.network(); + let (document, fragment): (IotaDocument, String) = create_did_document(network_name, storage).await?; + + let document = identity_client + .publish_did_document(document) + .execute(&identity_client) + .await?; + + Ok((address, document, key_id, fragment)) } /// Creates an example DID document with the given `network_name`. @@ -144,77 +176,57 @@ where Ok((document, fragment)) } -/// Generates an address from the given [`SecretManager`] and adds funds from the faucet. -pub async fn get_address_with_funds( - client: &Client, - stronghold: &SecretManager, - faucet_endpoint: &str, -) -> anyhow::Result
{ - let address = get_address(client, stronghold).await?; - - request_faucet_funds(client, address, faucet_endpoint) - .await - .context("failed to request faucet funds")?; +/// Generates a new Ed25519 key pair +pub async fn get_address(storage: &Storage) -> anyhow::Result<(IotaAddress, KeyId, Jwk)> +where + K: JwkStorage, + I: KeyIdStorage, +{ + let generated_key = storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await?; - Ok(*address) -} + let key_id = generated_key.key_id; -/// Initializes the [`SecretManager`] with a new mnemonic, if necessary, -/// and generates an address from the given [`SecretManager`]. -pub async fn get_address(client: &Client, secret_manager: &SecretManager) -> anyhow::Result { - let random: [u8; 32] = rand::random(); - let mnemonic = bip39::wordlist::encode(random.as_ref(), &bip39::wordlist::ENGLISH) - .map_err(|err| anyhow::anyhow!(format!("{err:?}")))?; - - if let SecretManager::Stronghold(ref stronghold) = secret_manager { - match stronghold.store_mnemonic(mnemonic).await { - Ok(()) => (), - Err(iota_sdk::client::stronghold::Error::MnemonicAlreadyStored) => (), - Err(err) => anyhow::bail!(err), - } - } else { - anyhow::bail!("expected a `StrongholdSecretManager`"); - } + let pub_key_jwt = generated_key.jwk.to_public().expect("should not fail"); + let pub_key_bytes = pub_key_jwt + .try_okp_params() + .map(|key| identity_jose::jwu::decode_b64(key.x.clone()).expect("should be decodeable"))?; - let bech32_hrp: Hrp = client.get_bech32_hrp().await?; - let address: Bech32Address = secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_range(0..1) - .with_bech32_hrp(bech32_hrp), - ) - .await?[0]; + let address = Ed25519PublicKey::from_bytes(&pub_key_bytes)?; - Ok(address) + Ok((IotaAddress::from(&address), key_id, pub_key_jwt)) } /// Requests funds from the faucet for the given `address`. -async fn request_faucet_funds(client: &Client, address: Bech32Address, faucet_endpoint: &str) -> anyhow::Result<()> { - iota_sdk::client::request_funds_from_faucet(faucet_endpoint, &address).await?; - - tokio::time::timeout(std::time::Duration::from_secs(45), async { - loop { - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - - let balance = get_address_balance(client, &address) - .await - .context("failed to get address balance")?; - if balance > 0 { - break; - } - } - Ok::<(), anyhow::Error>(()) - }) - .await - .context("maximum timeout exceeded")??; +async fn request_faucet_funds(address: IotaAddress, faucet_endpoint: &str) -> anyhow::Result<()> { + let output = Command::new("iota") + .arg("client") + .arg("faucet") + .arg("--address") + .arg(address.to_string()) + .arg("--url") + .arg(faucet_endpoint) + .arg("--json") + .output() + .await + .context("Failed to execute command")?; + + // Check if the output is success + if !output.status.success() { + anyhow::bail!( + "Failed to request funds from faucet: {}", + std::str::from_utf8(&output.stderr).unwrap() + ); + } Ok(()) } pub struct Entity { - secret_manager: SecretManager, storage: Storage, - did: Option<(Address, IotaDocument, String)>, + did: Option<(IotaAddress, IotaDocument, KeyId, String)>, } pub fn random_password(len: usize) -> Password { @@ -232,14 +244,9 @@ pub fn random_stronghold_path() -> PathBuf { impl Default for Entity { fn default() -> Self { - let secret_manager = SecretManager::Stronghold(make_stronghold()); let storage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - Self { - secret_manager, - storage, - did: None, - } + Self { storage, did: None } } } @@ -251,25 +258,17 @@ impl Entity { impl Entity { pub fn new_with_stronghold(s: StrongholdStorage) -> Self { - let secret_manager = SecretManager::Stronghold(make_stronghold()); let storage = Storage::new(s.clone(), s); - Self { - secret_manager, - storage, - did: None, - } + Self { storage, did: None } } } impl Entity { - pub async fn create_did(&mut self, client: &Client) -> anyhow::Result<()> { - let Entity { - secret_manager, - storage, - did, - } = self; - *did = Some(create_did(client, secret_manager, storage).await?); + pub async fn create_did(&mut self, client: &IdentityClientReadOnly) -> anyhow::Result<()> { + let Entity { storage, did, .. } = self; + + *did = Some(create_did(client, storage).await?); Ok(()) } @@ -279,53 +278,40 @@ impl Entity { } pub fn document(&self) -> Option<&IotaDocument> { - self.did.as_ref().map(|(_, doc, _)| doc) + self.did.as_ref().map(|(_, doc, _, _)| doc) } pub fn fragment(&self) -> Option<&str> { - self.did.as_ref().map(|(_, _, frag)| frag.as_ref()) + self.did.as_ref().map(|(_, _, _, frag)| frag.as_ref()) } +} - pub async fn update_document(&mut self, client: &Client, f: F) -> anyhow::Result<()> +impl Entity { + pub async fn update_document(&mut self, client: &IdentityClientReadOnly, f: F) -> anyhow::Result<()> where F: FnOnce(IotaDocument) -> Option, { - let (address, doc, fragment) = self.did.take().context("Missing doc")?; + let (address, doc, key_id, fragment) = self.did.take().context("Missing doc")?; let mut new_doc = f(doc.clone()); if let Some(doc) = new_doc.take() { - let alias_output = client.update_did_output(doc.clone()).await?; - let rent_structure = client.get_rent_structure().await?; - let alias_output = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; + let Entity { storage, .. } = self; - new_doc = Some(client.publish_did_output(&self.secret_manager, alias_output).await?); - } + let public_key = storage.key_id_storage().get_public_key(&key_id).await?; + let signer = StorageSigner::new(storage, key_id.clone(), public_key); - self.did = Some((address, new_doc.unwrap_or(doc), fragment)); + let identity_client = IdentityClient::new(client.clone(), signer).await?; - Ok(()) - } -} -/// Returns the balance of the given Bech32-encoded `address`. -async fn get_address_balance(client: &Client, address: &Bech32Address) -> anyhow::Result { - let output_ids = client - .basic_output_ids(vec![ - QueryParameter::Address(address.to_owned()), - QueryParameter::HasExpiration(false), - QueryParameter::HasTimelock(false), - QueryParameter::HasStorageDepositReturn(false), - ]) - .await?; + new_doc = Some( + identity_client + .publish_did_document_update(doc, TEST_GAS_BUDGET) + .await?, + ); + } - let outputs = client.get_outputs(&output_ids).await?; + self.did = Some((address, new_doc.unwrap_or(doc), key_id, fragment)); - let mut total_amount = 0; - for output_response in outputs { - total_amount += output_response.output().amount(); + Ok(()) } - - Ok(total_amount) } pub fn make_stronghold() -> StrongholdAdapter { @@ -334,3 +320,79 @@ pub fn make_stronghold() -> StrongholdAdapter { .build(random_stronghold_path()) .expect("Failed to create temporary stronghold") } + +async fn get_active_address() -> anyhow::Result { + Command::new("iota") + .arg("client") + .arg("active-address") + .arg("--json") + .output() + .await + .context("Failed to execute command") + .and_then(|output| Ok(serde_json::from_slice::(&output.stdout)?)) +} + +async fn init(iota_client: &IotaClient) -> anyhow::Result { + let network_id = iota_client.read_api().get_chain_identifier().await?; + let address = get_active_address().await?; + + // Request Funds + + request_faucet_funds(address, FAUCET_ENDPOINT).await.unwrap(); + + if let Ok(id) = std::env::var("IDENTITY_IOTA_PKG_ID").or(get_cached_id(&network_id).await) { + std::env::set_var("IDENTITY_IOTA_PKG_ID", id.clone()); + id.parse().context("failed to parse object id from str") + } else { + publish_package(address).await + } +} + +async fn get_cached_id(network_id: &str) -> anyhow::Result { + let cache = tokio::fs::read_to_string(CACHED_PKG_ID).await?; + let (cached_id, cached_network_id) = cache.split_once(';').ok_or(anyhow!("Invalid or empty cached data"))?; + + if cached_network_id == network_id { + Ok(cached_id.to_owned()) + } else { + Err(anyhow!("A network change has invalidated the cached data")) + } +} + +async fn publish_package(active_address: IotaAddress) -> anyhow::Result { + let output = Command::new("sh") + .current_dir(SCRIPT_DIR) + .arg("publish_identity_package.sh") + .output() + .await?; + + if !output.status.success() { + anyhow::bail!( + "Failed to publish move package: \n\n{}\n\n{}", + std::str::from_utf8(&output.stdout).unwrap(), + std::str::from_utf8(&output.stderr).unwrap() + ); + } + + let publish_result = { + let output_str = std::str::from_utf8(&output.stdout).unwrap(); + let start_of_json = output_str.find('{').ok_or(anyhow!("No json in output"))?; + serde_json::from_str::(output_str[start_of_json..].trim())? + }; + + let package_id = publish_result + .path("$.objectChanges[?(@.type == 'published')].packageId") + .map_err(|e| anyhow!("Failed to parse JSONPath: {e}")) + .and_then(|value| Ok(serde_json::from_value::>(value)?))? + .first() + .copied() + .ok_or_else(|| anyhow!("Failed to parse package ID after publishing"))?; + + // Persist package ID in order to avoid publishing the package for every test. + let package_id_str = package_id.to_string(); + std::env::set_var("IDENTITY_IOTA_PKG_ID", package_id_str.as_str()); + let mut file = std::fs::File::create(CACHED_PKG_ID)?; + write!(&mut file, "{};{}", package_id_str, active_address)?; + + Ok(package_id) +} diff --git a/bindings/grpc/tests/api/main.rs b/bindings/grpc/tests/api/main.rs index af4929bfae..d070ba4c04 100644 --- a/bindings/grpc/tests/api/main.rs +++ b/bindings/grpc/tests/api/main.rs @@ -1,13 +1,22 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#[cfg(test)] mod credential_revocation_check; +#[cfg(test)] mod credential_validation; +#[cfg(test)] mod did_document_creation; +#[cfg(test)] mod domain_linkage; +#[cfg(test)] mod health_check; +#[cfg(test)] mod helpers; +#[cfg(test)] mod jwt; +#[cfg(test)] mod sd_jwt_validation; +#[cfg(test)] mod status_list_2021; mod utils; diff --git a/bindings/grpc/tests/api/status_list_2021.rs b/bindings/grpc/tests/api/status_list_2021.rs index 67ad31b34d..726cd2fd9c 100644 --- a/bindings/grpc/tests/api/status_list_2021.rs +++ b/bindings/grpc/tests/api/status_list_2021.rs @@ -82,9 +82,7 @@ async fn status_list_2021_credential_update() -> anyhow::Result<()> { status_list_credential.update(|status_list| { for idx in entries_to_set { - if let Err(e) = status_list.set_entry(idx as usize, true) { - return Err(e); - } + status_list.set_entry(idx as usize, true)? } Ok(()) })?; diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 8406b386b2..005f79c78e 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -17,9 +17,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] async-trait = { version = "0.1", default-features = false } -bls12_381_plus = "0.8.17" console_error_panic_hook = { version = "0.1" } -futures = { version = "0.3" } identity_ecdsa_verifier = { path = "../../identity_ecdsa_verifier", default-features = false, features = ["es256", "es256k"] } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } js-sys = { version = "0.3.61" } @@ -32,19 +30,22 @@ serde_repr = { version = "0.1", default-features = false } tokio = { version = "1.29", default-features = false, features = ["sync"] } wasm-bindgen = { version = "0.2.85", features = ["serde-serialize"] } wasm-bindgen-futures = { version = "0.4", default-features = false } -zkryptium = "0.2.2" [dependencies.identity_iota] path = "../../identity_iota" default-features = false -features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021", "jpt-bbs-plus"] - -[dev-dependencies] -rand = "0.8.5" +features = [ + "client", + "revocation-bitmap", + "resolver", + "domain-linkage", + "sd-jwt", + "status-list-2021", + "jpt-bbs-plus", +] [target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies] getrandom = { version = "0.2", default-features = false, features = ["js"] } -instant = { version = "0.1", default-features = false, features = ["wasm-bindgen"] } [profile.release] opt-level = 's' diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 1673750e23..5701fa41dc 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -12,7 +12,7 @@ "scripts": { "build:src": "cargo build --lib --release --target wasm32-unknown-unknown", "bundle:nodejs": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ./build/node && tsc --project ./lib/tsconfig.json && node ./build/replace_paths ./lib/tsconfig.json node", - "bundle:web": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target web --out-dir web && node ./build/web && tsc --project ./lib/tsconfig.web.json && node ./build/replace_paths ./lib/tsconfig.web.json web", + "bundle:web": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --target web --out-dir web && node ./build/web && tsc --project ./lib/tsconfig.web.json && node ./build/replace_paths ./lib/tsconfig.web.json web", "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", "build:web": "npm run build:src && npm run bundle:web && wasm-opt -O web/identity_wasm_bg.wasm -o web/identity_wasm_bg.wasm", "build:docs": "typedoc && npm run fix_docs", diff --git a/bindings/wasm/rust-toolchain.toml b/bindings/wasm/rust-toolchain.toml index 825d39b571..55ff02013f 100644 --- a/bindings/wasm/rust-toolchain.toml +++ b/bindings/wasm/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "stable" +channel = "1.81.0" components = ["rustfmt"] targets = ["wasm32-unknown-unknown"] profile = "minimal" diff --git a/examples/0_basic/0_create_did.rs b/examples/0_basic/0_create_did.rs index 61f157cb37..0503a7bad3 100644 --- a/examples/0_basic/0_create_did.rs +++ b/examples/0_basic/0_create_did.rs @@ -1,26 +1,12 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::get_address_with_funds; -use examples::random_stronghold_path; -use examples::MemStorage; -use identity_iota::iota::IotaClientExt; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use identity_iota::verification::jws::JwsAlgorithm; -use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; -/// Demonstrates how to create a DID Document and publish it in a new Alias Output. +use examples::get_memstorage; + +/// Demonstrates how to create a DID Document and publish it on chain. /// /// In this example we connect to a locally running private network, but it can be adapted /// to run on any IOTA node by setting the network and faucet endpoints. @@ -29,54 +15,17 @@ use iota_sdk::types::block::output::AliasOutput; /// https://github.com/iotaledger/hornet/tree/develop/private_tangle #[tokio::main] async fn main() -> anyhow::Result<()> { - // The API endpoint of an IOTA node, e.g. Hornet. - let api_endpoint: &str = "http://localhost"; - - // The faucet endpoint allows requesting funds for testing purposes. - let faucet_endpoint: &str = "http://localhost/faucet/api/enqueue"; - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(api_endpoint, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Get an address with funds for testing. - let address: Address = get_address_with_funds(&client, &secret_manager, faucet_endpoint).await?; + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_client_and_create_account(&storage).await?; - // Get the Bech32 human-readable part (HRP) of the network. - let network_name: NetworkName = client.network_name().await?; - - // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - let mut document: IotaDocument = IotaDocument::new(&network_name); - - // Insert a new Ed25519 verification method in the DID document. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - document - .generate_method( - &storage, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) - .await?; - - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; - - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client.publish_did_output(&secret_manager, alias_output).await?; + // create new DID document and publish it + let (document, _) = create_kinesis_did_document(&identity_client, &storage).await?; println!("Published DID document: {document:#}"); + // check if we can resolve it via client + let resolved = identity_client.resolve_did(document.id()).await?; + println!("Resolved DID document: {resolved:#}"); + Ok(()) } diff --git a/examples/0_basic/1_update_did.rs b/examples/0_basic/1_update_did.rs index 2b89b74348..42b418939a 100644 --- a/examples/0_basic/1_update_did.rs +++ b/examples/0_basic/1_update_did.rs @@ -1,62 +1,39 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Timestamp; use identity_iota::did::DIDUrl; use identity_iota::did::DID; use identity_iota::document::Service; -use identity_iota::iota::block::address::Address; -use identity_iota::iota::block::output::RentStructure; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodRelationship; use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; /// Demonstrates how to update a DID document in an existing Alias Output. #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID in an Alias Output for us to modify. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, document, fragment_1): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_client_and_create_account(&storage).await?; + // create new DID document and publish it + let (document, vm_fragment_1) = create_kinesis_did_document(&identity_client, &storage).await?; let did: IotaDID = document.id().clone(); // Resolve the latest state of the document. - let mut document: IotaDocument = client.resolve_did(&did).await?; + let mut document: IotaDocument = identity_client.resolve_did(&did).await?; // Insert a new Ed25519 verification method in the DID document. - let fragment_2: String = document + let vm_fragment_2: String = document .generate_method( &storage, JwkMemStore::ED25519_KEY_TYPE, @@ -68,7 +45,7 @@ async fn main() -> anyhow::Result<()> { // Attach a new method relationship to the inserted method. document.attach_method_relationship( - &document.id().to_url().join(format!("#{fragment_2}"))?, + &document.id().to_url().join(format!("#{vm_fragment_2}"))?, MethodRelationship::Authentication, )?; @@ -82,22 +59,16 @@ async fn main() -> anyhow::Result<()> { document.metadata.updated = Some(Timestamp::now_utc()); // Remove a verification method. - let original_method: DIDUrl = document.resolve_method(fragment_1.as_str(), None).unwrap().id().clone(); + let original_method: DIDUrl = document.resolve_method(&vm_fragment_1, None).unwrap().id().clone(); document.purge_method(&storage, &original_method).await.unwrap(); - // Resolve the latest output and update it with the given document. - let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; + let updated = identity_client + .publish_did_document_update(document.clone(), TEST_GAS_BUDGET) + .await?; + println!("Updated DID document result: {updated:#}"); - // Publish the updated Alias Output. - let updated: IotaDocument = client.publish_did_output(&secret_manager, alias_output).await?; - println!("Updated DID document: {updated:#}"); + let resolved: IotaDocument = identity_client.resolve_did(&did).await?; + println!("Updated DID document resolved from chain: {resolved:#}"); Ok(()) } diff --git a/examples/0_basic/2_resolve_did.rs b/examples/0_basic/2_resolve_did.rs index 4e648f8370..52b26f81e8 100644 --- a/examples/0_basic/2_resolve_did.rs +++ b/examples/0_basic/2_resolve_did.rs @@ -1,48 +1,27 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::block::address::Address; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::prelude::Resolver; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::output::AliasOutput; /// Demonstrates how to resolve an existing DID in an Alias Output. #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID in an Alias Output for us to resolve. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, document, _): (Address, IotaDocument, String) = create_did(&client, &mut secret_manager, &storage).await?; + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_client_and_create_account(&storage).await?; + // create new DID document and publish it + let (document, _) = create_kinesis_did_document(&identity_client, &storage).await?; let did = document.id().clone(); - // We can resolve a `IotaDID` with the client itself. + // We can resolve a `IotaDID` to bytes via client. // Resolve the associated Alias Output and extract the DID document from it. - let client_document: IotaDocument = client.resolve_did(&did).await?; + let client_document: IotaDocument = identity_client.resolve_did(&did).await?; println!("Client resolved DID Document: {client_document:#}"); // We can also create a `Resolver` that has additional convenience methods, @@ -51,17 +30,12 @@ async fn main() -> anyhow::Result<()> { // We need to register a handler that can resolve IOTA DIDs. // This convenience method only requires us to provide a client. - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler((*identity_client).clone()); let resolver_document: IotaDocument = resolver.resolve(&did).await.unwrap(); - // Client and Resolver resolve to the same document in this case. + // Client and Resolver resolve to the same document. assert_eq!(client_document, resolver_document); - // We can also resolve the Alias Output directly. - let alias_output: AliasOutput = client.resolve_did_output(&did).await?; - - println!("The Alias Output holds {} tokens", alias_output.amount()); - Ok(()) } diff --git a/examples/0_basic/3_deactivate_did.rs b/examples/0_basic/3_deactivate_did.rs index 1a4a5d998f..1a850bbab7 100644 --- a/examples/0_basic/3_deactivate_did.rs +++ b/examples/0_basic/3_deactivate_did.rs @@ -1,82 +1,46 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::block::address::Address; -use identity_iota::iota::IotaClientExt; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; /// Demonstrates how to deactivate a DID in an Alias Output. #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_client_and_create_account(&storage).await?; - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); + // create new DID document and publish it + let (document, _) = create_kinesis_did_document(&identity_client, &storage).await?; - // Create a new DID in an Alias Output for us to modify. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, document, _): (Address, IotaDocument, String) = create_did(&client, &mut secret_manager, &storage).await?; - let did: IotaDID = document.id().clone(); + println!("Published DID document: {document:#}"); - // Resolve the latest state of the DID document. - let document: IotaDocument = client.resolve_did(&did).await?; + let did: IotaDID = document.id().clone(); // Deactivate the DID by publishing an empty document. // This process can be reversed since the Alias Output is not destroyed. // Deactivation may only be performed by the state controller of the Alias Output. - let deactivated_output: AliasOutput = client.deactivate_did_output(&did).await?; - - // Optional: reduce and reclaim the storage deposit, sending the tokens to the state controller. - let rent_structure = client.get_rent_structure().await?; - let deactivated_output = AliasOutputBuilder::from(&deactivated_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the deactivated DID document. - let _ = client.publish_did_output(&secret_manager, deactivated_output).await?; + identity_client.deactivate_did_output(&did, TEST_GAS_BUDGET).await?; // Resolving a deactivated DID returns an empty DID document // with its `deactivated` metadata field set to `true`. - let deactivated: IotaDocument = client.resolve_did(&did).await?; + let deactivated: IotaDocument = identity_client.resolve_did(&did).await?; println!("Deactivated DID document: {deactivated:#}"); assert_eq!(deactivated.metadata.deactivated, Some(true)); // Re-activate the DID by publishing a valid DID document. - let reactivated_output: AliasOutput = client.update_did_output(document.clone()).await?; - - // Increase the storage deposit to the minimum again, if it was reclaimed during deactivation. - let rent_structure = client.get_rent_structure().await?; - let reactivated_output = AliasOutputBuilder::from(&reactivated_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - client.publish_did_output(&secret_manager, reactivated_output).await?; + let reactivated: IotaDocument = identity_client + .publish_did_document_update(document.clone(), TEST_GAS_BUDGET) + .await?; + println!("Reactivated DID document result: {reactivated:#}"); - // Resolve the reactivated DID document. - let reactivated: IotaDocument = client.resolve_did(&did).await?; - assert_eq!(document, reactivated); - assert!(!reactivated.metadata.deactivated.unwrap_or_default()); + let resolved: IotaDocument = identity_client.resolve_did(&did).await?; + println!("Reactivated DID document resolved from chain: {resolved:#}"); Ok(()) } diff --git a/examples/0_basic/4_delete_did.rs b/examples/0_basic/4_delete_did.rs deleted file mode 100644 index 738ba534aa..0000000000 --- a/examples/0_basic/4_delete_did.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::Error; -use identity_iota::iota::IotaClientExt; - -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; - -/// Demonstrates how to delete a DID in an Alias Output, reclaiming the storage deposit. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID in an Alias Output for us to modify. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (address, document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let did = document.id().clone(); - - // Deletes the Alias Output and its contained DID Document, rendering the DID permanently destroyed. - // This operation is *not* reversible. - // Deletion can only be done by the governor of the Alias Output. - client.delete_did_output(&secret_manager, address, &did).await?; - - // Attempting to resolve a deleted DID results in a `NoOutput` error. - let error: Error = client.resolve_did(&did).await.unwrap_err(); - - assert!(matches!( - error, - identity_iota::iota::Error::DIDResolutionError(iota_sdk::client::Error::Node( - iota_sdk::client::node_api::error::Error::NotFound(..) - )) - )); - - Ok(()) -} diff --git a/examples/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index 3a14e262e2..0affc163e2 100644 --- a/examples/0_basic/5_create_vc.rs +++ b/examples/0_basic/5_create_vc.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! This example shows how to create a Verifiable Credential and validate it. @@ -9,8 +9,9 @@ //! //! cargo run --release --example 5_create_vc -use examples::create_did; -use examples::MemStorage; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Object; @@ -19,17 +20,8 @@ use identity_iota::credential::Jwt; use identity_iota::credential::JwtCredentialValidationOptions; use identity_iota::credential::JwtCredentialValidator; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use examples::random_stronghold_path; -use examples::API_ENDPOINT; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Url; @@ -38,39 +30,23 @@ use identity_iota::credential::CredentialBuilder; use identity_iota::credential::FailFast; use identity_iota::credential::Subject; use identity_iota::did::DID; -use identity_iota::iota::IotaDocument; #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create an identity for the issuer with one verification method `key-1`. - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - let issuer_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, issuer_document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &issuer_storage).await?; + // create new issuer account with did document + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_client_and_create_account(&issuer_storage).await?; + let (issuer_document, issuer_vm_fragment) = + create_kinesis_did_document(&issuer_identity_client, &issuer_storage).await?; - // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let alice_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &alice_storage).await?; + // create new holder account with did document + let holder_storage = get_memstorage()?; + let holder_identity_client = get_client_and_create_account(&holder_storage).await?; + let (holder_document, _) = create_kinesis_did_document(&holder_identity_client, &holder_storage).await?; // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", @@ -91,7 +67,7 @@ async fn main() -> anyhow::Result<()> { .create_credential_jwt( &credential, &issuer_storage, - &fragment, + &issuer_vm_fragment, &JwsSignatureOptions::default(), None, ) diff --git a/examples/0_basic/6_create_vp.rs b/examples/0_basic/6_create_vp.rs index 8c157295ef..b213f993a7 100644 --- a/examples/0_basic/6_create_vp.rs +++ b/examples/0_basic/6_create_vp.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! This example shows how to create a Verifiable Presentation and validate it. @@ -9,8 +9,9 @@ use std::collections::HashMap; -use examples::create_did; -use examples::MemStorage; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Object; use identity_iota::credential::DecodedJwtCredential; @@ -26,17 +27,8 @@ use identity_iota::credential::PresentationBuilder; use identity_iota::did::CoreDID; use identity_iota::document::verifiable::JwsVerificationOptions; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; - -use examples::random_stronghold_path; -use examples::API_ENDPOINT; + use identity_iota::core::json; use identity_iota::core::Duration; use identity_iota::core::FromJson; @@ -59,31 +51,22 @@ async fn main() -> anyhow::Result<()> { // Step 1: Create identities for the issuer and the holder. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; + // create new issuer account with did document + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_client_and_create_account(&issuer_storage).await?; + let (issuer_document, issuer_vm_fragment) = + create_kinesis_did_document(&issuer_identity_client, &issuer_storage).await?; + + // create new holder account with did document + let holder_storage = get_memstorage()?; + let holder_identity_client = get_client_and_create_account(&holder_storage).await?; + let (holder_document, holder_vm_fragment) = + create_kinesis_did_document(&holder_identity_client, &holder_storage).await?; - // Create an identity for the issuer with one verification method `key-1`. - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &storage_issuer).await?; - - // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_alice: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, fragment_alice): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &storage_alice).await?; + // create new client for verifier + // new client actually not necessary, but shows, that client is independent from issuer and holder + let verifier_storage = &get_memstorage()?; + let verifier_client = get_client_and_create_account(verifier_storage).await?; // =========================================================================== // Step 2: Issuer creates and signs a Verifiable Credential. @@ -91,7 +74,7 @@ async fn main() -> anyhow::Result<()> { // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", @@ -111,8 +94,8 @@ async fn main() -> anyhow::Result<()> { let credential_jwt: Jwt = issuer_document .create_credential_jwt( &credential, - &storage_issuer, - &fragment_issuer, + &issuer_storage, + &issuer_vm_fragment, &JwsSignatureOptions::default(), None, ) @@ -156,17 +139,17 @@ async fn main() -> anyhow::Result<()> { // Create an unsigned Presentation from the previously issued Verifiable Credential. let presentation: Presentation = - PresentationBuilder::new(alice_document.id().to_url().into(), Default::default()) + PresentationBuilder::new(holder_document.id().to_url().into(), Default::default()) .credential(credential_jwt) .build()?; // Create a JWT verifiable presentation using the holder's verification method // and include the requested challenge and expiry timestamp. - let presentation_jwt: Jwt = alice_document + let presentation_jwt: Jwt = holder_document .create_presentation_jwt( &presentation, - &storage_alice, - &fragment_alice, + &holder_storage, + &holder_vm_fragment, &JwsSignatureOptions::default().nonce(challenge.to_owned()), &JwtPresentationOptions::default().expiration_date(expires), ) @@ -191,7 +174,7 @@ async fn main() -> anyhow::Result<()> { JwsVerificationOptions::default().nonce(challenge.to_owned()); let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); + resolver.attach_kinesis_iota_handler((*verifier_client).clone()); // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; @@ -231,7 +214,7 @@ async fn main() -> anyhow::Result<()> { // Since no errors were thrown by `verify_presentation` we know that the validation was successful. println!("VP successfully validated: {:#?}", presentation.presentation); - // Note that we did not declare a latest allowed issuance date for credentials. This is because we only want to check - // that the credentials do not have an issuance date in the future which is a default check. + // Note that we did not declare a latest allowed issuance date for credentials. This is because we only want to + // check // that the credentials do not have an issuance date in the future which is a default check. Ok(()) } diff --git a/examples/0_basic/7_revoke_vc.rs b/examples/0_basic/7_revoke_vc.rs index 864041f3e3..c4d7225e3e 100644 --- a/examples/0_basic/7_revoke_vc.rs +++ b/examples/0_basic/7_revoke_vc.rs @@ -11,10 +11,10 @@ //! cargo run --release --example 7_revoke_vc use anyhow::anyhow; -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::json; use identity_iota::core::FromJson; @@ -37,23 +37,11 @@ use identity_iota::credential::Subject; use identity_iota::did::DIDUrl; use identity_iota::did::DID; use identity_iota::document::Service; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::prelude::IotaDID; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -61,32 +49,16 @@ async fn main() -> anyhow::Result<()> { // Create a Verifiable Credential. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); + // create new issuer account with did document + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_client_and_create_account(&issuer_storage).await?; + let (mut issuer_document, issuer_vm_fragment) = + create_kinesis_did_document(&issuer_identity_client, &issuer_storage).await?; - // Create an identity for the issuer with one verification method `key-1`. - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, mut issuer_document, fragment_issuer): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &storage_issuer).await?; - - // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_alice: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &storage_alice).await?; + // create new holder account with did document + let holder_storage = get_memstorage()?; + let holder_identity_client = get_client_and_create_account(&holder_storage).await?; + let (holder_document, _) = create_kinesis_did_document(&holder_identity_client, &holder_storage).await?; // Create a new empty revocation bitmap. No credential is revoked yet. let revocation_bitmap: RevocationBitmap = RevocationBitmap::new(); @@ -98,23 +70,13 @@ async fn main() -> anyhow::Result<()> { assert!(issuer_document.insert_service(service).is_ok()); // Resolve the latest output and update it with the given document. - let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the updated Alias Output. - issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; - - println!("DID Document > {issuer_document:#}"); + issuer_document = issuer_identity_client + .publish_did_document_update(issuer_document.clone(), TEST_GAS_BUDGET) + .await?; // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", @@ -129,7 +91,7 @@ async fn main() -> anyhow::Result<()> { let credential_index: u32 = 5; let status: Status = RevocationBitmapStatus::new(service_url, credential_index).into(); - // Build credential using subject above, status, and issuer. + // Build credential using subject above and issuer. let credential: Credential = CredentialBuilder::default() .id(Url::parse("https://example.edu/credentials/3732")?) .issuer(Url::parse(issuer_document.id().as_str())?) @@ -143,8 +105,8 @@ async fn main() -> anyhow::Result<()> { let credential_jwt: Jwt = issuer_document .create_credential_jwt( &credential, - &storage_issuer, - &fragment_issuer, + &issuer_storage, + &issuer_vm_fragment, &JwsSignatureOptions::default(), None, ) @@ -169,12 +131,9 @@ async fn main() -> anyhow::Result<()> { issuer_document.revoke_credentials("my-revocation-service", &[credential_index])?; // Publish the changes. - let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + issuer_document = issuer_identity_client + .publish_did_document_update(issuer_document.clone(), TEST_GAS_BUDGET) + .await?; let validation_result: std::result::Result = validator .validate( @@ -197,7 +156,7 @@ async fn main() -> anyhow::Result<()> { // By removing the verification method, that signed the credential, from the issuer's DID document, // we effectively revoke the credential, as it will no longer be possible to validate the signature. let original_method: DIDUrl = issuer_document - .resolve_method(&fragment_issuer, None) + .resolve_method(&issuer_vm_fragment, None) .ok_or_else(|| anyhow!("expected method to exist"))? .id() .clone(); @@ -206,13 +165,13 @@ async fn main() -> anyhow::Result<()> { .ok_or_else(|| anyhow!("expected method to exist"))?; // Publish the changes. - let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output).finish()?; - client.publish_did_output(&secret_manager_issuer, alias_output).await?; + issuer_identity_client + .publish_did_document_update(issuer_document.clone(), TEST_GAS_BUDGET) + .await?; // We expect the verifiable credential to be revoked. let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); + resolver.attach_kinesis_iota_handler((*holder_identity_client).clone()); let resolved_issuer_did: IotaDID = JwtCredentialValidatorUtils::extract_issuer_from_jwt(&credential_jwt)?; let resolved_issuer_doc: IotaDocument = resolver.resolve(&resolved_issuer_did).await?; diff --git a/examples/0_basic/8_stronghold.rs b/examples/0_basic/8_stronghold.rs index 0681e5b612..df2578db9e 100644 --- a/examples/0_basic/8_stronghold.rs +++ b/examples/0_basic/8_stronghold.rs @@ -1,111 +1,48 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::get_address_with_funds; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_stronghold_storage; use examples::random_stronghold_path; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::credential::Jws; use identity_iota::document::verifiable::JwsVerificationOptions; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::Storage; use identity_iota::verification::jws::DecodedJws; -use identity_iota::verification::jws::JwsAlgorithm; -use identity_iota::verification::MethodScope; -use identity_stronghold::StrongholdStorage; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; /// Demonstrates how to use stronghold for secure storage. #[tokio::main] async fn main() -> anyhow::Result<()> { - // The API endpoint of an IOTA node, e.g. Hornet. - let api_endpoint: &str = "http://localhost"; - - // The faucet endpoint allows requesting funds for testing purposes. - let faucet_endpoint: &str = "http://localhost/faucet/api/enqueue"; - - // Stronghold snapshot path. - let path = random_stronghold_path(); - - // Stronghold password. - let password = Password::from("secure_password".to_owned()); - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(api_endpoint, None)? - .finish() - .await?; - - let stronghold = StrongholdSecretManager::builder() - .password(password.clone()) - .build(path.clone())?; - - // Create a `StrongholdStorage`. - // `StrongholdStorage` creates internally a `SecretManager` that can be - // referenced to avoid creating multiple instances around the same stronghold snapshot. - let stronghold_storage = StrongholdStorage::new(stronghold); - - // Create a DID document. - let address: Address = - get_address_with_funds(&client, stronghold_storage.as_secret_manager(), faucet_endpoint).await?; - let network_name: NetworkName = client.network_name().await?; - let mut document: IotaDocument = IotaDocument::new(&network_name); - // Create storage for key-ids and JWKs. // // In this example, the same stronghold file that is used to store // key-ids as well as the JWKs. - let storage = Storage::new(stronghold_storage.clone(), stronghold_storage.clone()); - - // Generates a verification method. This will store the key-id as well as the private key - // in the stronghold file. - let fragment = document - .generate_method( - &storage, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) - .await?; - - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; + let path = random_stronghold_path(); + let storage = get_stronghold_storage(Some(path.clone()))?; - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client - .publish_did_output(stronghold_storage.as_secret_manager(), alias_output) - .await?; + // use stronghold storage to create new client to interact with chain and get funded account with keys + let identity_client = get_client_and_create_account(&storage).await?; + // create and publish document with stronghold storage + let (document, vm_fragment) = create_kinesis_did_document(&identity_client, &storage).await?; // Resolve the published DID Document. let mut resolver = Resolver::::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler((*identity_client).clone()); let resolved_document: IotaDocument = resolver.resolve(document.id()).await.unwrap(); - drop(stronghold_storage); + drop(storage); - // Create the storage again to demonstrate that data are read from the stronghold file. - let stronghold = StrongholdSecretManager::builder() - .password(password.clone()) - .build(path.clone())?; - let stronghold_storage = StrongholdStorage::new(stronghold); - let storage = Storage::new(stronghold_storage.clone(), stronghold_storage.clone()); + // Create the storage again to demonstrate that data are read from the existing stronghold file. + let storage = get_stronghold_storage(Some(path))?; // Sign data with the created verification method. let data = b"test_data"; let jws: Jws = resolved_document - .create_jws(&storage, &fragment, data, &JwsSignatureOptions::default()) + .create_jws(&storage, &vm_fragment, data, &JwsSignatureOptions::default()) .await?; // Verify Signature. @@ -118,5 +55,7 @@ async fn main() -> anyhow::Result<()> { assert_eq!(String::from_utf8_lossy(decoded_jws.claims.as_ref()), "test_data"); + println!("successfully verified signature"); + Ok(()) } diff --git a/examples/1_advanced/0_did_controls_did.rs b/examples/1_advanced/0_did_controls_did.rs deleted file mode 100644 index 86cf4eb8b8..0000000000 --- a/examples/1_advanced/0_did_controls_did.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::ops::Deref; - -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::block::output::AliasId; -use identity_iota::iota::block::output::UnlockCondition; -use identity_iota::iota::IotaClientExt; -use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use identity_iota::verification::jws::JwsAlgorithm; -use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::AliasAddress; -use iota_sdk::types::block::output::feature::IssuerFeature; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; - -/// Demonstrates how an identity can control another identity. -/// -/// For this example, we consider the case where a parent company's DID controls the DID of a subsidiary. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // ======================================================== - // Create the company's and subsidiary's Alias Output DIDs. - // ======================================================== - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID for the company. - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, company_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage_issuer).await?; - let company_did = company_document.id().clone(); - - // Get the current byte costs and network name. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let network_name: NetworkName = client.network_name().await?; - - // Construct a new DID document for the subsidiary. - let subsidiary_document: IotaDocument = IotaDocument::new(&network_name); - - // Create a DID for the subsidiary that is controlled by the parent company's DID. - // This means the subsidiary's Alias Output can only be updated or destroyed by - // the state controller or governor of the company's Alias Output respectively. - let subsidiary_alias: AliasOutput = client - .new_did_output( - Address::Alias(AliasAddress::new(AliasId::from(&company_did))), - subsidiary_document, - Some(rent_structure), - ) - .await?; - - let subsidiary_alias: AliasOutput = AliasOutputBuilder::from(&subsidiary_alias) - // Optionally, we can mark the company as the issuer of the subsidiary DID. - // This allows to verify trust relationships between DIDs, as a resolver can - // verify that the subsidiary DID was created by the parent company. - .add_immutable_feature(IssuerFeature::new(AliasAddress::new(AliasId::from(&company_did)))) - // Adding the issuer feature means we have to recalculate the required storage deposit. - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the subsidiary's DID. - let mut subsidiary_document: IotaDocument = client.publish_did_output(&secret_manager, subsidiary_alias).await?; - - // ===================================== - // Update the subsidiary's Alias Output. - // ===================================== - - // Add a verification method to the subsidiary. - // This only serves as an example for updating the subsidiary DID. - - let storage_subsidary: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - subsidiary_document - .generate_method( - &storage_subsidary, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) - .await?; - - // Update the subsidiary's Alias Output with the updated document - // and increase the storage deposit. - let subsidiary_alias: AliasOutput = client.update_did_output(subsidiary_document).await?; - let subsidiary_alias: AliasOutput = AliasOutputBuilder::from(&subsidiary_alias) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the updated subsidiary's DID. - // - // This works because `secret_manager` can unlock the company's Alias Output, - // which is required in order to update the subsidiary's Alias Output. - let subsidiary_document: IotaDocument = client.publish_did_output(&secret_manager, subsidiary_alias).await?; - - // =================================================================== - // Determine the controlling company's DID given the subsidiary's DID. - // =================================================================== - - // Resolve the subsidiary's Alias Output. - let subsidiary_output: AliasOutput = client.resolve_did_output(subsidiary_document.id()).await?; - - // Extract the company's Alias Id from the state controller unlock condition. - // - // If instead we wanted to determine the original creator of the DID, - // we could inspect the issuer feature. This feature needs to be set when creating the DID. - let company_alias_id: AliasId = if let Some(UnlockCondition::StateControllerAddress(address)) = - subsidiary_output.unlock_conditions().iter().next() - { - if let Address::Alias(alias) = *address.address() { - *alias.alias_id() - } else { - anyhow::bail!("expected an alias address as the state controller"); - } - } else { - anyhow::bail!("expected two unlock conditions"); - }; - - // Reconstruct the company's DID from the Alias Id and the network. - let company_did = IotaDID::new(company_alias_id.deref(), &network_name); - - // Resolve the company's DID document. - let company_document: IotaDocument = client.resolve_did(&company_did).await?; - - println!("Company: {company_document:#}"); - println!("Subsidiary: {subsidiary_document:#}"); - - Ok(()) -} diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index a78dea0e76..7787c147ff 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -1,11 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 -use examples::get_address_with_funds; -use examples::random_stronghold_path; +use examples::get_client_and_create_account; use examples::MemStorage; -use examples::API_ENDPOINT; -use examples::FAUCET_ENDPOINT; +use examples::TEST_GAS_BUDGET; + use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::json; use identity_iota::core::Duration; @@ -46,9 +45,11 @@ use identity_iota::did::DIDUrl; use identity_iota::did::DID; use identity_iota::document::verifiable::JwsVerificationOptions; use identity_iota::document::Service; -use identity_iota::iota::IotaClientExt; +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IotaKeySignature; +use identity_iota::iota::rebased::transaction::Transaction; +use identity_iota::iota::rebased::transaction::TransactionOutput; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::iota::NetworkName; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; @@ -60,43 +61,65 @@ use identity_iota::storage::KeyType; use identity_iota::storage::TimeframeRevocationExtension; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; +use identity_storage::Storage; use jsonprooftoken::jpa::algs::ProofAlgorithm; +use secret_storage::Signer; use std::thread; use std::time::Duration as SleepDuration; -async fn create_did( - client: &Client, - secret_manager: &SecretManager, - storage: &MemStorage, +// // Creates a DID with a JWP verification method. +// pub async fn create_did( +// identity_client: &IdentityClient, +// storage: &Storage, +// key_type: KeyType, +// alg: ProofAlgorithm, +// ) -> anyhow::Result<(IotaDocument, String)> +// where +// K: identity_storage::JwkStorage + identity_storage::JwkStorageBbsPlusExt, +// I: identity_storage::KeyIdStorage, +// S: Signer + Sync, +// { +// // Create a new DID document with a placeholder DID. +// // The DID will be derived from the Alias Id of the Alias Output after publishing. +// let mut unpublished: IotaDocument = IotaDocument::new(identity_client.network()); + +// let verification_method_fragment = unpublished +// .generate_method_jwp(storage, key_type, alg, None, MethodScope::VerificationMethod) +// .await?; + +// let document = identity_client +// .publish_did_document(unpublished) +// .execute_with_gas(TEST_GAS_BUDGET, identity_client) +// .await?; + +// Ok((document, verification_method_fragment)) +// } +async fn create_did( + identity_client: &IdentityClient, + storage: &Storage, key_type: KeyType, alg: Option, proof_alg: Option, -) -> anyhow::Result<(Address, IotaDocument, String)> { - // Get an address with funds for testing. - let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?; - - // Get the Bech32 human-readable part (HRP) of the network. - let network_name: NetworkName = client.network_name().await?; +) -> anyhow::Result<(IotaDocument, String)> +where + K: identity_storage::JwkStorage + identity_storage::JwkStorageBbsPlusExt, + I: identity_storage::KeyIdStorage, + S: Signer + Sync, +{ + // Get the network name. + let network_name: &NetworkName = identity_client.network(); // Create a new DID document with a placeholder DID. // The DID will be derived from the Alias Id of the Alias Output after publishing. - let mut document: IotaDocument = IotaDocument::new(&network_name); + let mut unpublished: IotaDocument = IotaDocument::new(network_name); // New Verification Method containing a BBS+ key let fragment = if let Some(alg) = alg { - document + unpublished .generate_method(storage, key_type, alg, None, MethodScope::VerificationMethod) .await? } else if let Some(proof_alg) = proof_alg { - let fragment = document + let fragment = unpublished .generate_method_jwp(storage, key_type, proof_alg, None, MethodScope::VerificationMethod) .await?; @@ -104,55 +127,42 @@ async fn create_did( let revocation_bitmap: RevocationBitmap = RevocationBitmap::new(); // Add the revocation bitmap to the DID document of the issuer as a service. - let service_id: DIDUrl = document.id().to_url().join("#my-revocation-service")?; + let service_id: DIDUrl = unpublished.id().to_url().join("#my-revocation-service")?; let service: Service = revocation_bitmap.to_service(service_id)?; - assert!(document.insert_service(service).is_ok()); + assert!(unpublished.insert_service(service).is_ok()); fragment } else { return Err(anyhow::Error::msg("You have to pass at least one algorithm")); }; - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; + let TransactionOutput:: { output: document, .. } = identity_client + .publish_did_document(unpublished) + .execute_with_gas(TEST_GAS_BUDGET, identity_client) + .await?; - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; println!("Published DID document: {document:#}"); - Ok((address, document, fragment)) + Ok((document, fragment)) } /// Demonstrates how to create an Anonymous Credential with BBS+. #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - let secret_manager_issuer = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - + // =========================================================================== + // Step 1: Create identities and for the issuer and the holder. + // =========================================================================== let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let secret_manager_holder = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_holder: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, mut issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( - &client, - &secret_manager_issuer, + let issuer_identity_client = get_client_and_create_account(&storage_issuer).await?; + + let holder_identity_client = get_client_and_create_account(&storage_holder).await?; + + let (mut issuer_document, fragment_issuer): (IotaDocument, String) = create_did( + &issuer_identity_client, &storage_issuer, JwkMemStore::BLS12381G2_KEY_TYPE, None, @@ -160,9 +170,8 @@ async fn main() -> anyhow::Result<()> { ) .await?; - let (_, holder_document, fragment_holder): (Address, IotaDocument, String) = create_did( - &client, - &secret_manager_holder, + let (holder_document, fragment_holder): (IotaDocument, String) = create_did( + &holder_identity_client, &storage_holder, JwkMemStore::ED25519_KEY_TYPE, Some(JwsAlgorithm::EdDSA), @@ -414,7 +423,7 @@ async fn main() -> anyhow::Result<()> { JwsVerificationOptions::default().nonce(challenge.to_owned()); let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler((*holder_identity_client).clone()); // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; @@ -515,12 +524,9 @@ async fn main() -> anyhow::Result<()> { issuer_document.revoke_credentials("my-revocation-service", &[credential_index])?; // Publish the changes. - let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + issuer_identity_client + .publish_did_document_update(issuer_document.clone(), TEST_GAS_BUDGET) + .await?; // Holder checks if his credential has been revoked by the Issuer let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( @@ -529,6 +535,7 @@ async fn main() -> anyhow::Result<()> { StatusCheck::Strict, ); assert!(revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked))); + println!("Credential Revoked!"); Ok(()) } diff --git a/examples/1_advanced/11_linked_verifiable_presentation.rs b/examples/1_advanced/11_linked_verifiable_presentation.rs index 550bad3d41..75c22e2104 100644 --- a/examples/1_advanced/11_linked_verifiable_presentation.rs +++ b/examples/1_advanced/11_linked_verifiable_presentation.rs @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use anyhow::Context; -use examples::create_did; -use examples::random_stronghold_path; + +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::TEST_GAS_BUDGET; + use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::FromJson; use identity_iota::core::Object; @@ -26,45 +29,27 @@ use identity_iota::did::CoreDID; use identity_iota::did::DIDUrl; use identity_iota::did::DID; use identity_iota::document::verifiable::JwsVerificationOptions; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - let stronghold_path = random_stronghold_path(); - - println!("Using stronghold path: {stronghold_path:?}"); - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(stronghold_path)?, - ); - - // Create a DID for the entity that will be the holder of the Verifiable Presentation. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, mut did_document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; + // =========================================================================== + // Step 1: Create identities and Client + // =========================================================================== + + let storage = get_memstorage()?; + + let identity_client = get_client_and_create_account(&storage).await?; + + // create new DID document and publish it + let (mut did_document, fragment) = create_kinesis_did_document(&identity_client, &storage).await?; + + println!("Published DID document: {did_document:#}"); + let did: IotaDID = did_document.id().clone(); // ===================================================== @@ -85,7 +70,10 @@ async fn main() -> anyhow::Result<()> { let linked_verifiable_presentation_service = LinkedVerifiablePresentationService::new(service_url, verifiable_presentation_urls, Object::new())?; did_document.insert_service(linked_verifiable_presentation_service.into())?; - let updated_did_document: IotaDocument = publish_document(client.clone(), secret_manager, did_document).await?; + + let updated_did_document: IotaDocument = identity_client + .publish_did_document_update(did_document, TEST_GAS_BUDGET) + .await?; println!("DID document with linked verifiable presentation service: {updated_did_document:#}"); @@ -95,7 +83,7 @@ async fn main() -> anyhow::Result<()> { // Init a resolver for resolving DID Documents. let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler((*identity_client).clone()); // Resolve the DID Document of the DID that issued the credential. let did_document: IotaDocument = resolver.resolve(&did).await?; @@ -107,6 +95,7 @@ async fn main() -> anyhow::Result<()> { .cloned() .filter_map(|service| LinkedVerifiablePresentationService::try_from(service).ok()) .collect(); + assert_eq!(linked_verifiable_presentation_services.len(), 1); // Get the VPs included in the service. @@ -139,25 +128,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -async fn publish_document( - client: Client, - secret_manager: SecretManager, - document: IotaDocument, -) -> anyhow::Result { - // Resolve the latest output and update it with the given document. - let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the updated Alias Output. - Ok(client.publish_did_output(&secret_manager, alias_output).await?) -} - async fn make_vp_jwt(did_doc: &IotaDocument, storage: &MemStorage, fragment: &str) -> anyhow::Result { // first we create a credential encoding it as jwt let credential = CredentialBuilder::new(Object::default()) diff --git a/examples/1_advanced/1_did_issues_nft.rs b/examples/1_advanced/1_did_issues_nft.rs deleted file mode 100644 index 6509032b74..0000000000 --- a/examples/1_advanced/1_did_issues_nft.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::block::output::feature::MetadataFeature; -use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::AliasAddress; -use iota_sdk::types::block::output::feature::IssuerFeature; -use iota_sdk::types::block::output::unlock_condition::AddressUnlockCondition; -use iota_sdk::types::block::output::AliasId; -use iota_sdk::types::block::output::Feature; -use iota_sdk::types::block::output::NftId; -use iota_sdk::types::block::output::NftOutput; -use iota_sdk::types::block::output::NftOutputBuilder; -use iota_sdk::types::block::output::Output; -use iota_sdk::types::block::output::OutputId; -use iota_sdk::types::block::output::RentStructure; -use iota_sdk::types::block::output::UnlockCondition; -use iota_sdk::types::block::payload::transaction::TransactionEssence; -use iota_sdk::types::block::payload::Payload; -use iota_sdk::types::block::Block; - -/// Demonstrates how an identity can issue and own NFTs, -/// and how observers can verify the issuer of the NFT. -/// -/// For this example, we consider the case where a manufacturer issues -/// a digital product passport (DPP) as an NFT. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // ============================================== - // Create the manufacturer's DID and the DPP NFT. - // ============================================== - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID for the manufacturer. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, manufacturer_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let manufacturer_did = manufacturer_document.id().clone(); - - // Get the current byte cost. - let rent_structure: RentStructure = client.get_rent_structure().await?; - - // Create a Digital Product Passport NFT issued by the manufacturer. - let product_passport_nft: NftOutput = - NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, NftId::null()) - // The NFT will initially be owned by the manufacturer. - .add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(Address::Alias( - AliasAddress::new(AliasId::from(&manufacturer_did)), - )))) - // Set the manufacturer as the immutable issuer. - .add_immutable_feature(Feature::Issuer(IssuerFeature::new(Address::Alias(AliasAddress::new( - AliasId::from(&manufacturer_did), - ))))) - // A proper DPP would hold its metadata here. - .add_immutable_feature(Feature::Metadata(MetadataFeature::new( - b"Digital Product Passport Metadata".to_vec(), - )?)) - .finish()?; - - // Publish the NFT. - let block: Block = client - .build_block() - .with_secret_manager(&secret_manager) - .with_outputs(vec![product_passport_nft.into()])? - .finish() - .await?; - let _ = client.retry_until_included(&block.id(), None, None).await?; - - // ======================================================== - // Resolve the Digital Product Passport NFT and its issuer. - // ======================================================== - - // Extract the identifier of the NFT from the published block. - let nft_id: NftId = NftId::from(&get_nft_output_id( - block - .payload() - .ok_or_else(|| anyhow::anyhow!("expected block to contain a payload"))?, - )?); - - // Fetch the NFT Output. - let nft_output_id: OutputId = client.nft_output_id(nft_id).await?; - let output: Output = client.get_output(&nft_output_id).await?.into_output(); - - // Extract the issuer of the NFT. - let nft_output: NftOutput = if let Output::Nft(nft_output) = output { - nft_output - } else { - anyhow::bail!("expected NFT output") - }; - - let issuer_address: Address = if let Some(Feature::Issuer(issuer)) = nft_output.immutable_features().iter().next() { - *issuer.address() - } else { - anyhow::bail!("expected an issuer feature") - }; - - let manufacturer_alias_id: AliasId = if let Address::Alias(alias_address) = issuer_address { - *alias_address.alias_id() - } else { - anyhow::bail!("expected an Alias Address") - }; - - // Reconstruct the manufacturer's DID from the Alias Id. - let network: NetworkName = client.network_name().await?; - let manufacturer_did: IotaDID = IotaDID::new(&manufacturer_alias_id, &network); - - // Resolve the issuer of the NFT. - let manufacturer_document: IotaDocument = client.resolve_did(&manufacturer_did).await?; - - println!("The issuer of the Digital Product Passport NFT is: {manufacturer_document:#}"); - - Ok(()) -} - -// Helper function to get the output id for the first NFT output in a Block. -fn get_nft_output_id(payload: &Payload) -> anyhow::Result { - match payload { - Payload::Transaction(tx_payload) => { - let TransactionEssence::Regular(regular) = tx_payload.essence(); - for (index, output) in regular.outputs().iter().enumerate() { - if let Output::Nft(_nft_output) = output { - return Ok(OutputId::new(tx_payload.id(), index.try_into().unwrap())?); - } - } - anyhow::bail!("no NFT output in transaction essence") - } - _ => anyhow::bail!("No transaction payload"), - } -} diff --git a/examples/1_advanced/2_nft_owns_did.rs b/examples/1_advanced/2_nft_owns_did.rs deleted file mode 100644 index 435964097c..0000000000 --- a/examples/1_advanced/2_nft_owns_did.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use examples::create_did_document; -use examples::get_address_with_funds; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use examples::FAUCET_ENDPOINT; -use identity_iota::iota::block::address::NftAddress; -use identity_iota::iota::block::output::AliasOutput; -use identity_iota::iota::IotaClientExt; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::unlock_condition::AddressUnlockCondition; -use iota_sdk::types::block::output::NftId; -use iota_sdk::types::block::output::NftOutput; -use iota_sdk::types::block::output::NftOutputBuilder; -use iota_sdk::types::block::output::Output; -use iota_sdk::types::block::output::OutputId; -use iota_sdk::types::block::output::RentStructure; -use iota_sdk::types::block::output::UnlockCondition; -use iota_sdk::types::block::payload::transaction::TransactionEssence; -use iota_sdk::types::block::payload::Payload; -use iota_sdk::types::block::Block; - -/// Demonstrates how an identity can be owned by NFTs, -/// and how observers can verify that relationship. -/// -/// For this example, we consider the case where a car's NFT owns -/// the DID of the car, so that transferring the NFT also transfers DID ownership. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // ============================= - // Create the car's NFT and DID. - // ============================= - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Get an address with funds for testing. - let address: Address = get_address_with_funds(&client, &secret_manager, FAUCET_ENDPOINT).await?; - - // Get the current byte cost. - let rent_structure: RentStructure = client.get_rent_structure().await?; - - // Create the car NFT with an Ed25519 address as the unlock condition. - let car_nft: NftOutput = NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, NftId::null()) - .add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(address))) - .finish()?; - - // Publish the NFT output. - let block: Block = client - .build_block() - .with_secret_manager(&secret_manager) - .with_outputs(vec![car_nft.into()])? - .finish() - .await?; - let _ = client.retry_until_included(&block.id(), None, None).await?; - - let car_nft_id: NftId = NftId::from(&get_nft_output_id( - block - .payload() - .ok_or_else(|| anyhow::anyhow!("expected the block to contain a payload"))?, - )?); - - let network: NetworkName = client.network_name().await?; - - // Construct a DID document for the car. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (car_document, _): (IotaDocument, _) = create_did_document(&network, &storage).await?; - - // Create a new DID for the car that is owned by the car NFT. - let car_did_output: AliasOutput = client - .new_did_output(Address::Nft(car_nft_id.into()), car_document, Some(rent_structure)) - .await?; - - // Publish the car DID. - let car_document: IotaDocument = client.publish_did_output(&secret_manager, car_did_output).await?; - - // ============================================ - // Determine the car's NFT given the car's DID. - // ============================================ - - // Resolve the Alias Output of the DID. - let output: AliasOutput = client.resolve_did_output(car_document.id()).await?; - - // Extract the NFT address from the state controller unlock condition. - let unlock_condition: &UnlockCondition = output - .unlock_conditions() - .iter() - .next() - .ok_or_else(|| anyhow::anyhow!("expected at least one unlock condition"))?; - - let car_nft_address: NftAddress = - if let UnlockCondition::StateControllerAddress(state_controller_unlock_condition) = unlock_condition { - if let Address::Nft(nft_address) = state_controller_unlock_condition.address() { - *nft_address - } else { - anyhow::bail!("expected an NFT address as the unlock condition"); - } - } else { - anyhow::bail!("expected an Address as the unlock condition"); - }; - - // Retrieve the NFT Output of the car. - let car_nft_id: &NftId = car_nft_address.nft_id(); - let output_id: OutputId = client.nft_output_id(*car_nft_id).await?; - let output: Output = client.get_output(&output_id).await?.into_output(); - - let car_nft: NftOutput = if let Output::Nft(nft_output) = output { - nft_output - } else { - anyhow::bail!("expected an NFT output"); - }; - - println!("The car's DID is: {car_document:#}"); - println!("The car's NFT is: {car_nft:#?}"); - - Ok(()) -} - -// Helper function to get the output id for the first NFT output in a Block. -fn get_nft_output_id(payload: &Payload) -> anyhow::Result { - match payload { - Payload::Transaction(tx_payload) => { - let TransactionEssence::Regular(regular) = tx_payload.essence(); - for (index, output) in regular.outputs().iter().enumerate() { - if let Output::Nft(_nft_output) = output { - return Ok(OutputId::new(tx_payload.id(), index.try_into().unwrap())?); - } - } - anyhow::bail!("no NFT output in transaction essence") - } - _ => anyhow::bail!("No transaction payload"), - } -} diff --git a/examples/1_advanced/3_did_issues_tokens.rs b/examples/1_advanced/3_did_issues_tokens.rs deleted file mode 100644 index 3ebfeb5018..0000000000 --- a/examples/1_advanced/3_did_issues_tokens.rs +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::ops::Deref; - -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::core::Duration; -use identity_iota::core::Timestamp; -use identity_iota::iota::block::output::unlock_condition::AddressUnlockCondition; -use identity_iota::iota::block::output::unlock_condition::ExpirationUnlockCondition; -use identity_iota::iota::block::output::BasicOutput; -use identity_iota::iota::block::output::BasicOutputBuilder; -use identity_iota::iota::block::output::Output; -use identity_iota::iota::block::output::OutputId; -use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::api::GetAddressesOptions; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::AliasAddress; -use iota_sdk::types::block::address::ToBech32Ext; -use iota_sdk::types::block::output::unlock_condition::ImmutableAliasAddressUnlockCondition; -use iota_sdk::types::block::output::AliasId; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::FoundryId; -use iota_sdk::types::block::output::FoundryOutput; -use iota_sdk::types::block::output::FoundryOutputBuilder; -use iota_sdk::types::block::output::NativeToken; -use iota_sdk::types::block::output::RentStructure; -use iota_sdk::types::block::output::SimpleTokenScheme; -use iota_sdk::types::block::output::TokenId; -use iota_sdk::types::block::output::TokenScheme; -use iota_sdk::types::block::output::UnlockCondition; -use iota_sdk::types::block::Block; -use primitive_types::U256; - -/// Demonstrates how an identity can issue and control -/// a Token Foundry and its tokens. -/// -/// For this example, we consider the case where an authority issues -/// carbon credits that can be used to pay for carbon emissions or traded on a marketplace. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // =========================================== - // Create the authority's DID and the foundry. - // =========================================== - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID for the authority. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, authority_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let authority_did = authority_document.id().clone(); - - let rent_structure: RentStructure = client.get_rent_structure().await?; - - // We want to update the foundry counter of the authority's Alias Output, so we create an - // updated version of the output. We pass in the previous document, - // because we don't want to modify it in this update. - let authority_document: IotaDocument = client.resolve_did(&authority_did).await?; - let authority_alias_output: AliasOutput = client.update_did_output(authority_document).await?; - - // We will add one foundry to this Alias Output. - let authority_alias_output = AliasOutputBuilder::from(&authority_alias_output) - .with_foundry_counter(1) - .finish()?; - - // Create a token foundry that represents carbon credits. - let token_scheme = TokenScheme::Simple(SimpleTokenScheme::new( - U256::from(500_000u32), - U256::from(0u8), - U256::from(1_000_000u32), - )?); - - // Create the identifier of the foundry, which is partially derived from the Alias Address. - let foundry_id = FoundryId::build( - &AliasAddress::new(AliasId::from(&authority_did)), - 1, - token_scheme.kind(), - ); - - // Create the Foundry Output. - let carbon_credits_foundry: FoundryOutput = - FoundryOutputBuilder::new_with_minimum_storage_deposit(rent_structure, 1, token_scheme) - // Initially, all carbon credits are owned by the foundry. - .add_native_token(NativeToken::new(TokenId::from(foundry_id), U256::from(500_000u32))?) - // The authority is set as the immutable owner. - .add_unlock_condition(UnlockCondition::ImmutableAliasAddress( - ImmutableAliasAddressUnlockCondition::new(AliasAddress::new(AliasId::from(&authority_did))), - )) - .finish()?; - - let carbon_credits_foundry_id: FoundryId = carbon_credits_foundry.id(); - - // Publish all outputs. - let block: Block = client - .build_block() - .with_secret_manager(&secret_manager) - .with_outputs(vec![authority_alias_output.into(), carbon_credits_foundry.into()])? - .finish() - .await?; - let _ = client.retry_until_included(&block.id(), None, None).await?; - - // =================================== - // Resolve Foundry and its issuer DID. - // =================================== - - // Get the latest output that contains the foundry. - let foundry_output_id: OutputId = client.foundry_output_id(carbon_credits_foundry_id).await?; - let carbon_credits_foundry: Output = client.get_output(&foundry_output_id).await?.into_output(); - - let carbon_credits_foundry: FoundryOutput = if let Output::Foundry(foundry_output) = carbon_credits_foundry { - foundry_output - } else { - anyhow::bail!("expected foundry output") - }; - - // Get the Alias Id of the authority that issued the carbon credits foundry. - let authority_alias_id: &AliasId = carbon_credits_foundry.alias_address().alias_id(); - - // Reconstruct the DID of the authority. - let network: NetworkName = client.network_name().await?; - let authority_did: IotaDID = IotaDID::new(authority_alias_id.deref(), &network); - - // Resolve the authority's DID document. - let authority_document: IotaDocument = client.resolve_did(&authority_did).await?; - - println!("The authority's DID is: {authority_document:#}"); - - // ========================================================= - // Transfer 1000 carbon credits to the address of a company. - // ========================================================= - - // Create a new address that represents the company. - let company_address: Address = *secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_bech32_hrp((&network).try_into()?) - .with_range(1..2), - ) - .await?[0]; - - // Create the timestamp at which the basic output will expire. - let tomorrow: u32 = Timestamp::now_utc() - .checked_add(Duration::seconds(60 * 60 * 24)) - .ok_or_else(|| anyhow::anyhow!("timestamp overflow"))? - .to_unix() - .try_into() - .map_err(|err| anyhow::anyhow!("cannot fit timestamp into u32: {err}"))?; - - // Create a basic output containing our carbon credits that we'll send to the company's address. - let basic_output: BasicOutput = BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure) - .add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(company_address))) - .add_native_token(NativeToken::new(carbon_credits_foundry.token_id(), U256::from(1000))?) - // Allow the company to claim the credits within 24 hours by using an expiration unlock condition. - .add_unlock_condition(UnlockCondition::Expiration(ExpirationUnlockCondition::new( - Address::Alias(AliasAddress::new(*authority_alias_id)), - tomorrow, - )?)) - .finish()?; - - // Reduce the carbon credits in the foundry by the amount that is sent to the company. - let carbon_credits_foundry = FoundryOutputBuilder::from(&carbon_credits_foundry) - .with_native_tokens(vec![NativeToken::new( - carbon_credits_foundry.token_id(), - U256::from(499_000u32), - )?]) - .finish()?; - - // Publish the output, transferring the carbon credits. - let block: Block = client - .build_block() - .with_secret_manager(&secret_manager) - .with_outputs(vec![basic_output.into(), carbon_credits_foundry.into()])? - .finish() - .await?; - let _ = client.retry_until_included(&block.id(), None, None).await?; - - println!( - "Sent carbon credits to {}", - company_address.to_bech32((&network).try_into()?) - ); - - Ok(()) -} diff --git a/examples/1_advanced/4_alias_output_history.rs b/examples/1_advanced/4_alias_output_history.rs index b46b05dd6c..e11a1d81ae 100644 --- a/examples/1_advanced/4_alias_output_history.rs +++ b/examples/1_advanced/4_alias_output_history.rs @@ -1,70 +1,40 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use anyhow::Context; -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Timestamp; use identity_iota::did::DID; use identity_iota::document::Service; -use identity_iota::iota::block::address::Address; -use identity_iota::iota::block::output::RentStructure; -use identity_iota::iota::IotaClientExt; +use identity_iota::iota::rebased::client::get_object_id_from_did; +use identity_iota::iota::rebased::migration::has_previous_version; +use identity_iota::iota::rebased::migration::Identity; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClient; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; use identity_iota::verification::MethodRelationship; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::input::Input; -use iota_sdk::types::block::output::AliasId; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::Output; -use iota_sdk::types::block::output::OutputId; -use iota_sdk::types::block::output::OutputMetadata; -use iota_sdk::types::block::payload::transaction::TransactionEssence; -use iota_sdk::types::block::payload::Payload; -use iota_sdk::types::block::Block; +use iota_sdk::rpc_types::IotaObjectData; /// Demonstrates how to obtain the alias output history. #[tokio::main] async fn main() -> anyhow::Result<()> { // Create a new client to interact with the IOTA ledger. // NOTE: a permanode is required to fetch older output histories. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID in an Alias Output for us to modify. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; + let storage = get_memstorage()?; + let identity_client = get_client_and_create_account(&storage).await?; + // create new DID document and publish it + let (document, vm_fragment_1) = create_kinesis_did_document(&identity_client, &storage).await?; let did: IotaDID = document.id().clone(); // Resolve the latest state of the document. - let mut document: IotaDocument = client.resolve_did(&did).await?; + let mut document: IotaDocument = identity_client.resolve_did(&did).await?; // Attach a new method relationship to the existing method. document.attach_method_relationship( - &document.id().to_url().join(format!("#{fragment}"))?, + &document.id().to_url().join(format!("#{vm_fragment_1}"))?, MethodRelationship::Authentication, )?; @@ -77,99 +47,59 @@ async fn main() -> anyhow::Result<()> { assert!(document.insert_service(service).is_ok()); document.metadata.updated = Some(Timestamp::now_utc()); - // Increase the storage deposit and publish the update. - let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - client.publish_did_output(&secret_manager, alias_output).await?; + identity_client + .publish_did_document_update(document.clone(), TEST_GAS_BUDGET) + .await?; } // ==================================== // Retrieving the Alias Output History // ==================================== - let mut alias_history: Vec = Vec::new(); - - // Step 0 - Get the latest Alias Output - let alias_id: AliasId = AliasId::from(client.resolve_did(&did).await?.id()); - let (mut output_id, mut alias_output): (OutputId, AliasOutput) = client.get_alias_output(alias_id).await?; - while alias_output.state_index() != 0 { - // Step 1 - Get the current block - let block: Block = current_block(&client, &output_id).await?; - // Step 2 - Get the OutputId of the previous block - output_id = previous_output_id(&block)?; - // Step 3 - Get the Alias Output from the block - alias_output = block_alias_output(&block, &alias_id)?; - alias_history.push(alias_output.clone()); + // Step 1 - Get the latest identity + let identity = identity_client.get_identity(get_object_id_from_did(&did)?).await?; + let onchain_identity = if let Identity::FullFledged(value) = identity { + value + } else { + anyhow::bail!("history only available for onchain identities"); + }; + + // Step 2 - Get history + let history = onchain_identity.get_history(&identity_client, None, None).await?; + println!("Alias History has {} entries", history.len()); + + // Depending on your use case, you can also page through the results + // Alternative Step 2 - Page by looping until no result is returned (here with page size 1) + let mut current_item: Option<&IotaObjectData> = None; + let mut history: Vec; + loop { + history = onchain_identity + .get_history(&identity_client, current_item, Some(1)) + .await?; + if history.is_empty() { + break; + } + current_item = history.first(); + let IotaObjectData { object_id, version, .. } = current_item.unwrap(); + println!("Alias History entry: object_id: {object_id}, version: {version}"); } - println!("Alias History: {alias_history:?}"); - - Ok(()) -} + // Alternative Step 2 - Page by looping with pre-fetch next page check (again with page size 1) + let mut current_item: Option<&IotaObjectData> = None; + let mut history: Vec; + loop { + history = onchain_identity + .get_history(&identity_client, current_item, Some(1)) + .await?; -async fn current_block(client: &Client, output_id: &OutputId) -> anyhow::Result { - let output_metadata: OutputMetadata = client.get_output_metadata(output_id).await?; - let block: Block = client.get_block(output_metadata.block_id()).await?; - Ok(block) -} + current_item = history.first(); + let IotaObjectData { object_id, version, .. } = current_item.unwrap(); + println!("Alias History entry: object_id: {object_id}, version: {version}"); -fn previous_output_id(block: &Block) -> anyhow::Result { - match block - .payload() - .context("expected a transaction payload, but no payload was found")? - { - Payload::Transaction(transaction_payload) => match transaction_payload.essence() { - TransactionEssence::Regular(regular_transaction_essence) => { - match regular_transaction_essence - .inputs() - .first() - .context("expected an utxo for the block, but no input was found")? - { - Input::Utxo(utxo_input) => Ok(*utxo_input.output_id()), - Input::Treasury(_) => { - anyhow::bail!("expected an utxo input, found a treasury input"); - } - } - } - }, - Payload::Milestone(_) | Payload::TreasuryTransaction(_) | Payload::TaggedData(_) => { - anyhow::bail!("expected a transaction payload"); + if !has_previous_version(current_item.unwrap())? { + break; } } -} -fn block_alias_output(block: &Block, alias_id: &AliasId) -> anyhow::Result { - match block - .payload() - .context("expected a transaction payload, but no payload was found")? - { - Payload::Transaction(transaction_payload) => match transaction_payload.essence() { - TransactionEssence::Regular(regular_transaction_essence) => { - for (index, output) in regular_transaction_essence.outputs().iter().enumerate() { - match output { - Output::Alias(alias_output) => { - if &alias_output.alias_id().or_from_output_id( - &OutputId::new( - transaction_payload.id(), - index.try_into().context("output index must fit into a u16")?, - ) - .context("failed to create OutputId")?, - ) == alias_id - { - return Ok(alias_output.clone()); - } - } - Output::Basic(_) | Output::Foundry(_) | Output::Nft(_) | Output::Treasury(_) => continue, - } - } - } - }, - Payload::Milestone(_) | Payload::TreasuryTransaction(_) | Payload::TaggedData(_) => { - anyhow::bail!("expected a transaction payload"); - } - } - anyhow::bail!("no alias output has been found"); + Ok(()) } diff --git a/examples/1_advanced/5_custom_resolution.rs b/examples/1_advanced/5_custom_resolution.rs index b0675c8dd5..dae4672fc8 100644 --- a/examples/1_advanced/5_custom_resolution.rs +++ b/examples/1_advanced/5_custom_resolution.rs @@ -1,25 +1,16 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; use identity_iota::core::FromJson; use identity_iota::core::ToJson; use identity_iota::did::CoreDID; use identity_iota::did::DID; use identity_iota::document::CoreDocument; -use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; use identity_iota::resolver::Resolver; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; /// Demonstrates how to set up a resolver using custom handlers. /// @@ -27,41 +18,28 @@ use iota_sdk::types::block::address::Address; /// Resolver in this example and just worked with `CoreDocument` representations throughout. #[tokio::main] async fn main() -> anyhow::Result<()> { + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_client_and_create_account(&storage).await?; + // create new DID document and publish it + let (document, _) = create_kinesis_did_document(&identity_client, &storage).await?; + // Create a resolver returning an enum of the documents we are interested in and attach handlers for the "foo" and // "iota" methods. let mut resolver: Resolver = Resolver::new(); - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - // This is a convenience method for attaching a handler for the "iota" method by providing just a client. - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler((*identity_client).clone()); resolver.attach_handler("foo".to_owned(), resolve_did_foo); // A fake did:foo DID for demonstration purposes. let did_foo: CoreDID = "did:foo:0e9c8294eeafee326a4e96d65dbeaca0".parse()?; - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID for us to resolve. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, iota_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let iota_did: IotaDID = iota_document.id().clone(); - // Resolve did_foo to get an abstract document. let did_foo_doc: Document = resolver.resolve(&did_foo).await?; // Resolve iota_did to get an abstract document. - let iota_doc: Document = resolver.resolve(&iota_did).await?; + let iota_doc: Document = resolver.resolve(&document.id().clone()).await?; // The Resolver is mainly meant for validating presentations, but here we will just // check that the resolved documents match our expectations. diff --git a/examples/1_advanced/6_domain_linkage.rs b/examples/1_advanced/6_domain_linkage.rs index 03d0472a37..397c7ba5ae 100644 --- a/examples/1_advanced/6_domain_linkage.rs +++ b/examples/1_advanced/6_domain_linkage.rs @@ -1,10 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Duration; use identity_iota::core::FromJson; @@ -24,46 +24,20 @@ use identity_iota::credential::LinkedDomainService; use identity_iota::did::CoreDID; use identity_iota::did::DIDUrl; use identity_iota::did::DID; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - let stronghold_path = random_stronghold_path(); - - println!("Using stronghold path: {stronghold_path:?}"); - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(stronghold_path)?, - ); - + // Create new client to interact with chain and get funded account with keys. + let storage = get_memstorage()?; + let identity_client = get_client_and_create_account(&storage).await?; // Create a DID for the entity that will issue the Domain Linkage Credential. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, mut did_document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let did: IotaDID = did_document.id().clone(); + let (mut document, vm_fragment_1) = create_kinesis_did_document(&identity_client, &storage).await?; + let did: IotaDID = document.id().clone(); // ===================================================== // Create Linked Domain service @@ -81,8 +55,10 @@ async fn main() -> anyhow::Result<()> { // This is optional since it is not a hard requirement by the specs. let service_url: DIDUrl = did.clone().join("#domain-linkage")?; let linked_domain_service: LinkedDomainService = LinkedDomainService::new(service_url, domains, Object::new())?; - did_document.insert_service(linked_domain_service.into())?; - let updated_did_document: IotaDocument = publish_document(client.clone(), secret_manager, did_document).await?; + document.insert_service(linked_domain_service.into())?; + let updated_did_document: IotaDocument = identity_client + .publish_did_document_update(document.clone(), TEST_GAS_BUDGET) + .await?; println!("DID document with linked domain service: {updated_did_document:#}"); @@ -112,7 +88,7 @@ async fn main() -> anyhow::Result<()> { .create_credential_jwt( &domain_linkage_credential, &storage, - &fragment, + &vm_fragment_1, &JwsSignatureOptions::default(), None, ) @@ -138,7 +114,7 @@ async fn main() -> anyhow::Result<()> { // Init a resolver for resolving DID Documents. let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_kinesis_iota_handler((*identity_client).clone()); // ===================================================== // → Case 1: starting from domain @@ -212,22 +188,3 @@ async fn main() -> anyhow::Result<()> { assert!(validation_result.is_ok()); Ok(()) } - -async fn publish_document( - client: Client, - secret_manager: SecretManager, - document: IotaDocument, -) -> anyhow::Result { - // Resolve the latest output and update it with the given document. - let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the updated Alias Output. - Ok(client.publish_did_output(&secret_manager, alias_output).await?) -} diff --git a/examples/1_advanced/7_sd_jwt.rs b/examples/1_advanced/7_sd_jwt.rs index 2d2a4665ee..c7c877f31c 100644 --- a/examples/1_advanced/7_sd_jwt.rs +++ b/examples/1_advanced/7_sd_jwt.rs @@ -6,11 +6,10 @@ //! //! cargo run --release --example 7_sd_jwt -use examples::create_did; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; use examples::pretty_print_json; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::json; use identity_iota::core::FromJson; @@ -27,16 +26,8 @@ use identity_iota::credential::KeyBindingJWTValidationOptions; use identity_iota::credential::SdJwtCredentialValidator; use identity_iota::credential::Subject; use identity_iota::did::DID; -use identity_iota::iota::IotaDocument; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; use sd_jwt_payload::KeyBindingJwtClaims; use sd_jwt_payload::SdJwt; use sd_jwt_payload::SdObjectDecoder; @@ -49,31 +40,17 @@ async fn main() -> anyhow::Result<()> { // Step 1: Create identities for the issuer and the holder. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - // Create an identity for the issuer with one verification method `key-1`. - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - let issuer_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, issuer_document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &issuer_storage).await?; + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_client_and_create_account(&issuer_storage).await?; + let (issuer_document, issuer_vm_fragment) = + create_kinesis_did_document(&issuer_identity_client, &issuer_storage).await?; // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let alice_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, alice_fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &alice_storage).await?; + let holder_storage = get_memstorage()?; + let holder_identity_client = get_client_and_create_account(&holder_storage).await?; + let (holder_document, holder_vm_fragment) = + create_kinesis_did_document(&holder_identity_client, &holder_storage).await?; // =========================================================================== // Step 2: Issuer creates and signs a selectively disclosable JWT verifiable credential. @@ -81,7 +58,7 @@ async fn main() -> anyhow::Result<()> { // Create an address credential subject. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "address": { "locality": "Maxstadt", @@ -128,7 +105,7 @@ async fn main() -> anyhow::Result<()> { let jwt: Jws = issuer_document .create_jws( &issuer_storage, - &fragment, + &issuer_vm_fragment, encoded_payload.as_bytes(), &JwsSignatureOptions::default(), ) @@ -183,8 +160,13 @@ async fn main() -> anyhow::Result<()> { let options = JwsSignatureOptions::new().typ(KeyBindingJwtClaims::KB_JWT_HEADER_TYP); // Create the KB-JWT. - let kb_jwt: Jws = alice_document - .create_jws(&alice_storage, &alice_fragment, binding_claims.as_bytes(), &options) + let kb_jwt: Jws = holder_document + .create_jws( + &holder_storage, + &holder_vm_fragment, + binding_claims.as_bytes(), + &options, + ) .await?; // Create the final SD-JWT. @@ -219,7 +201,7 @@ async fn main() -> anyhow::Result<()> { // Verify the Key Binding JWT. let options = KeyBindingJWTValidationOptions::new().nonce(nonce).aud(VERIFIER_DID); - let _kb_validation = validator.validate_key_binding_jwt(&sd_jwt_obj, &alice_document, &options)?; + let _kb_validation = validator.validate_key_binding_jwt(&sd_jwt_obj, &holder_document, &options)?; println!("Key Binding JWT successfully validated"); diff --git a/examples/1_advanced/8_status_list_2021.rs b/examples/1_advanced/8_status_list_2021.rs index 0a70690e91..fba8dcc288 100644 --- a/examples/1_advanced/8_status_list_2021.rs +++ b/examples/1_advanced/8_status_list_2021.rs @@ -1,10 +1,9 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_kinesis_did_document; +use examples::get_client_and_create_account; +use examples::get_memstorage; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::FromJson; use identity_iota::core::Object; @@ -28,16 +27,9 @@ use identity_iota::credential::Status; use identity_iota::credential::StatusCheck; use identity_iota::credential::Subject; use identity_iota::did::DID; -use identity_iota::iota::IotaDocument; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; + use serde_json::json; #[tokio::main] @@ -46,32 +38,16 @@ async fn main() -> anyhow::Result<()> { // Create a Verifiable Credential. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create an identity for the issuer with one verification method `key-1`. - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &storage_issuer).await?; + // create new issuer account with did document + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_client_and_create_account(&issuer_storage).await?; + let (issuer_document, issuer_vm_fragment) = + create_kinesis_did_document(&issuer_identity_client, &issuer_storage).await?; - // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_alice: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &storage_alice).await?; + // create new holder account with did document + let holder_storage = get_memstorage()?; + let holder_identity_client = get_client_and_create_account(&holder_storage).await?; + let (holder_document, _) = create_kinesis_did_document(&holder_identity_client, &holder_storage).await?; // Create a new empty status list. No credentials have been revoked yet. let status_list: StatusList2021 = StatusList2021::default(); @@ -89,7 +65,7 @@ async fn main() -> anyhow::Result<()> { // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", @@ -123,8 +99,8 @@ async fn main() -> anyhow::Result<()> { let credential_jwt: Jwt = issuer_document .create_credential_jwt( &credential, - &storage_issuer, - &fragment_issuer, + &issuer_storage, + &issuer_vm_fragment, &JwsSignatureOptions::default(), None, ) diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index eeb4246280..ddceb2e900 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -1,11 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 -use examples::get_address_with_funds; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use examples::FAUCET_ENDPOINT; +use examples::get_client_and_create_account; + +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Object; @@ -26,81 +25,63 @@ use identity_iota::credential::SelectiveDisclosurePresentation; use identity_iota::credential::Subject; use identity_iota::did::CoreDID; use identity_iota::did::DID; -use identity_iota::iota::IotaClientExt; + +use identity_iota::iota::rebased::transaction::TransactionOutput; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwpDocumentExt; -use identity_iota::storage::KeyIdMemstore; use identity_iota::storage::KeyType; use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; + +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IotaKeySignature; +use identity_iota::iota::rebased::transaction::Transaction; +use identity_storage::Storage; use jsonprooftoken::jpa::algs::ProofAlgorithm; +use secret_storage::Signer; // Creates a DID with a JWP verification method. -async fn create_did( - client: &Client, - secret_manager: &SecretManager, - storage: &MemStorage, +pub async fn create_did( + identity_client: &IdentityClient, + storage: &Storage, key_type: KeyType, alg: ProofAlgorithm, -) -> anyhow::Result<(Address, IotaDocument, String)> { - // Get an address with funds for testing. - let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?; - - // Get the Bech32 human-readable part (HRP) of the network. - let network_name: NetworkName = client.network_name().await?; - +) -> anyhow::Result<(IotaDocument, String)> +where + K: identity_storage::JwkStorage + identity_storage::JwkStorageBbsPlusExt, + I: identity_storage::KeyIdStorage, + S: Signer + Sync, +{ // Create a new DID document with a placeholder DID. // The DID will be derived from the Alias Id of the Alias Output after publishing. - let mut document: IotaDocument = IotaDocument::new(&network_name); + let mut unpublished: IotaDocument = IotaDocument::new(identity_client.network()); - let fragment = document + let verification_method_fragment = unpublished .generate_method_jwp(storage, key_type, alg, None, MethodScope::VerificationMethod) .await?; - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; - - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; - println!("Published DID document: {document:#}"); + let TransactionOutput:: { output: document, .. } = identity_client + .publish_did_document(unpublished) + .execute_with_gas(TEST_GAS_BUDGET, identity_client) + .await?; - Ok((address, document, fragment)) + Ok((document, verification_method_fragment)) } /// Demonstrates how to create an Anonymous Credential with BBS+. #[tokio::main] async fn main() -> anyhow::Result<()> { // =========================================================================== - // Step 1: Create identity for the issuer. + // Step 1: Create identities and Client // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - let secret_manager_issuer = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); + let storage_issuer = get_memstorage()?; - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let identity_client = get_client_and_create_account(&storage_issuer).await?; - let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( - &client, - &secret_manager_issuer, + let (issuer_document, fragment_issuer): (IotaDocument, String) = create_did( + &identity_client, &storage_issuer, JwkMemStore::BLS12381G2_KEY_TYPE, ProofAlgorithm::BLS12381_SHA256, @@ -165,7 +146,7 @@ async fn main() -> anyhow::Result<()> { // ============================================================================================ let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); + resolver.attach_kinesis_iota_handler((*identity_client).clone()); // Holder resolves issuer's DID let issuer: CoreDID = JptCredentialValidatorUtils::extract_issuer_from_issued_jpt(&credential_jpt).unwrap(); diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 1a3d313705..167115e919 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,18 +7,36 @@ publish = false [dependencies] anyhow = "1.0.62" -bls12_381_plus.workspace = true -identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } -identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus", "resolver"] } -identity_stronghold = { path = "../identity_stronghold", default-features = false, features = ["bbs-plus"] } -iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } +identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +identity_storage = { path = "../identity_storage" } +identity_stronghold = { path = "../identity_stronghold", default-features = false, features = [ + "send-sync-storage", +] } +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.7.0-alpha" } +iota-sdk-legacy = { package = "iota-sdk", version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } json-proof-token.workspace = true -primitive-types = "0.12.1" rand = "0.8.5" sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"] } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", branch = "main" } serde_json = { version = "1.0", default-features = false } tokio = { version = "1.29", default-features = false, features = ["rt"] } +[dependencies.identity_iota] +path = "../identity_iota" +default-features = false +features = [ + "client", + "domain-linkage", + "iota-client", + "jpt-bbs-plus", + "kinesis-client", + "memstore", + "resolver", + "revocation-bitmap", + "sd-jwt", + "status-list-2021", +] + [lib] path = "utils/utils.rs" @@ -38,10 +56,6 @@ name = "2_resolve_did" path = "0_basic/3_deactivate_did.rs" name = "3_deactivate_did" -[[example]] -path = "0_basic/4_delete_did.rs" -name = "4_delete_did" - [[example]] path = "0_basic/5_create_vc.rs" name = "5_create_vc" @@ -58,22 +72,6 @@ name = "7_revoke_vc" path = "0_basic/8_stronghold.rs" name = "8_stronghold" -[[example]] -path = "1_advanced/0_did_controls_did.rs" -name = "0_did_controls_did" - -[[example]] -path = "1_advanced/1_did_issues_nft.rs" -name = "1_did_issues_nft" - -[[example]] -path = "1_advanced/2_nft_owns_did.rs" -name = "2_nft_owns_did" - -[[example]] -path = "1_advanced/3_did_issues_tokens.rs" -name = "3_did_issues_tokens" - [[example]] path = "1_advanced/4_alias_output_history.rs" name = "4_alias_output_history" diff --git a/examples/utils/utils.rs b/examples/utils/utils.rs index a79a74312e..1f72f0415e 100644 --- a/examples/utils/utils.rs +++ b/examples/utils/utils.rs @@ -4,70 +4,50 @@ use std::path::PathBuf; use anyhow::Context; - -use identity_iota::iota::block::output::AliasOutput; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; use identity_iota::storage::KeyIdMemstore; use identity_iota::storage::Storage; +use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; -use identity_iota::verification::jws::JwsAlgorithm; -use iota_sdk::client::api::GetAddressesOptions; -use iota_sdk::client::node_api::indexer::query_parameters::QueryParameter; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::crypto::keys::bip39; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::Bech32Address; -use iota_sdk::types::block::address::Hrp; +use identity_iota::iota::rebased::client::convert_to_address; +use identity_iota::iota::rebased::client::get_sender_public_key; +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use identity_iota::iota::rebased::client::IotaKeySignature; +use identity_iota::iota::rebased::transaction::Transaction; +use identity_iota::iota::rebased::utils::request_funds; +use identity_storage::JwkStorage; +use identity_storage::KeyIdStorage; +use identity_storage::KeyType; +use identity_storage::StorageSigner; +use identity_stronghold::StrongholdStorage; +use iota_sdk::IotaClientBuilder; +use iota_sdk_legacy::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk_legacy::client::Password; use rand::distributions::DistString; +use secret_storage::Signer; use serde_json::Value; -pub static API_ENDPOINT: &str = "http://localhost"; -pub static FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue"; +pub const TEST_GAS_BUDGET: u64 = 50_000_000; pub type MemStorage = Storage; -/// Creates a DID Document and publishes it in a new Alias Output. -/// -/// Its functionality is equivalent to the "create DID" example -/// and exists for convenient calling from the other examples. -pub async fn create_did( - client: &Client, - secret_manager: &mut SecretManager, - storage: &MemStorage, -) -> anyhow::Result<(Address, IotaDocument, String)> { - let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT) - .await - .context("failed to get address with funds")?; - - let network_name: NetworkName = client.network_name().await?; - - let (document, fragment): (IotaDocument, String) = create_did_document(&network_name, storage).await?; - - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; - - let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; - - Ok((address, document, fragment)) -} - -/// Creates an example DID document with the given `network_name`. -/// -/// Its functionality is equivalent to the "create DID" example -/// and exists for convenient calling from the other examples. -pub async fn create_did_document( - network_name: &NetworkName, - storage: &MemStorage, -) -> anyhow::Result<(IotaDocument, String)> { - let mut document: IotaDocument = IotaDocument::new(network_name); - - let fragment: String = document +pub async fn create_kinesis_did_document( + identity_client: &IdentityClient, + storage: &Storage, +) -> anyhow::Result<(IotaDocument, String)> +where + K: identity_storage::JwkStorage, + I: identity_storage::KeyIdStorage, + S: Signer + Sync, +{ + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + let mut unpublished: IotaDocument = IotaDocument::new(identity_client.network()); + let verification_method_fragment = unpublished .generate_method( storage, JwkMemStore::ED25519_KEY_TYPE, @@ -77,104 +57,86 @@ pub async fn create_did_document( ) .await?; - Ok((document, fragment)) + let document = identity_client + .publish_did_document(unpublished) + .execute_with_gas(TEST_GAS_BUDGET, identity_client) + .await? + .output; + + Ok((document, verification_method_fragment)) } -/// Generates an address from the given [`SecretManager`] and adds funds from the faucet. -pub async fn get_address_with_funds( - client: &Client, - stronghold: &SecretManager, - faucet_endpoint: &str, -) -> anyhow::Result
{ - let address: Bech32Address = get_address(client, stronghold).await?; +/// Creates a random stronghold path in the temporary directory, whose exact location is OS-dependent. +pub fn random_stronghold_path() -> PathBuf { + let mut file = std::env::temp_dir(); + file.push("test_strongholds"); + file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); + file.set_extension("stronghold"); + file.to_owned() +} - request_faucet_funds(client, address, faucet_endpoint) +pub async fn get_client_and_create_account( + storage: &Storage, +) -> Result>, anyhow::Error> +where + K: JwkStorage, + I: KeyIdStorage, +{ + // The API endpoint of an IOTA node + let api_endpoint: &str = "http://127.0.0.1:9000"; + + let iota_client = IotaClientBuilder::default() + .build(api_endpoint) .await - .context("failed to request faucet funds")?; + .map_err(|err| anyhow::anyhow!(format!("failed to connect to network; {}", err)))?; - Ok(*address) -} + // generate new key + let generate = storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await?; + let public_key_jwk = generate.jwk.to_public().expect("public components should be derivable"); + let public_key_bytes = get_sender_public_key(&public_key_jwk)?; + let sender_address = convert_to_address(&public_key_bytes)?; + request_funds(&sender_address).await?; + let package_id = std::env::var("IDENTITY_IOTA_PKG_ID") + .map_err(|e| { + anyhow::anyhow!("env variable IDENTITY_IOTA_PKG_ID must be set in order to run the examples").context(e) + }) + .and_then(|pkg_str| pkg_str.parse().context("invalid package id"))?; -/// Initializes the [`SecretManager`] with a new mnemonic, if necessary, -/// and generates an address from the given [`SecretManager`]. -pub async fn get_address(client: &Client, secret_manager: &SecretManager) -> anyhow::Result { - let random: [u8; 32] = rand::random(); - let mnemonic = bip39::wordlist::encode(random.as_ref(), &bip39::wordlist::ENGLISH) - .map_err(|err| anyhow::anyhow!(format!("{err:?}")))?; - - if let SecretManager::Stronghold(ref stronghold) = secret_manager { - match stronghold.store_mnemonic(mnemonic).await { - Ok(()) => (), - Err(iota_sdk::client::stronghold::Error::MnemonicAlreadyStored) => (), - Err(err) => anyhow::bail!(err), - } - } else { - anyhow::bail!("expected a `StrongholdSecretManager`"); - } - - let bech32_hrp: Hrp = client.get_bech32_hrp().await?; - let address: Bech32Address = secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_range(0..1) - .with_bech32_hrp(bech32_hrp), - ) - .await?[0]; + let read_only_client = IdentityClientReadOnly::new(iota_client, package_id).await?; + + let signer = StorageSigner::new(storage, generate.key_id, public_key_jwk); - Ok(address) + let identity_client = IdentityClient::new(read_only_client, signer).await?; + + Ok(identity_client) } -/// Requests funds from the faucet for the given `address`. -async fn request_faucet_funds(client: &Client, address: Bech32Address, faucet_endpoint: &str) -> anyhow::Result<()> { - iota_sdk::client::request_funds_from_faucet(faucet_endpoint, &address).await?; - - tokio::time::timeout(std::time::Duration::from_secs(45), async { - loop { - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - - let balance = get_address_balance(client, &address) - .await - .context("failed to get address balance")?; - if balance > 0 { - break; - } - } - Ok::<(), anyhow::Error>(()) - }) - .await - .context("maximum timeout exceeded")??; - - Ok(()) +pub fn get_memstorage() -> Result { + Ok(MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new())) } -/// Returns the balance of the given Bech32-encoded `address`. -async fn get_address_balance(client: &Client, address: &Bech32Address) -> anyhow::Result { - let output_ids = client - .basic_output_ids(vec![ - QueryParameter::Address(address.to_owned()), - QueryParameter::HasExpiration(false), - QueryParameter::HasTimelock(false), - QueryParameter::HasStorageDepositReturn(false), - ]) - .await?; +pub fn get_stronghold_storage( + path: Option, +) -> Result, anyhow::Error> { + // Stronghold snapshot path. + let path = path.unwrap_or_else(random_stronghold_path); - let outputs = client.get_outputs(&output_ids).await?; + // Stronghold password. + let password = Password::from("secure_password".to_owned()); - let mut total_amount = 0; - for output_response in outputs { - total_amount += output_response.output().amount(); - } + let stronghold = StrongholdSecretManager::builder() + .password(password.clone()) + .build(path.clone())?; - Ok(total_amount) -} + // Create a `StrongholdStorage`. + // `StrongholdStorage` creates internally a `SecretManager` that can be + // referenced to avoid creating multiple instances around the same stronghold snapshot. + let stronghold_storage = StrongholdStorage::new(stronghold); -/// Creates a random stronghold path in the temporary directory, whose exact location is OS-dependent. -pub fn random_stronghold_path() -> PathBuf { - let mut file = std::env::temp_dir(); - file.push("test_strongholds"); - file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); - file.set_extension("stronghold"); - file.to_owned() + Ok(Storage::new(stronghold_storage.clone(), stronghold_storage.clone())) } pub fn pretty_print_json(label: &str, value: &str) { diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index fcdd263cc7..a286a9bb85 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -19,7 +19,6 @@ strum.workspace = true thiserror.workspace = true time = { version = "0.3.23", default-features = false, features = ["std", "serde", "parsing", "formatting"] } url = { version = "2.4", default-features = false, features = ["serde"] } -zeroize = { version = "1.6", default-features = false } [target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies] js-sys = { version = "0.3.55", default-features = false, optional = true } diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 62cb6d0a41..e5cda39ff7 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -12,7 +12,6 @@ rust-version.workspace = true description = "An implementation of the Verifiable Credentials standard." [dependencies] -async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } futures = { version = "0.3", default-features = false, optional = true } @@ -41,7 +40,6 @@ anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519", "std", "random"] } proptest = { version = "1.4.0", default-features = false, features = ["std"] } -tokio = { version = "1.35.0", default-features = false, features = ["rt-multi-thread", "macros"] } [package.metadata.docs.rs] # To build locally: diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index 3f2a33f0a7..b1d1886183 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -27,6 +27,18 @@ use crate::credential::Subject; use crate::Error; use crate::Result; +/// A JWT representing a Verifiable Credential. +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct JwtCredential(CredentialJwtClaims<'static>); + +impl TryFrom for Credential { + type Error = Error; + fn try_from(value: JwtCredential) -> std::result::Result { + value.0.try_into_credential() + } +} + /// Implementation of JWT Encoding/Decoding according to [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). /// /// This type is opinionated in the following ways: diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 07b15f4eba..3d7422c83b 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -37,6 +37,7 @@ pub use self::jpt::Jpt; pub use self::jwp_credential_options::JwpCredentialOptions; pub use self::jws::Jws; pub use self::jwt::Jwt; +pub use self::jwt_serialization::JwtCredential; pub use self::linked_domain_service::LinkedDomainService; pub use self::linked_verifiable_presentation_service::LinkedVerifiablePresentationService; pub use self::policy::Policy; diff --git a/identity_credential/src/validator/jwt_credential_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs index a531f088d7..3fb0211ee2 100644 --- a/identity_credential/src/validator/jwt_credential_validation/error.rs +++ b/identity_credential/src/validator/jwt_credential_validation/error.rs @@ -48,7 +48,7 @@ pub enum JwtValidationError { IssuanceDate, /// Indicates that the credential's (resp. presentation's) signature could not be verified using /// the issuer's (resp. holder's) DID Document. - #[error("could not verify the {signer_ctx}'s signature")] + #[error("could not verify the {signer_ctx}'s signature; {source}")] #[non_exhaustive] Signature { /// Signature verification error. diff --git a/identity_document/Cargo.toml b/identity_document/Cargo.toml index 4bb50dd09d..a257ea4de8 100644 --- a/identity_document/Cargo.toml +++ b/identity_document/Cargo.toml @@ -12,7 +12,6 @@ rust-version.workspace = true description = "Method-agnostic implementation of the Decentralized Identifiers (DID) standard." [dependencies] -did_url_parser = { version = "0.2.0", features = ["std", "serde"] } identity_core = { version = "=1.4.0", path = "../identity_core", default-features = false } identity_did = { version = "=1.4.0", path = "../identity_did" } identity_verification = { version = "=1.4.0", path = "../identity_verification", default-features = false } diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 67933e0634..f6db2209d2 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -21,14 +21,8 @@ identity_resolver = { version = "=1.4.0", path = "../identity_resolver", default identity_storage = { version = "=1.4.0", path = "../identity_storage", default-features = false, features = ["iota-document"] } identity_verification = { version = "=1.4.0", path = "../identity_verification", default-features = false } -[dev-dependencies] -anyhow = "1.0.64" -iota-sdk = { version = "1.1.5", default-features = false, features = ["tls", "client"] } -rand = "0.8.5" -tokio = { version = "1.29.0", features = ["full"] } - [features] -default = ["revocation-bitmap", "client", "iota-client", "resolver"] +default = ["revocation-bitmap", "client", "iota-client", "kinesis-client", "resolver"] # Exposes the `IotaIdentityClient` and `IotaIdentityClientExt` traits. client = ["identity_iota_core/client"] @@ -36,6 +30,14 @@ client = ["identity_iota_core/client"] # Enables the iota-client integration, the client trait implementations for it, and the `IotaClientExt` trait. iota-client = ["identity_iota_core/iota-client", "identity_resolver?/iota"] +# Enables the kinesis client integration. +kinesis-client = [ + "identity_iota_core/kinesis-client", + "identity_resolver/kinesis", + "identity_storage/send-sync-storage", + "identity_storage/storage-signer", +] + # Enables revocation with `RevocationBitmap2022`. revocation-bitmap = [ "identity_credential/revocation-bitmap", diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 73dcc4190e..8201050677 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -12,28 +12,58 @@ rust-version.workspace = true description = "An IOTA Ledger integration for the IOTA DID Method." [dependencies] -async-trait = { version = "0.1.56", default-features = false, optional = true } +anyhow = "1.0.75" +async-trait = { version = "0.1.81", default-features = false, optional = true } futures = { version = "0.3", default-features = false } identity_core = { version = "=1.4.0", path = "../identity_core", default-features = false } identity_credential = { version = "=1.4.0", path = "../identity_credential", default-features = false, features = ["validator"] } identity_did = { version = "=1.4.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.4.0", path = "../identity_document", default-features = false } identity_verification = { version = "=1.4.0", path = "../identity_verification", default-features = false } -iota-sdk = { version = "1.1.5", default-features = false, features = ["serde", "std"], optional = true } +iota_sdk_legacy = { version = "1.1.5", default-features = false, features = ["serde", "std"], optional = true, package="iota-sdk" } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } once_cell = { version = "1.18", default-features = false, features = ["std"] } prefix-hex = { version = "0.7", default-features = false } ref-cast = { version = "1.0.14", default-features = false } serde.workspace = true +serde_json.workspace = true strum.workspace = true thiserror.workspace = true +# for feature `kinesis-client` +bcs = { version = "0.1.4", optional = true } +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto", optional = true } +identity_eddsa_verifier = { version = "=1.4.0", path = "../identity_eddsa_verifier", optional = true } +identity_jose = { version = "=1.4.0", path = "../identity_jose", optional = true } +iota-config = { git = "https://github.com/iotaledger/iota.git", package = "iota-config", tag = "v0.7.0-alpha", optional = true } +iota-crypto = { version = "0.23", optional = true } +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.7.0-alpha", optional = true } +itertools = { version = "0.13.0", optional = true } +move-core-types = { git = "https://github.com/iotaledger/iota.git", package = "move-core-types", tag = "v0.7.0-alpha", optional = true } +rand = { version = "0.8.5", optional = true } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", branch = "main", optional = true } +serde-aux = { version = "4.5.0", optional = true } +shared-crypto = { git = "https://github.com/iotaledger/iota.git", package = "shared-crypto", tag = "v0.7.0-alpha", optional = true } +tokio = { version = "1.29.0", default-features = false, optional = true, features = [ + "macros", + "sync", + "rt", + "process", +] } + [dev-dependencies] -anyhow = { version = "1.0.57" } -iota-crypto = { version = "0.23.2", default-features = false, features = ["bip39", "bip39-en"] } +iota-crypto = { version = "0.23", default-features = false, features = ["bip39", "bip39-en"] } proptest = { version = "1.0.0", default-features = false, features = ["std"] } -tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } + +# for feature kinesis-client tests +identity_iota_core= { path = ".", features = ["kinesis-client"] } # enable for e2e tests +identity_storage = { path = "../identity_storage", features = [ + "send-sync-storage", + "storage-signer", +] } +jsonpath-rust = "0.5.1" +serial_test = "3.1.1" [package.metadata.docs.rs] # To build locally: @@ -42,11 +72,29 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["client", "iota-client", "revocation-bitmap", "send-sync-client-ext"] +default = ["client", "iota-client", "kinesis-client", "revocation-bitmap", "send-sync-client-ext"] # Exposes the IotaIdentityClient and IotaIdentityClientExt traits. -client = ["dep:async-trait", "iota-sdk"] -# Enables the implementation of the extension traits on the iota-sdk's Client. -iota-client = ["client", "iota-sdk/client", "iota-sdk/tls"] +client = ["dep:async-trait", "iota_sdk_legacy"] +# Client for rebased. +kinesis-client = [ + "dep:async-trait", + "dep:bcs", + "dep:fastcrypto", + "dep:identity_eddsa_verifier", + "dep:identity_jose", + "dep:iota-config", + "dep:iota-crypto", + "dep:iota-sdk", + "dep:itertools", + "dep:move-core-types", + "dep:rand", + "dep:secret-storage", + "dep:serde-aux", + "dep:shared-crypto", + "dep:tokio", +] +# Enables the implementation of the extension traits on the iota_sdk_legacy's Client. +iota-client = ["client", "iota_sdk_legacy/client", "iota_sdk_legacy/tls"] # Enables revocation with `RevocationBitmap2022`. revocation-bitmap = ["identity_credential/revocation-bitmap"] # Adds Send bounds on the futures produces by the client extension traits. diff --git a/identity_iota_core/packages/iota_identity/Move.toml b/identity_iota_core/packages/iota_identity/Move.toml new file mode 100644 index 0000000000..83cc4519b9 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/Move.toml @@ -0,0 +1,18 @@ +# Copyright (c) 2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "IotaIdentity" +edition = "2024.beta" + +[dependencies] +MoveStdlib = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/move-stdlib", rev = "ead968e573d8feaa1aa78dddadb8b18a8a99870d" } +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "ead968e573d8feaa1aa78dddadb8b18a8a99870d" } +Stardust = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/stardust", rev = "ead968e573d8feaa1aa78dddadb8b18a8a99870d" } + +[addresses] +iota_identity = "0x0" + +[dev-dependencies] + +[dev-addresses] diff --git a/identity_iota_core/packages/iota_identity/sources/asset.move b/identity_iota_core/packages/iota_identity/sources/asset.move new file mode 100644 index 0000000000..5747bf21ab --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/asset.move @@ -0,0 +1,263 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::asset { + public use fun delete_recipient_cap as RecipientCap.delete; + + const EImmutable: u64 = 0; + const ENonTransferable: u64 = 1; + const ENonDeletable: u64 = 2; + const EInvalidRecipient: u64 = 3; + const EInvalidSender: u64 = 4; + const EInvalidAsset: u64 = 5; + + + /// Structures that couples some data `T` with well known + /// ownership and origin, along configurable abilities e.g. + /// transferability, mutability and deletability. + public struct AuthenticatedAsset has key { + id: UID, + inner: T, + origin: address, + owner: address, + mutable: bool, + transferable: bool, + deletable: bool, + } + + /// Creates a new `AuthenticatedAsset` with default configuration: immutable, non-transferable, non-deletable; + /// and sends it to the tx's sender. + public fun new(inner: T, ctx: &mut TxContext) { + new_with_address(inner, ctx.sender(), false, false, false, ctx); + } + + /// Creates a new `AuthenticatedAsset` with configurable properties and sends it to the tx's sender. + public fun new_with_config( + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, + ctx: &mut TxContext + ) { + new_with_address(inner, ctx.sender(), mutable, transferable, deletable, ctx); + } + + /// Returns the address that created this `AuthenticatedAsset`. + public fun origin(self: &AuthenticatedAsset): address { + self.origin + } + + /// Immutably borrow the content of an `AuthenticatedAsset` + public fun borrow(self: &AuthenticatedAsset): &T { + &self.inner + } + + /// Mutably borrow the content of an `AuthenticatedAsset`. + /// This operation will fail if `AuthenticatedAsset` is configured as non-mutable. + public fun borrow_mut(self: &mut AuthenticatedAsset): &mut T { + assert!(self.mutable, EImmutable); + &mut self.inner + } + + /// Updates the value of the stored content. Fails if this `AuthenticatedAsset` is immutable. + public fun set_content(self: &mut AuthenticatedAsset, new_content: T) { + assert!(self.mutable, EImmutable); + self.inner = new_content; + } + + public fun delete(self: AuthenticatedAsset) { + assert!(self.deletable, ENonDeletable); + let AuthenticatedAsset { + id, + inner: _, + origin: _, + owner: _, + mutable: _, + transferable: _, + deletable: _, + } = self; + object::delete(id); + } + + public(package) fun new_with_address( + inner: T, + addr: address, + mutable: bool, + transferable: bool, + deletable: bool, + ctx: &mut TxContext, + ) { + let asset = AuthenticatedAsset { + id: object::new(ctx), + inner, + origin: addr, + owner: addr, + mutable, + transferable, + deletable, + }; + transfer::transfer(asset, addr); + } + + public fun transfer( + asset: AuthenticatedAsset, + recipient: address, + ctx: &mut TxContext, + ) { + assert!(asset.transferable, ENonTransferable); + let sender_cap = SenderCap { id: object::new(ctx) }; + let recipient_cap = RecipientCap { id: object::new(ctx) }; + let proposal = TransferProposal { + id: object::new(ctx), + asset_id: object::id(&asset), + sender_cap_id: object::id(&sender_cap), + sender_address: asset.owner, + recipient_cap_id: object::id(&recipient_cap), + recipient_address: recipient, + done: false, + }; + + transfer::transfer(sender_cap, asset.owner); + transfer::transfer(recipient_cap, recipient); + transfer::transfer(asset, proposal.id.to_address()); + + transfer::share_object(proposal); + } + + /// Strucure that encodes the logic required to transfer an `AuthenticatedAsset` + /// from one address to another. The transfer can be refused by the recipient. + public struct TransferProposal has key { + id: UID, + asset_id: ID, + sender_address: address, + sender_cap_id: ID, + recipient_address: address, + recipient_cap_id: ID, + done: bool, + } + + public struct SenderCap has key { + id: UID, + } + + public struct RecipientCap has key { + id: UID, + } + + /// Accept the transfer of the asset. + public fun accept( + self: &mut TransferProposal, + cap: RecipientCap, + asset: transfer::Receiving> + ) { + assert!(self.recipient_cap_id == object::id(&cap), EInvalidRecipient); + let mut asset = transfer::receive(&mut self.id, asset); + assert!(self.asset_id == object::id(&asset), EInvalidAsset); + + asset.owner = self.recipient_address; + transfer::transfer(asset, self.recipient_address); + cap.delete(); + + self.done = true; + } + + /// The sender of the asset consumes the `TransferProposal` to either + /// cancel it or to conclude it. + public fun conclude_or_cancel( + mut proposal: TransferProposal, + cap: SenderCap, + asset: transfer::Receiving>, + ) { + assert!(proposal.sender_cap_id == object::id(&cap), EInvalidSender); + if (!proposal.done) { + let asset = transfer::receive(&mut proposal.id, asset); + assert!(proposal.asset_id == object::id(&asset), EInvalidAsset); + transfer::transfer(asset, proposal.sender_address); + }; + + delete_transfer(proposal); + delete_sender_cap(cap); + } + + public(package) fun delete_sender_cap(cap: SenderCap) { + let SenderCap { + id, + } = cap; + object::delete(id); + } + + public fun delete_recipient_cap(cap: RecipientCap) { + let RecipientCap { + id, + } = cap; + object::delete(id); + } + + public(package) fun delete_transfer(self: TransferProposal) { + let TransferProposal { + id, + asset_id: _, + sender_cap_id: _, + recipient_cap_id: _, + sender_address: _, + recipient_address: _, + done: _, + } = self; + object::delete(id); + } +} + +#[test_only] +module iota_identity::asset_tests { + use iota_identity::asset::{Self, AuthenticatedAsset, EImmutable, ENonTransferable, ENonDeletable}; + use iota::test_scenario; + + const ALICE: address = @0x471c3; + const BOB: address = @0xb0b; + + #[test, expected_failure(abort_code = EImmutable)] + fun authenticated_asset_is_immutable_by_default() { + // Alice creates a new asset with default a configuration. + let mut scenario = test_scenario::begin(ALICE); + asset::new(42, scenario.ctx()); + scenario.next_tx(ALICE); + + // Alice fetches her newly created asset and attempts to modify it. + let mut asset = scenario.take_from_address>(ALICE); + *asset.borrow_mut() = 420; + + scenario.next_tx(ALICE); + scenario.return_to_sender(asset); + scenario.end(); + } + + #[test, expected_failure(abort_code = ENonTransferable)] + fun authenticated_asset_is_non_transferable_by_default() { + // Alice creates a new asset with default a configuration. + let mut scenario = test_scenario::begin(ALICE); + asset::new(42, scenario.ctx()); + scenario.next_tx(ALICE); + + // Alice fetches her newly created asset and attempts to send it to Bob. + let asset = scenario.take_from_address>(ALICE); + asset.transfer(BOB, scenario.ctx()); + + scenario.next_tx(ALICE); + scenario.end(); + } + + #[test, expected_failure(abort_code = ENonDeletable)] + fun authenticated_asset_is_non_deletable_by_default() { + // Alice creates a new asset with default a configuration. + let mut scenario = test_scenario::begin(ALICE); + asset::new(42, scenario.ctx()); + scenario.next_tx(ALICE); + + // Alice fetches her newly created asset and attempts to delete it. + let asset = scenario.take_from_address>(ALICE); + asset.delete(); + + scenario.next_tx(ALICE); + scenario.end(); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move new file mode 100644 index 0000000000..0e68188df4 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -0,0 +1,733 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::identity { + use iota::{ + transfer::Receiving, + vec_map::{Self, VecMap}, + clock::Clock, + }; + use iota_identity::{ + multicontroller::{Self, ControllerCap, Multicontroller, Action}, + update_value_proposal, + config_proposal, + transfer_proposal::{Self, Send}, + borrow_proposal::{Self, Borrow}, + did_deactivation_proposal::{Self, DidDeactivation}, + }; + + const ENotADidDocument: u64 = 0; + const EInvalidTimestamp: u64 = 1; + /// The threshold specified upon document creation was not valid. + /// Threshold must be greater than or equal to 1. + const EInvalidThreshold: u64 = 2; + /// The controller list must contain at least 1 element. + const EInvalidControllersList: u64 = 3; + + /// On-chain Identity. + public struct Identity has key, store { + id: UID, + /// Same as stardust `state_metadata`. + did_doc: Multicontroller>, + /// Timestamp of this Identity's creation. + created: u64, + /// Timestamp of this Identity's last update. + updated: u64, + } + + /// Creates a new DID Document with a single controller. + public fun new( + doc: vector, + clock: &Clock, + ctx: &mut TxContext + ): Identity { + new_with_controller(doc, ctx.sender(), clock, ctx) + } + + /// Creates an identity specifying its `created` timestamp. + /// Should only be used for migration! + public(package) fun new_with_creation_timestamp( + doc: vector, + creation_timestamp: u64, + clock: &Clock, + ctx: &mut TxContext + ): Identity { + let mut identity = new_with_controller(doc, ctx.sender(), clock, ctx); + assert!(identity.updated >= creation_timestamp, EInvalidTimestamp); + identity.created = creation_timestamp; + + identity + } + + /// Creates a new `Identity` wrapping DID DOC `doc` and controller by + /// a single address `controller`. + public fun new_with_controller( + doc: vector, + controller: address, + clock: &Clock, + ctx: &mut TxContext, + ): Identity { + let now = clock.timestamp_ms(); + Identity { + id: object::new(ctx), + did_doc: multicontroller::new_with_controller(doc, controller, ctx), + created: now, + updated: now, + } + } + + /// Creates a new DID Document controlled by multiple controllers. + /// The `weights` vectors is used to create a vector of `ControllerCap`s `controller_caps`, + /// where `controller_caps[i].weight = weights[i]` for all `i`s in `[0, weights.length())`. + public fun new_with_controllers( + doc: vector, + controllers: VecMap, + threshold: u64, + clock: &Clock, + ctx: &mut TxContext, + ): Identity { + assert!(is_did_output(&doc), ENotADidDocument); + assert!(threshold >= 1, EInvalidThreshold); + assert!(controllers.size() > 0, EInvalidControllersList); + + let now = clock.timestamp_ms(); + Identity { + id: object::new(ctx), + did_doc: multicontroller::new_with_controllers(doc, controllers, threshold, ctx), + created: now, + updated: now, + } + } + + /// Returns a reference to the `UID` of an `Identity`. + public fun id(self: &Identity): &UID { + &self.id + } + + /// Returns the unsigned amount of milliseconds + /// that passed from the UNIX epoch to the creation of this `Identity`. + public fun created(self: &Identity): u64 { + self.created + } + + /// Returns the unsigned amount of milliseconds + /// that passed from the UNIX epoch to the last update on this `Identity`. + public fun updated(self: &Identity): u64 { + self.updated + } + + /// Returns this `Identity`'s threshold. + public fun threshold(self: &Identity): u64 { + self.did_doc.threshold() + } + + /// Approve an `Identity`'s `Proposal`. + public fun approve_proposal( + self: &mut Identity, + cap: &ControllerCap, + proposal_id: ID, + ) { + self.did_doc.approve_proposal, T>(cap, proposal_id); + } + + /// Proposes the deativates the DID Document contained in this `Identity`. + /// This function can deactivate the DID Document right away if `cap` has + /// enough voting power. + public fun propose_deactivation( + self: &mut Identity, + cap: &ControllerCap, + expiration: Option, + clock: &Clock, + ctx: &mut TxContext, + ): Option { + let proposal_id = self.did_doc.create_proposal( + cap, + did_deactivation_proposal::new(), + expiration, + ctx, + ); + let is_approved = self + .did_doc + .is_proposal_approved<_, did_deactivation_proposal::DidDeactivation>(proposal_id); + if (is_approved) { + self.execute_deactivation(cap, proposal_id, clock, ctx); + option::none() + } else { + option::some(proposal_id) + } + } + + /// Executes a proposal to deactivate this `Identity`'s DID document. + public fun execute_deactivation( + self: &mut Identity, + cap: &ControllerCap, + proposal_id: ID, + clock: &Clock, + ctx: &mut TxContext, + ) { + let _ = self.did_doc.execute_proposal, DidDeactivation>( + cap, + proposal_id, + ctx, + ).unwrap(); + self.did_doc.set_controlled_value(vector[]); + self.updated = clock.timestamp_ms(); + } + + /// Proposes an update to the DID Document contained in this `Identity`. + /// This function can update the DID Document right away if `cap` has + /// enough voting power. + public fun propose_update( + self: &mut Identity, + cap: &ControllerCap, + updated_doc: vector, + expiration: Option, + clock: &Clock, + ctx: &mut TxContext, + ): Option { + assert!(is_did_output(&updated_doc), ENotADidDocument); + let proposal_id = update_value_proposal::propose_update( + &mut self.did_doc, + cap, + updated_doc, + expiration, + ctx, + ); + + let is_approved = self + .did_doc + .is_proposal_approved<_, update_value_proposal::UpdateValue>>(proposal_id); + if (is_approved) { + self.execute_update(cap, proposal_id, clock, ctx); + option::none() + } else { + option::some(proposal_id) + } + } + + /// Executes a proposal to update the DID Document contained in this `Identity`. + public fun execute_update( + self: &mut Identity, + cap: &ControllerCap, + proposal_id: ID, + clock: &Clock, + ctx: &mut TxContext, + ) { + update_value_proposal::execute_update( + &mut self.did_doc, + cap, + proposal_id, + ctx, + ); + + self.updated = clock.timestamp_ms(); + } + + /// Proposes to update this `Identity`'s AC. + /// This operation might be carried out right away if `cap` + /// has enough voting power. + public fun propose_config_change( + self: &mut Identity, + cap: &ControllerCap, + expiration: Option, + threshold: Option, + controllers_to_add: VecMap, + controllers_to_remove: vector, + controllers_to_update: VecMap, + ctx: &mut TxContext, + ): Option { + let proposal_id = config_proposal::propose_modify( + &mut self.did_doc, + cap, + expiration, + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + ctx + ); + + let is_approved = self + .did_doc + .is_proposal_approved<_, config_proposal::Modify>(proposal_id); + if (is_approved) { + self.execute_config_change(cap, proposal_id, ctx); + option::none() + } else { + option::some(proposal_id) + } + } + + /// Execute a proposal to change this `Identity`'s AC. + public fun execute_config_change( + self: &mut Identity, + cap: &ControllerCap, + proposal_id: ID, + ctx: &mut TxContext + ) { + config_proposal::execute_modify( + &mut self.did_doc, + cap, + proposal_id, + ctx, + ) + } + + /// Proposes the transfer of a set of objects owned by this `Identity`. + public fun propose_send( + self: &mut Identity, + cap: &ControllerCap, + expiration: Option, + objects: vector, + recipients: vector
, + ctx: &mut TxContext, + ) { + transfer_proposal::propose_send( + &mut self.did_doc, + cap, + expiration, + objects, + recipients, + ctx + ); + } + + /// Sends one object among the one specified in a `Send` proposal. + public fun execute_send( + self: &mut Identity, + send_action: &mut Action, + receiving: Receiving, + ) { + transfer_proposal::send(send_action, &mut self.id, receiving); + } + + /// Requests the borrowing of a set of assets + /// in order to use them in a transaction. Borrowed assets must be returned. + public fun propose_borrow( + self: &mut Identity, + cap: &ControllerCap, + expiration: Option, + objects: vector, + ctx: &mut TxContext, + ) { + let identity_address = self.id().to_address(); + borrow_proposal::propose_borrow( + &mut self.did_doc, + cap, + expiration, + objects, + identity_address, + ctx, + ); + } + + /// Takes one of the borrowed assets. + public fun execute_borrow( + self: &mut Identity, + borrow_action: &mut Action, + receiving: Receiving, + ): T { + borrow_proposal::borrow(borrow_action, &mut self.id, receiving) + } + + /// Simplified version of `Identity::propose_config_change` that allows + /// to add a new controller. + public fun propose_new_controller( + self: &mut Identity, + cap: &ControllerCap, + expiration: Option, + new_controller_addr: address, + voting_power: u64, + ctx: &mut TxContext, + ): Option { + let mut new_controllers = vec_map::empty(); + new_controllers.insert(new_controller_addr, voting_power); + + self.propose_config_change(cap, expiration, option::none(), new_controllers, vector[], vec_map::empty(), ctx) + } + + /// Executes an `Identity`'s proposal. + public fun execute_proposal( + self: &mut Identity, + cap: &ControllerCap, + proposal_id: ID, + ctx: &mut TxContext, + ): Action { + self.did_doc.execute_proposal(cap, proposal_id, ctx) + } + + /// Checks if `data` is a state matadata representing a DID. + /// i.e. starts with the bytes b"DID". + public(package) fun is_did_output(data: &vector): bool { + data[0] == 0x44 && // b'D' + data[1] == 0x49 && // b'I' + data[2] == 0x44 // b'D' + } + + public(package) fun did_doc(self: &Identity): &Multicontroller> { + &self.did_doc + } + + #[test_only] + public(package) fun to_address(self: &Identity): address { + self.id().to_inner().id_to_address() + } +} + + +#[test_only] +module iota_identity::identity_tests { + use iota::test_scenario; + use iota_identity::identity::{new, ENotADidDocument, Identity, new_with_controllers}; + use iota_identity::config_proposal::Modify; + use iota_identity::multicontroller::{ControllerCap, EExpiredProposal, EThresholdNotReached}; + use iota::vec_map; + use iota::clock; + + #[test] + fun adding_a_controller_works() { + let controller1 = @0x1; + let controller2 = @0x2; + let mut scenario = test_scenario::begin(controller1); + let clock = clock::create_for_testing(scenario.ctx()); + + + // Create a DID document with no funds and 1 controller with a weight of 1 and a threshold of 1. + // Share the document and send the controller capability to `controller1`. + let identity = new(b"DID", &clock, scenario.ctx()); + transfer::public_share_object(identity); + + scenario.next_tx(controller1); + + // Create a request to add a second controller. + let mut identity = scenario.take_shared(); + let controller1_cap = scenario.take_from_address(controller1); + // This is carried out immediately. + identity.propose_new_controller(&controller1_cap, option::none(), controller2, 1, scenario.ctx()); + + scenario.next_tx(controller2); + + let controller2_cap = scenario.take_from_address(controller2); + + identity.did_doc().assert_is_member(&controller2_cap); + + // Cleanup + test_scenario::return_to_address(controller1, controller1_cap); + test_scenario::return_to_address(controller2, controller2_cap); + test_scenario::return_shared(identity); + + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test] + fun removing_a_controller_works() { + let controller1 = @0x1; + let controller2 = @0x2; + let controller3 = @0x3; + let mut scenario = test_scenario::begin(controller1); + let clock = clock::create_for_testing(scenario.ctx()); + + let mut controllers = vec_map::empty(); + controllers.insert(controller1, 1); + controllers.insert(controller2, 1); + controllers.insert(controller3, 1); + + // Create an identity shared by `controller1`, `controller2`, `controller3`. + let identity = new_with_controllers( + b"DID", + controllers, + 2, + &clock, + scenario.ctx(), + ); + transfer::public_share_object(identity); + + scenario.next_tx(controller1); + + // `controller1` creates a request to remove `controller3`. + let mut identity = scenario.take_shared(); + let controller1_cap = scenario.take_from_address(controller1); + let controller3_cap = scenario.take_from_address(controller3); + + let proposal_id = identity.propose_config_change( + &controller1_cap, + option::none(), + option::none(), + vec_map::empty(), + vector[controller3_cap.id().to_inner()], + vec_map::empty(), + scenario.ctx() + ).destroy_some(); + + scenario.next_tx(controller2); + + // `controller2` also approves the removal of `controller3`. + let controller2_cap = scenario.take_from_address(controller2); + identity.approve_proposal(&controller2_cap, proposal_id); + + scenario.next_tx(controller2); + + // `controller3` is removed. + identity.execute_config_change(&controller2_cap, proposal_id, scenario.ctx()); + assert!(!identity.did_doc().controllers().contains(&controller3_cap.id().to_inner()), 0); + + // cleanup. + test_scenario::return_to_address(controller1, controller1_cap); + test_scenario::return_to_address(controller2, controller2_cap); + test_scenario::return_to_address(controller3, controller3_cap); + test_scenario::return_shared(identity); + + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test, expected_failure(abort_code = EThresholdNotReached)] + fun test_controller_addition_fails_when_threshold_not_met() { + let controller_a = @0x1; + let controller_b = @0x2; + let controller_c = @0x3; + + // The controller that is not part of the ACL. + let controller_d = @0x4; + + let mut scenario = test_scenario::begin(controller_a); + let clock = clock::create_for_testing(scenario.ctx()); + + let mut controllers = vec_map::empty(); + controllers.insert(controller_a, 10); + controllers.insert(controller_b, 5); + controllers.insert(controller_c, 5); + + // === First transaction === + // Controller A can execute config changes + { + let identity = new_with_controllers( + b"DID", + controllers, + 10, + &clock, + scenario.ctx(), + ); + transfer::public_share_object(identity); + scenario.next_tx(controller_a); + + // Controller A alone should be able to do anything. + let mut identity = scenario.take_shared(); + let controller_a_cap = scenario.take_from_address(controller_a); + + // Create a request to add a new controller. This is carried out immediately as controller_a has enough voting power + identity.propose_new_controller(&controller_a_cap, option::none(), controller_d, 1, scenario.ctx()); + + scenario.next_tx(controller_d); + + let controller_d_cap = scenario.take_from_address(controller_d); + + identity.did_doc().assert_is_member(&controller_d_cap); + + test_scenario::return_shared(identity); + test_scenario::return_to_address(controller_a, controller_a_cap); + test_scenario::return_to_address(controller_d, controller_d_cap); + }; + + + // Controller B alone should not be able to make changes. + { + let identity = new_with_controllers( + b"DID", + controllers, + 10, + &clock, + scenario.ctx(), + ); + transfer::public_share_object(identity); + scenario.next_tx(controller_a); + + let mut identity = scenario.take_shared(); + let controller_b_cap = scenario.take_from_address(controller_b); + + let proposal_id = identity.propose_new_controller(&controller_b_cap, option::none(), controller_d, 1, scenario.ctx()).destroy_some(); + + scenario.next_tx(controller_b); + identity.execute_config_change(&controller_b_cap, proposal_id, scenario.ctx()); + scenario.next_tx(controller_d); + + let controller_d_cap = scenario.take_from_address(controller_d); + assert!(!identity.did_doc().controllers().contains(&controller_d_cap.id().to_inner()), 0); + + test_scenario::return_to_address(controller_b, controller_b_cap); + test_scenario::return_to_address(controller_d, controller_d_cap); + test_scenario::return_shared(identity); + }; + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test] + fun test_controller_addition_works_when_threshold_met() { + let controller_a = @0x1; + let controller_b = @0x2; + let controller_c = @0x3; + + // The controller that is not part of the ACL. + let controller_d = @0x4; + + let mut scenario = test_scenario::begin(controller_b); + let clock = clock::create_for_testing(scenario.ctx()); + + let mut controllers = vec_map::empty(); + controllers.insert(controller_a, 10); + controllers.insert(controller_b, 5); + controllers.insert(controller_c, 5); + + // === First transaction === + // Controller B & C can execute config changes + let identity = new_with_controllers( + b"DID", + controllers, + 10, + &clock, + scenario.ctx(), + ); + transfer::public_share_object(identity); + scenario.next_tx(controller_b); + + let mut identity = scenario.take_shared(); + let controller_b_cap = scenario.take_from_address(controller_b); + + // Create a request to add a new controller. + let proposal_id = identity.propose_new_controller(&controller_b_cap, option::none(), controller_d, 10, scenario.ctx()).destroy_some(); + + scenario.next_tx(controller_b); + let controller_c_cap = scenario.take_from_address(controller_c); + identity.approve_proposal(&controller_c_cap, proposal_id); + + scenario.next_tx(controller_a); + identity.execute_config_change(&controller_c_cap, proposal_id, scenario.ctx()); + + scenario.next_tx(controller_d); + + let controller_d_cap = scenario.take_from_address(controller_d); + + identity.did_doc().assert_is_member(&controller_d_cap); + + test_scenario::return_shared(identity); + test_scenario::return_to_address(controller_b, controller_b_cap); + test_scenario::return_to_address(controller_c, controller_c_cap); + test_scenario::return_to_address(controller_d, controller_d_cap); + + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test] + fun check_identity_can_own_another_identity() { + let controller_a = @0x1; + let mut scenario = test_scenario::begin(controller_a); + let clock = clock::create_for_testing(scenario.ctx()); + + let first_identity = new(b"DID", &clock, scenario.ctx()); + transfer::public_share_object(first_identity); + + scenario.next_tx(controller_a); + let first_identity = scenario.take_shared(); + + let mut controllers = vec_map::empty(); + controllers.insert(first_identity.to_address(), 10); + + // Create a second identity. + let second_identity = new_with_controllers( + b"DID", + controllers, + 10, + &clock, + scenario.ctx(), + ); + + transfer::public_share_object(second_identity); + + scenario.next_tx(first_identity.to_address()); + let first_identity_cap = scenario.take_from_address(first_identity.to_address()); + + let mut second_identity = scenario.take_shared(); + + assert!(second_identity.did_doc().controllers().contains(&first_identity_cap.id().to_inner()), 0); + + second_identity.propose_new_controller(&first_identity_cap, option::none(), controller_a, 10, scenario.ctx()).destroy_none(); + + scenario.next_tx(controller_a); + let controller_a_cap = scenario.take_from_address(controller_a); + + second_identity.did_doc().assert_is_member(&controller_a_cap); + + test_scenario::return_shared(second_identity); + test_scenario::return_to_address(controller_a, controller_a_cap); + test_scenario::return_to_address(first_identity.to_address(), first_identity_cap); + test_scenario::return_shared(first_identity); + + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test, expected_failure(abort_code = ENotADidDocument)] + fun test_update_proposal_cannot_propose_non_did_doc() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + let clock = clock::create_for_testing(scenario.ctx()); + + let identity = new(b"DID", &clock, scenario.ctx()); + transfer::public_share_object(identity); + + scenario.next_tx(controller); + + // Propose a change for updating the did document + let mut identity = scenario.take_shared(); + let cap = scenario.take_from_address(controller); + + let _proposal_id = identity.propose_update(&cap, b"NOT DID", option::none(), &clock, scenario.ctx()); + + test_scenario::return_to_address(controller, cap); + test_scenario::return_shared(identity); + + scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test, expected_failure(abort_code = EExpiredProposal)] + fun expired_proposals_cannot_be_executed() { + let controller_a = @0x1; + let controller_b = @0x2; + let new_controller = @0x3; + let mut scenario = test_scenario::begin(controller_a); + let expiration_epoch = scenario.ctx().epoch(); + let clock = clock::create_for_testing(scenario.ctx()); + + let mut controllers = vec_map::empty(); + controllers.insert(controller_a, 1); + controllers.insert(controller_b, 1); + + let identity = new_with_controllers(b"DID", controllers, 2, &clock, scenario.ctx()); + transfer::public_share_object(identity); + + scenario.next_tx(controller_a); + + let mut identity = scenario.take_shared(); + let cap = scenario.take_from_address(controller_a); + let proposal_id = identity.propose_new_controller(&cap, option::some(expiration_epoch), new_controller, 1, scenario.ctx()).destroy_some(); + + scenario.next_tx(controller_b); + let cap_b = scenario.take_from_address(controller_b); + identity.approve_proposal(&cap_b, proposal_id); + + scenario.later_epoch(100, controller_a); + // this should fail! + identity.execute_config_change(&cap, proposal_id, scenario.ctx()); + + test_scenario::return_to_address(controller_a, cap); + test_scenario::return_to_address(controller_b, cap_b); + test_scenario::return_shared(identity); + + scenario.end(); + clock::destroy_for_testing(clock); + } +} diff --git a/identity_iota_core/packages/iota_identity/sources/migration.move b/identity_iota_core/packages/iota_identity/sources/migration.move new file mode 100644 index 0000000000..566f41406c --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/migration.move @@ -0,0 +1,136 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::migration { + use iota_identity::{migration_registry::MigrationRegistry, identity}; + use stardust::{alias::Alias, alias_output::AliasOutput}; + use iota::{coin, iota::IOTA, clock::Clock}; + + const ENotADidOutput: u64 = 1; + + public fun migrate_alias( + alias: Alias, + migration_registry: &mut MigrationRegistry, + creation_timestamp: u64, + clock: &Clock, + ctx: &mut TxContext, + ): address { + // Extract needed data from `alias`. + let alias_id = object::id(&alias); + let mut state_metadata = *alias.state_metadata(); + // `alias` is not needed anymore, destroy it. + alias.destroy(); + + // Check if `state_metadata` contains a DID document. + assert!(state_metadata.is_some() && identity::is_did_output(state_metadata.borrow()), ENotADidOutput); + + let identity = identity::new_with_creation_timestamp( + state_metadata.extract(), + creation_timestamp, + clock, + ctx + ); + let identity_addr = identity.id().to_address(); + + // Add a migration record. + migration_registry.add(alias_id, identity.id().to_inner()); + transfer::public_share_object(identity); + + identity_addr + } + + /// Creates a new `Identity` from an Iota 1.0 legacy `AliasOutput` containing a DID Document. + public fun migrate_alias_output( + alias_output: AliasOutput, + migration_registry: &mut MigrationRegistry, + creation_timestamp: u64, + clock: &Clock, + ctx: &mut TxContext + ) { + // Extract required data from output. + let (iota, native_tokens, alias_data) = alias_output.extract_assets(); + + let identity_addr = migrate_alias( + alias_data, + migration_registry, + creation_timestamp, + clock, + ctx + ); + + let coin = coin::from_balance(iota, ctx); + transfer::public_transfer(coin, identity_addr); + transfer::public_transfer(native_tokens, identity_addr); + } +} + + +#[test_only] +module iota_identity::migration_tests { + use iota::{test_scenario, balance, bag, iota::IOTA, clock}; + use stardust::alias_output::{Self, AliasOutput}; + use iota_identity::identity::{Identity}; + use iota_identity::migration::migrate_alias_output; + use stardust::alias::{Self, Alias}; + use iota_identity::migration_registry::{MigrationRegistry, init_testing}; + use iota_identity::multicontroller::ControllerCap; + + fun create_did_alias(ctx: &mut TxContext): Alias { + let sender = ctx.sender(); + alias::create_for_testing( + sender, + 1, + option::some(b"DID"), + option::some(sender), + option::none(), + option::none(), + option::none(), + ctx + ) + } + + fun create_empty_did_output(ctx: &mut TxContext): (AliasOutput, ID) { + let mut alias_output = alias_output::create_for_testing(balance::zero(), bag::new(ctx), ctx); + let alias = create_did_alias(ctx); + let alias_id = object::id(&alias); + alias_output.attach_alias(alias); + + (alias_output, alias_id) + } + + #[test] + fun test_migration_of_legacy_did_output() { + let controller_a = @0x1; + let mut scenario = test_scenario::begin(controller_a); + let clock = clock::create_for_testing(scenario.ctx()); + + let (did_output, alias_id) = create_empty_did_output(scenario.ctx()); + + init_testing(scenario.ctx()); + + scenario.next_tx(controller_a); + let mut registry = scenario.take_shared(); + + migrate_alias_output(did_output, &mut registry, clock.timestamp_ms(), &clock, scenario.ctx()); + + scenario.next_tx(controller_a); + let identity = scenario.take_shared(); + let controller_a_cap = scenario.take_from_address(controller_a); + + // Assert correct binding in migration regitry + assert!(registry.lookup(alias_id) == identity.id().to_inner(), 0); + + // Assert the sender is controller + identity.did_doc().assert_is_member(&controller_a_cap); + + // assert the metadata is b"DID" + let did = identity.did_doc().value(); + assert!(did == b"DID", 0); + + test_scenario::return_to_address(controller_a, controller_a_cap); + test_scenario::return_shared(registry); + test_scenario::return_shared(identity); + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } +} diff --git a/identity_iota_core/packages/iota_identity/sources/migration_registry.move b/identity_iota_core/packages/iota_identity/sources/migration_registry.move new file mode 100644 index 0000000000..25161bd9c2 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/migration_registry.move @@ -0,0 +1,54 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::migration_registry { + use iota::{dynamic_field as field, transfer::share_object, event}; + + const BEACON_BYTES: vector = b"identity.rs_pkg"; + + /// One time witness needed to construct a singleton `MigrationRegistry`. + public struct MIGRATION_REGISTRY has drop {} + + /// Event type that is fired upon creation of a `MigrationRegistry`. + public struct MigrationRegistryCreated has copy, drop { + id: ID, + beacon: vector, + } + + /// Object that tracks migrated alias outputs to their corresponding object IDs. + public struct MigrationRegistry has key { + id: UID, + } + + /// Creates a singleton instance of `MigrationRegistry` when publishing this package. + fun init(_otw: MIGRATION_REGISTRY, ctx: &mut TxContext) { + let id = object::new(ctx); + let registry_id = id.to_inner(); + let registry = MigrationRegistry { + id, + }; + share_object(registry); + // Signal the creation of a migration registry. + event::emit(MigrationRegistryCreated { id: registry_id, beacon: BEACON_BYTES }); + } + + /// Checks whether the given alias ID exists in the migration registry. + public fun exists(self: &MigrationRegistry, alias_id: ID): bool { + field::exists_(&self.id, alias_id) + } + + /// Lookup an alias ID into the migration registry. + public fun lookup(self: &MigrationRegistry, alias_id: ID): ID { + *field::borrow(&self.id, alias_id) + } + + /// Adds a new Alias ID -> Object ID binding to the regitry. + public(package) fun add(self: &mut MigrationRegistry, alias_id: ID, identity_id: ID) { + field::add(&mut self.id, alias_id, identity_id); + } + + #[test_only] + public fun init_testing(ctx: &mut TxContext) { + init(MIGRATION_REGISTRY {}, ctx); + } +} diff --git a/identity_iota_core/packages/iota_identity/sources/multicontroller.move b/identity_iota_core/packages/iota_identity/sources/multicontroller.move new file mode 100644 index 0000000000..b8ae07f7ed --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/multicontroller.move @@ -0,0 +1,304 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::multicontroller { + use iota::{object_bag::{Self, ObjectBag}, vec_map::{Self, VecMap}, vec_set::{Self, VecSet}}; + + const EInvalidController: u64 = 0; + const EControllerAlreadyVoted: u64 = 1; + const EThresholdNotReached: u64 = 2; + const EInvalidThreshold: u64 = 3; + const EExpiredProposal: u64 = 4; + const ENotVotedYet: u64 = 5; + const EProposalNotFound: u64 = 6; + + /// Capability that allows to access mutative APIs of a `Multicontroller`. + public struct ControllerCap has key { + id: UID, + } + + public fun id(self: &ControllerCap): &UID { + &self.id + } + + /// Shares control of a value `V` with multiple entities called controllers. + public struct Multicontroller has store { + threshold: u64, + controllers: VecMap, + controlled_value: V, + active_proposals: vector, + proposals: ObjectBag, + } + + /// Wraps a `V` in `Multicontroller`, making the tx's sender a controller with + /// voting power 1. + public fun new(controlled_value: V, ctx: &mut TxContext): Multicontroller { + new_with_controller(controlled_value, ctx.sender(), ctx) + } + + /// Wraps a `V` in `Multicontroller` and sends `controller` a `ControllerCap`. + public fun new_with_controller( + controlled_value: V, + controller: address, + ctx: &mut TxContext + ): Multicontroller { + let mut controllers = vec_map::empty(); + controllers.insert(controller, 1); + + new_with_controllers(controlled_value, controllers, 1, ctx) + } + + /// Wraps a `V` in `Multicontroller`, settings `threshold` as the threshold, + /// and using `controllers` to set controllers: i.e. each `(recipient, voting power)` + /// in `controllers` results in `recipient` obtaining a `ControllerCap` with the + /// specified voting power. + public fun new_with_controllers( + controlled_value: V, + controllers: VecMap, + threshold: u64, + ctx: &mut TxContext, + ): Multicontroller { + let (mut addrs, mut vps) = controllers.into_keys_values(); + let mut controllers = vec_map::empty(); + while(!addrs.is_empty()) { + let addr = addrs.pop_back(); + let vp = vps.pop_back(); + + let cap = ControllerCap { id: object::new(ctx) }; + controllers.insert(cap.id.to_inner(), vp); + + transfer::transfer(cap, addr); + }; + + let mut multi = Multicontroller { + controlled_value, + controllers, + threshold, + active_proposals: vector[], + proposals: object_bag::new(ctx), + }; + multi.set_threshold(threshold); + + multi + } + + /// Structure that encapsulates the logic required to make changes + /// to a multicontrolled value. + public struct Proposal has key, store { + id: UID, + votes: u64, + voters: VecSet, + expiration_epoch: Option, + action: T, + } + + /// Returns `true` if `Proposal` `self` is expired. + public fun is_expired(self: &Proposal, ctx: &mut TxContext): bool { + if (self.expiration_epoch.is_some()) { + let expiration = *self.expiration_epoch.borrow(); + expiration < ctx.epoch() + } else { + false + } + } + + /// Strucure that encapsulate the kind of change that will be performed + /// when a proposal is carried out. + public struct Action { + inner: T, + } + + /// Consumes `Action` returning the inner value. + public fun unwrap(action: Action): T { + let Action { inner } = action; + inner + } + + /// Borrows the content of `action`. + public fun borrow(action: &Action): &T { + &action.inner + } + + /// Mutably borrows the content of `action`. + public fun borrow_mut(action: &mut Action): &mut T { + &mut action.inner + } + + public(package) fun assert_is_member(multi: &Multicontroller, cap: &ControllerCap) { + assert!(multi.controllers.contains(&cap.id.to_inner()), EInvalidController); + } + + /// Creates a new proposal for `Multicontroller` `multi`. + public fun create_proposal( + multi: &mut Multicontroller, + cap: &ControllerCap, + action: T, + expiration_epoch: Option, + ctx: &mut TxContext, + ): ID { + multi.assert_is_member(cap); + let cap_id = cap.id.to_inner(); + let voting_power = multi.voting_power(cap_id); + + let proposal = Proposal { + id: object::new(ctx), + votes: voting_power, + voters: vec_set::singleton(cap.id.to_inner()), + expiration_epoch, + action, + }; + + let proposal_id = object::id(&proposal); + multi.proposals.add(proposal_id, proposal); + multi.active_proposals.push_back(proposal_id); + proposal_id + } + + /// Approves an active `Proposal` in `multi`. + public fun approve_proposal( + multi: &mut Multicontroller, + cap: &ControllerCap, + proposal_id: ID, + ) { + multi.assert_is_member(cap); + let cap_id = cap.id.to_inner(); + let voting_power = multi.voting_power(cap_id); + + let proposal = multi.proposals.borrow_mut>(proposal_id); + assert!(!proposal.voters.contains(&cap_id), EControllerAlreadyVoted); + + proposal.votes = proposal.votes + voting_power; + proposal.voters.insert(cap_id); + } + + /// Consumes the `multi`'s active `Proposal` with id `proposal_id`, + /// returning its inner `Action`. + /// This call fails if `multi`'s threshold has not been reached. + public fun execute_proposal( + multi: &mut Multicontroller, + cap: &ControllerCap, + proposal_id: ID, + ctx: &mut TxContext, + ): Action { + multi.assert_is_member(cap); + + let proposal = multi.proposals.remove>(proposal_id); + assert!(proposal.votes >= multi.threshold, EThresholdNotReached); + assert!(!proposal.is_expired(ctx), EExpiredProposal); + + let Proposal { + id, + votes: _, + voters: _, + expiration_epoch: _, + action: inner, + } = proposal; + + id.delete(); + + let (present, i) = multi.active_proposals.index_of(&proposal_id); + assert!(present, EProposalNotFound); + + multi.active_proposals.remove(i); + + Action { inner } + } + + /// Removes the approval given by the controller owning `cap` on `Proposal` + /// `proposal_id`. + public fun remove_approval( + multi: &mut Multicontroller, + cap: &ControllerCap, + proposal_id: ID, + ) { + let cap_id = cap.id.to_inner(); + let vp = multi.voting_power(cap_id); + + let proposal = multi.proposals.borrow_mut>(proposal_id); + assert!(proposal.voters.contains(&cap_id), ENotVotedYet); + + proposal.voters.remove(&cap_id); + proposal.votes = proposal.votes - vp; + } + + /// Returns a reference to `multi`'s value. + public fun value(multi: &Multicontroller): &V { + &multi.controlled_value + } + + /// Returns the list of `multi`'s controllers - i.e. the `ID` of its `ControllerCap`s. + public fun controllers(multi: &Multicontroller): vector { + multi.controllers.keys() + } + + /// Returns `multi`'s threshold. + public fun threshold(multi: &Multicontroller): u64 { + multi.threshold + } + + /// Returns the voting power of a given controller, identified by its `ID`. + public fun voting_power(multi: &Multicontroller, controller_id: ID): u64 { + *multi.controllers.get(&controller_id) + } + + public(package) fun set_voting_power(multi: &mut Multicontroller, controller_id: ID, vp: u64) { + assert!(multi.controllers().contains(&controller_id), EInvalidController); + *multi.controllers.get_mut(&controller_id) = vp; + } + + /// Returns the sum of all controllers voting powers. + public fun max_votes(multi: &Multicontroller): u64 { + let (_, mut values) = multi.controllers.into_keys_values(); + let mut sum = 0; + while (!values.is_empty()) { + sum = sum + values.pop_back(); + }; + + sum + } + + public(package) fun unpack_action(action: Action): T { + let Action { inner } = action; + inner + } + + public(package) fun is_proposal_approved(multi: &Multicontroller, proposal_id: ID): bool { + let proposal = multi.proposals.borrow>(proposal_id); + proposal.votes >= multi.threshold + } + + public(package) fun add_members(multi: &mut Multicontroller, to_add: VecMap, ctx: &mut TxContext) { + let mut i = 0; + while (i < to_add.size()) { + let (addr, vp) = to_add.get_entry_by_idx(i); + let new_cap = ControllerCap { id: object::new(ctx) }; + multi.controllers.insert(new_cap.id.to_inner(), *vp); + transfer::transfer(new_cap, *addr); + i = i + 1; + } + } + + public(package) fun remove_members(multi: &mut Multicontroller, mut to_remove: vector) { + while (!to_remove.is_empty()) { + let id = to_remove.pop_back(); + multi.controllers.remove(&id); + } + } + + public(package) fun update_members(multi: &mut Multicontroller, mut to_update: VecMap) { + while (!to_update.is_empty()) { + let (controller, vp) = to_update.pop(); + + multi.set_voting_power(controller, vp); + } + } + + public(package) fun set_threshold(multi: &mut Multicontroller, threshold: u64) { + assert!(threshold <= multi.max_votes(), EInvalidThreshold); + multi.threshold = threshold; + } + + public(package) fun set_controlled_value(multi: &mut Multicontroller, controlled_value: V) { + multi.controlled_value = controlled_value; + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move new file mode 100644 index 0000000000..4195e34615 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move @@ -0,0 +1,74 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::borrow_proposal { + use iota_identity::{multicontroller::{Multicontroller, Action, ControllerCap}}; + use iota::transfer::Receiving; + + const EInvalidObject: u64 = 0; + const EInvalidOwner: u64 = 1; + const EUnreturnedObjects: u64 = 2; + + /// Action used to "borrow" assets in a transaction - enforcing their return. + public struct Borrow has store { + objects: vector, + objects_to_return: vector, + owner: address, + } + + /// Propose the borrowing of a set of assets owned by this multicontroller. + public fun propose_borrow( + multi: &mut Multicontroller, + cap: &ControllerCap, + expiration: Option, + objects: vector, + owner: address, + ctx: &mut TxContext, + ) { + let action = Borrow { objects, objects_to_return: vector::empty(), owner }; + + multi.create_proposal(cap, action,expiration, ctx); + } + + /// Borrows an asset from this action. This function will fail if: + /// - the received object is not among `Borrow::objects`; + /// - controllee does not have the same address as `Borrow::owner`; + public fun borrow( + action: &mut Action, + controllee: &mut UID, + receiving: Receiving, + ): T { + let borrow_action = action.borrow_mut(); + assert!(borrow_action.owner == controllee.to_address(), EInvalidOwner); + let receiving_object_id = receiving.receiving_object_id(); + let (obj_exists, obj_idx) = borrow_action.objects.index_of(&receiving_object_id); + assert!(obj_exists, EInvalidObject); + + borrow_action.objects.swap_remove(obj_idx); + borrow_action.objects_to_return.push_back(receiving_object_id); + + transfer::public_receive(controllee, receiving) + } + + /// Transfer a borrowed object back to its original owner. + public fun put_back( + action: &mut Action, + obj: T, + ) { + let borrow_action = action.borrow_mut(); + let object_id = object::id(&obj); + let (contains, obj_idx) = borrow_action.objects_to_return.index_of(&object_id); + assert!(contains, EInvalidObject); + + borrow_action.objects_to_return.swap_remove(obj_idx); + transfer::public_transfer(obj, borrow_action.owner); + } + + /// Consumes a borrow action. + public fun conclude_borrow( + action: Action + ) { + let Borrow { objects: _, objects_to_return, owner: _ } = action.unpack_action(); + assert!(objects_to_return.is_empty(), EUnreturnedObjects); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/config.move b/identity_iota_core/packages/iota_identity/sources/proposals/config.move new file mode 100644 index 0000000000..80755b2afc --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/config.move @@ -0,0 +1,107 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::config_proposal { + use iota_identity::multicontroller::{ControllerCap, Multicontroller}; + use iota::vec_map::VecMap; + + const ENotMember: u64 = 0; + const EInvalidThreshold: u64 = 1; + + public struct Modify has store { + threshold: Option, + controllers_to_add: VecMap, + controllers_to_remove: vector, + controllers_to_update: VecMap, + } + + public fun propose_modify( + multi: &mut Multicontroller, + cap: &ControllerCap, + expiration: Option, + mut threshold: Option, + controllers_to_add: VecMap, + controllers_to_remove: vector, + controllers_to_update: VecMap, + ctx: &mut TxContext, + ): ID { + let mut max_votes = 0; + let (mut cs, mut vps) = controllers_to_update.into_keys_values(); + while (!cs.is_empty()) { + let c = cs.pop_back(); + let vp = vps.pop_back(); + assert!(multi.controllers().contains(&c), ENotMember); + max_votes = max_votes + vp; + }; + let (_, mut voting_powers) = controllers_to_add.into_keys_values(); + let mut voting_power_increase = 0; + while (!voting_powers.is_empty()) { + let voting_power = voting_powers.pop_back(); + + voting_power_increase = voting_power_increase + voting_power; + }; + voting_powers.destroy_empty(); + + let mut i = 0; + let mut voting_power_decrease = 0; + while (i < controllers_to_remove.length()) { + let controller_id = controllers_to_remove[i]; + assert!(multi.controllers().contains(&controller_id), ENotMember); + let mut vp = multi.voting_power(controller_id); + if (controllers_to_update.contains(&controller_id)) { + vp = *controllers_to_update.get(&controller_id); + }; + voting_power_decrease = voting_power_decrease + vp; + i = i + 1; + }; + + let mut i = 0; + while (i < multi.controllers().length()) { + let controller_id = multi.controllers()[i]; + if (!controllers_to_update.contains(&controller_id)) { + max_votes = max_votes + multi.voting_power(controller_id); + }; + i = i + 1; + }; + + let new_max_votes = max_votes + voting_power_increase - voting_power_decrease; + + let threshold = if (threshold.is_some()) { + let threshold = threshold.extract(); + threshold + } else { + multi.threshold() + }; + + assert!(threshold > 0 && threshold <= new_max_votes, EInvalidThreshold); + + let action = Modify { + threshold: option::some(threshold), + controllers_to_add, + controllers_to_remove, + controllers_to_update, + }; + + multi.create_proposal(cap, action, expiration, ctx) + } + + public fun execute_modify( + multi: &mut Multicontroller, + cap: &ControllerCap, + proposal_id: ID, + ctx: &mut TxContext, + ) { + let action = multi.execute_proposal(cap, proposal_id, ctx); + let Modify { + mut threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update + } = action.unpack_action(); + + if (threshold.is_some()) multi.set_threshold(threshold.extract()); + multi.update_members(controllers_to_update); + multi.add_members(controllers_to_add, ctx); + multi.remove_members(controllers_to_remove); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/did_deactivation.move b/identity_iota_core/packages/iota_identity/sources/proposals/did_deactivation.move new file mode 100644 index 0000000000..9bce989fad --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/did_deactivation.move @@ -0,0 +1,10 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::did_deactivation_proposal { + public struct DidDeactivation has store, copy, drop {} + + public fun new(): DidDeactivation { + DidDeactivation {} + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move new file mode 100644 index 0000000000..d16f8c36c1 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move @@ -0,0 +1,58 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::transfer_proposal { + use iota_identity::{multicontroller::{Multicontroller, Action, ControllerCap}}; + use iota::transfer::Receiving; + + const EDifferentLength: u64 = 0; + const EUnsentAssets: u64 = 1; + const EInvalidObject: u64 = 2; + + public struct Send has store { + objects: vector, + recipients: vector
, + } + + public fun propose_send( + multi: &mut Multicontroller, + cap: &ControllerCap, + expiration: Option, + objects: vector, + recipients: vector
, + ctx: &mut TxContext, + ) { + assert!(objects.length() == recipients.length(), EDifferentLength); + let action = Send { objects, recipients }; + + multi.create_proposal(cap, action,expiration, ctx); + } + + public fun send( + action: &mut Action, + controllee: &mut UID, + received: Receiving, + ) { + let send_action = action.borrow_mut(); + let object_id = received.receiving_object_id(); + let (object_exists, object_idx) = send_action.objects.index_of(&object_id); + // Check that the received object is among the objects that are actually supposed to be sent. + assert!(object_exists, EInvalidObject); + + let object = transfer::public_receive(controllee, received); + // Get the corresponding recipient. + let recipient = send_action.recipients.swap_remove(object_idx); + + transfer::public_transfer(object, recipient); + // Update the list of objects that have not been sent yet. + send_action.objects.swap_remove(object_idx); + } + + public fun complete_send(action: Action) { + let Send { objects, recipients } = action.unpack_action(); + assert!(recipients.is_empty() && objects.is_empty(), EUnsentAssets); + + recipients.destroy_empty(); + objects.destroy_empty(); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/value.move b/identity_iota_core/packages/iota_identity/sources/proposals/value.move new file mode 100644 index 0000000000..468971af60 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/value.move @@ -0,0 +1,33 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::update_value_proposal { + use iota_identity::multicontroller::{Multicontroller, ControllerCap}; + + public struct UpdateValue has store { + new_value: V, + } + + public fun propose_update( + multi: &mut Multicontroller, + cap: &ControllerCap, + new_value: V, + expiration: Option, + ctx: &mut TxContext, + ): ID { + let update_action = UpdateValue { new_value }; + multi.create_proposal(cap, update_action, expiration, ctx) + } + + public fun execute_update( + multi: &mut Multicontroller, + cap: &ControllerCap, + proposal_id: ID, + ctx: &mut TxContext, + ) { + let action = multi.execute_proposal(cap, proposal_id, ctx); + let UpdateValue { new_value } = action.unpack_action(); + + multi.set_controlled_value(new_value) + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/public_vc.move b/identity_iota_core/packages/iota_identity/sources/public_vc.move new file mode 100644 index 0000000000..ffe6138bbf --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/public_vc.move @@ -0,0 +1,20 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::public_vc { + public struct PublicVc has store { + data: vector, + } + + public fun new(data: vector): PublicVc { + PublicVc { data } + } + + public fun data(self: &PublicVc): &vector { + &self.data + } + + public fun set_data(self: &mut PublicVc, data: vector) { + self.data = data + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/utils.move b/identity_iota_core/packages/iota_identity/sources/utils.move new file mode 100644 index 0000000000..a7718a44a4 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/utils.move @@ -0,0 +1,35 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::utils { + use iota::vec_map::{Self, VecMap}; + + const ELengthMismatch: u64 = 0; + + public fun vec_map_from_keys_values( + mut keys: vector, + mut values: vector, + ): VecMap { + assert!(keys.length() == values.length(), ELengthMismatch); + + let mut map = vec_map::empty(); + while (!keys.is_empty()) { + let key = keys.swap_remove(0); + let value = values.swap_remove(0); + map.insert(key, value); + }; + keys.destroy_empty(); + values.destroy_empty(); + + map + } + + #[test] + fun from_keys_values_works() { + let addresses = vector[@0x1, @0x2]; + let vps = vector[1, 1]; + + let map = vec_map_from_keys_values(addresses, vps); + assert!(map.size() == 2, 0); + } +} \ No newline at end of file diff --git a/identity_iota_core/scripts/publish_identity_package.sh b/identity_iota_core/scripts/publish_identity_package.sh new file mode 100755 index 0000000000..977e9f27ca --- /dev/null +++ b/identity_iota_core/scripts/publish_identity_package.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Copyright 2020-2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +script_dir=$(dirname $0) +package_dir=$script_dir/../packages/iota_identity + +# echo "publishing package from $package_dir" +cd $package_dir +iota client publish --with-unpublished-dependencies --skip-dependency-verification --json --gas-budget 500000000 . diff --git a/identity_iota_core/src/client/identity_client.rs b/identity_iota_core/src/client/identity_client.rs index 34df1fd5f0..5c7e256aef 100644 --- a/identity_iota_core/src/client/identity_client.rs +++ b/identity_iota_core/src/client/identity_client.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #[cfg(feature = "test")] -use iota_sdk::client::Client; +use iota_sdk_legacy::client::Client; use crate::block::address::Address; use crate::block::output::feature::SenderFeature; @@ -137,7 +137,7 @@ pub trait IotaIdentityClientExt: IotaIdentityClient { /// # Errors /// /// - [`NetworkMismatch`](Error::NetworkMismatch) if the network of the DID and client differ. - /// - [`NotFound`](iota_sdk::client::Error::NoOutput) if the associated Alias Output was not found. + /// - [`NotFound`](iota_sdk_legacy::client::Error::NoOutput) if the associated Alias Output was not found. async fn resolve_did(&self, did: &IotaDID) -> Result { validate_network(self, did).await?; @@ -151,7 +151,7 @@ pub trait IotaIdentityClientExt: IotaIdentityClient { /// # Errors /// /// - [`NetworkMismatch`](Error::NetworkMismatch) if the network of the DID and client differ. - /// - [`NotFound`](iota_sdk::client::Error::NoOutput) if the associated Alias Output was not found. + /// - [`NotFound`](iota_sdk_legacy::client::Error::NoOutput) if the associated Alias Output was not found. async fn resolve_did_output(&self, did: &IotaDID) -> Result { validate_network(self, did).await?; diff --git a/identity_iota_core/src/client/iota_client.rs b/identity_iota_core/src/client/iota_client.rs index 8696fdf5e9..72381c7168 100644 --- a/identity_iota_core/src/client/iota_client.rs +++ b/identity_iota_core/src/client/iota_client.rs @@ -3,10 +3,10 @@ use std::ops::Deref; -use iota_sdk::client::api::input_selection::Burn; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::types::block::protocol::ProtocolParameters; +use iota_sdk_legacy::client::api::input_selection::Burn; +use iota_sdk_legacy::client::secret::SecretManager; +use iota_sdk_legacy::client::Client; +use iota_sdk_legacy::types::block::protocol::ProtocolParameters; use crate::block::address::Address; use crate::block::output::unlock_condition::AddressUnlockCondition; @@ -145,7 +145,7 @@ async fn publish_output( client: &Client, secret_manager: &SecretManager, alias_output: AliasOutput, -) -> iota_sdk::client::error::Result { +) -> iota_sdk_legacy::client::error::Result { let block: Block = client .build_block() .with_secret_manager(secret_manager) diff --git a/identity_iota_core/src/did/iota_did.rs b/identity_iota_core/src/did/iota_did.rs index 2dfbf5b1a8..e538423913 100644 --- a/identity_iota_core/src/did/iota_did.rs +++ b/identity_iota_core/src/did/iota_did.rs @@ -612,15 +612,15 @@ mod tests { // =========================================================================================================================== #[cfg(feature = "iota-client")] - fn arbitrary_alias_id() -> impl Strategy { + fn arbitrary_alias_id() -> impl Strategy { ( proptest::prelude::any::<[u8; 32]>(), - iota_sdk::types::block::output::OUTPUT_INDEX_RANGE, + iota_sdk_legacy::types::block::output::OUTPUT_INDEX_RANGE, ) .prop_map(|(bytes, idx)| { - let transaction_id = iota_sdk::types::block::payload::transaction::TransactionId::new(bytes); - let output_id = iota_sdk::types::block::output::OutputId::new(transaction_id, idx).unwrap(); - iota_sdk::types::block::output::AliasId::from(&output_id) + let transaction_id = iota_sdk_legacy::types::block::payload::transaction::TransactionId::new(bytes); + let output_id = iota_sdk_legacy::types::block::output::OutputId::new(transaction_id, idx).unwrap(); + iota_sdk_legacy::types::block::output::AliasId::from(&output_id) }) } @@ -650,7 +650,7 @@ mod tests { fn property_based_alias_id_string_representation_roundtrip(alias_id in arbitrary_alias_id()) { for network_name in VALID_NETWORK_NAMES.iter().map(|name| NetworkName::try_from(*name).unwrap()) { assert_eq!( - iota_sdk::types::block::output::AliasId::from_str(IotaDID::new(&alias_id, &network_name).tag_str()).unwrap(), + iota_sdk_legacy::types::block::output::AliasId::from_str(IotaDID::new(&alias_id, &network_name).tag_str()).unwrap(), alias_id ); } diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index bd3404045c..5ee3cfe778 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -396,8 +396,8 @@ impl IotaDocument { #[cfg(feature = "client")] mod client_document { - use iota_sdk::types::block::address::Hrp; - use iota_sdk::types::block::address::ToBech32Ext; + use iota_sdk_legacy::types::block::address::Hrp; + use iota_sdk_legacy::types::block::address::ToBech32Ext; use crate::block::address::Address; use crate::block::output::AliasId; diff --git a/identity_iota_core/src/error.rs b/identity_iota_core/src/error.rs index 2a5e16ef34..e43866d369 100644 --- a/identity_iota_core/src/error.rs +++ b/identity_iota_core/src/error.rs @@ -20,22 +20,28 @@ pub enum Error { #[cfg(feature = "iota-client")] /// Caused by a client failure during publishing. #[error("DID update: {0}")] - DIDUpdateError(&'static str, #[source] Option>), + DIDUpdateError( + &'static str, + #[source] Option>, + ), #[cfg(feature = "iota-client")] /// Caused by a client failure during resolution. #[error("DID resolution failed")] - DIDResolutionError(#[source] iota_sdk::client::error::Error), + DIDResolutionError(#[source] iota_sdk_legacy::client::error::Error), + /// Caused by a look failures during resolution. + #[error("DID resolution failed: {0}")] + DIDResolutionErrorKinesis(String), #[cfg(feature = "iota-client")] /// Caused by an error when building a basic output. #[error("basic output build error")] - BasicOutputBuildError(#[source] iota_sdk::types::block::Error), + BasicOutputBuildError(#[source] iota_sdk_legacy::types::block::Error), /// Caused by an invalid network name. #[error("\"{0}\" is not a valid network name in the context of the `iota` did method")] InvalidNetworkName(String), #[cfg(feature = "iota-client")] /// Caused by a failure to retrieve the token supply. #[error("unable to obtain the token supply from the client")] - TokenSupplyError(#[source] iota_sdk::client::Error), + TokenSupplyError(#[source] iota_sdk_legacy::client::Error), /// Caused by a mismatch of the DID's network and the network the client is connected with. #[error("unable to resolve a `{expected}` DID on network `{actual}`")] NetworkMismatch { @@ -47,7 +53,7 @@ pub enum Error { #[cfg(feature = "iota-client")] /// Caused by an error when fetching protocol parameters from a node. #[error("could not fetch protocol parameters")] - ProtocolParametersError(#[source] iota_sdk::client::Error), + ProtocolParametersError(#[source] iota_sdk_legacy::client::Error), /// Caused by an attempt to read state metadata that does not adhere to the IOTA DID method specification. #[error("invalid state metadata {0}")] InvalidStateMetadata(&'static str), @@ -62,7 +68,7 @@ pub enum Error { #[cfg(feature = "iota-client")] /// Caused by retrieving an output that is expected to be an alias output but is not. #[error("output with id `{0}` is not an alias output")] - NotAnAliasOutput(iota_sdk::types::block::output::OutputId), + NotAnAliasOutput(iota_sdk_legacy::types::block::output::OutputId), /// Caused by an error when constructing an output id. #[error("conversion to an OutputId failed: {0}")] OutputIdConversionError(String), diff --git a/identity_iota_core/src/lib.rs b/identity_iota_core/src/lib.rs index 6602fb10ba..27f4f803da 100644 --- a/identity_iota_core/src/lib.rs +++ b/identity_iota_core/src/lib.rs @@ -19,8 +19,8 @@ pub mod block { //! See [iota_sdk::types::block]. - pub use iota_sdk::types::block::*; - pub use iota_sdk::types::TryFromDto; + pub use iota_sdk_legacy::types::block::*; + pub use iota_sdk_legacy::types::TryFromDto; } #[cfg(feature = "client")] @@ -39,4 +39,6 @@ mod did; mod document; mod error; mod network; +#[cfg(feature = "kinesis-client")] +pub mod rebased; mod state_metadata; diff --git a/identity_iota_core/src/network/network_name.rs b/identity_iota_core/src/network/network_name.rs index 24291fe8a5..28527b7e06 100644 --- a/identity_iota_core/src/network/network_name.rs +++ b/identity_iota_core/src/network/network_name.rs @@ -96,7 +96,7 @@ impl Display for NetworkName { #[cfg(feature = "client")] mod try_from_network_name { - use iota_sdk::types::block::address::Hrp; + use iota_sdk_legacy::types::block::address::Hrp; use crate::Error; use crate::NetworkName; diff --git a/identity_iota_core/src/rebased/assets/asset.rs b/identity_iota_core/src/rebased/assets/asset.rs new file mode 100644 index 0000000000..525cefb5f6 --- /dev/null +++ b/identity_iota_core/src/rebased/assets/asset.rs @@ -0,0 +1,631 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr as _; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::sui::move_calls; +use crate::rebased::transaction::Transaction; +use crate::rebased::transaction::TransactionOutput; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; +use anyhow::anyhow; +use anyhow::Context; +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaData as _; +use iota_sdk::rpc_types::IotaExecutionStatus; +use iota_sdk::rpc_types::IotaObjectDataOptions; +use iota_sdk::rpc_types::IotaTransactionBlockEffectsAPI as _; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::base_types::SequenceNumber; +use iota_sdk::types::id::UID; +use iota_sdk::types::object::Owner; +use iota_sdk::types::TypeTag; +use iota_sdk::IotaClient; +use move_core_types::ident_str; +use move_core_types::language_storage::StructTag; +use secret_storage::Signer; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; + +/// An on-chain asset that carries information about its owned and its creator. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuthenticatedAsset { + id: UID, + #[serde( + deserialize_with = "deserialize_inner", + bound(deserialize = "T: for<'a> Deserialize<'a>") + )] + inner: T, + owner: IotaAddress, + origin: IotaAddress, + mutable: bool, + transferable: bool, + deletable: bool, +} + +fn deserialize_inner<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: DeserializeOwned, +{ + use serde::de::Error as _; + + match std::any::type_name::() { + "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => { + String::deserialize(deserializer).and_then(|s| serde_json::from_str(&s).map_err(D::Error::custom)) + } + _ => T::deserialize(deserializer), + } +} + +impl AuthenticatedAsset +where + T: DeserializeOwned, +{ + /// Resolves an [`AuthenticatedAsset`] by its ID `id`. + pub async fn get_by_id(id: ObjectID, client: &IotaClient) -> Result { + let res = client + .read_api() + .get_object_with_options(id, IotaObjectDataOptions::new().with_content()) + .await?; + let Some(data) = res.data else { + return Err(Error::ObjectLookup(res.error.map_or(String::new(), |e| e.to_string()))); + }; + data + .content + .ok_or_else(|| anyhow!("No content for object with ID {id}")) + .and_then(|content| content.try_into_move().context("not a Move object")) + .and_then(|obj_data| { + serde_json::from_value(obj_data.fields.to_json_value()).context("failed to deserialize move object") + }) + .map_err(|e| Error::ObjectLookup(e.to_string())) + } +} + +impl AuthenticatedAsset { + async fn object_ref(&self, client: &IotaClient) -> Result { + client + .read_api() + .get_object_with_options(self.id(), IotaObjectDataOptions::default()) + .await? + .object_ref_if_exists() + .ok_or_else(|| Error::ObjectLookup("missing object reference in response".to_owned())) + } + + /// Returns this [`AuthenticatedAsset`]'s ID. + pub fn id(&self) -> ObjectID { + *self.id.object_id() + } + + /// Returns a reference to this [`AuthenticatedAsset`]'s content. + pub fn content(&self) -> &T { + &self.inner + } + + /// Transfers ownership of this [`AuthenticatedAsset`] to `recipient`. + /// # Notes + /// This function doesn't perform the transfer right away, but instead creates a [`Transaction`] that + /// can be executed to carry out the transfer. + /// # Failures + /// * Returns an [`Error::InvalidConfig`] if this asset is not transferable. + pub fn transfer(self, recipient: IotaAddress) -> Result, Error> { + if !self.transferable { + return Err(Error::InvalidConfig(format!( + "`AuthenticatedAsset` {} is not transferable", + self.id() + ))); + } + Ok(TransferAssetTx { asset: self, recipient }) + } + + /// Destroys this [`AuthenticatedAsset`]. + /// # Notes + /// This function doesn't delete the asset right away, but instead creates a [`Transaction`] that + /// can be executed in order to destory the asset. + /// # Failures + /// * Returns an [`Error::InvalidConfig`] if this asset cannot be deleted. + pub fn delete(self) -> Result, Error> { + if !self.deletable { + return Err(Error::InvalidConfig(format!( + "`AuthenticatedAsset` {} cannot be deleted", + self.id() + ))); + } + + Ok(DeleteAssetTx(self)) + } + + /// Changes this [`AuthenticatedAsset`]'s content. + /// # Notes + /// This function doesn't update the asset right away, but instead creates a [`Transaction`] that + /// can be executed in order to update the asset's content. + /// # Failures + /// * Returns an [`Error::InvalidConfig`] if this asset cannot be updated. + pub fn set_content(&mut self, new_content: T) -> Result, Error> { + if !self.mutable { + return Err(Error::InvalidConfig(format!( + "`AuthenticatedAsset` {} is immutable", + self.id() + ))); + } + + Ok(UpdateContentTx { + asset: self, + new_content, + }) + } +} + +/// Builder-style struct to ease the creation of a new [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct AuthenticatedAssetBuilder { + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, +} + +impl MoveType for AuthenticatedAsset { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::Struct(Box::new(StructTag { + address: package.into(), + module: ident_str!("asset").into(), + name: ident_str!("AuthenticatedAsset").into(), + type_params: vec![T::move_type(package)], + })) + } +} + +impl AuthenticatedAssetBuilder { + /// Initializes the builder with the asset's content. + pub fn new(content: T) -> Self { + Self { + inner: content, + mutable: false, + transferable: false, + deletable: false, + } + } + + /// Sets whether the new asset allows for its modification. + /// + /// By default an [`AuthenticatedAsset`] is **immutable**. + pub fn mutable(mut self, mutable: bool) -> Self { + self.mutable = mutable; + self + } + + /// Sets whether the new asset allows the transfer of its ownership. + /// + /// By default an [`AuthenticatedAsset`] **cannot** be transfered. + pub fn transferable(mut self, transferable: bool) -> Self { + self.transferable = transferable; + self + } + + /// Sets whether the new asset can be deleted. + /// + /// By default an [`AuthenticatedAsset`] **cannot** be deleted. + pub fn deletable(mut self, deletable: bool) -> Self { + self.deletable = deletable; + self + } + + /// Creates a [`Transaction`] that will create the specified [`AuthenticatedAsset`] when executed. + pub fn finish(self) -> CreateAssetTx { + CreateAssetTx(self) + } +} + +/// Proposal for the transfer of an [`AuthenticatedAsset`]'s ownership from one [`IotaAddress`] to another. + +/// # Detailed Workflow +/// A [`TransferProposal`] is a **shared** _Move_ object that represents a request to transfer ownership +/// of an [`AuthenticatedAsset`] to a new owner. +/// +/// When a [`TransferProposal`] is created, it will seize the asset and send a `SenderCap` token to the current asset's +/// owner and a `RecipientCap` to the specified `recipient` address. +/// `recipient` can accept the transfer by presenting its `RecipientCap` (this prevents other users from claiming the +/// asset for themselves). +/// The current owner can cancel the proposal at any time - given the transfer hasn't been conclued yet - by presenting +/// its `SenderCap`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransferProposal { + id: UID, + asset_id: ObjectID, + sender_cap_id: ObjectID, + sender_address: IotaAddress, + recipient_cap_id: ObjectID, + recipient_address: IotaAddress, + done: bool, +} + +impl MoveType for TransferProposal { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::Struct(Box::new(StructTag { + address: package.into(), + module: ident_str!("asset").into(), + name: ident_str!("TransferProposal").into(), + type_params: vec![], + })) + } +} + +impl TransferProposal { + /// Resolves a [`TransferProposal`] by its ID `id`. + pub async fn get_by_id(id: ObjectID, client: &IotaClient) -> Result { + let res = client + .read_api() + .get_object_with_options(id, IotaObjectDataOptions::new().with_content()) + .await?; + let Some(data) = res.data else { + return Err(Error::ObjectLookup(res.error.map_or(String::new(), |e| e.to_string()))); + }; + data + .content + .ok_or_else(|| anyhow!("No content for object with ID {id}")) + .and_then(|content| content.try_into_move().context("not a Move object")) + .and_then(|obj_data| { + serde_json::from_value(obj_data.fields.to_json_value()).context("failed to deserialize move object") + }) + .map_err(|e| Error::ObjectLookup(e.to_string())) + } + + async fn get_cap(&self, cap_type: &str, client: &IdentityClient) -> Result { + let cap_tag = StructTag::from_str(&format!("{}::asset::{cap_type}", client.package_id())) + .map_err(|e| Error::ParsingFailed(e.to_string()))?; + client + .find_owned_ref(cap_tag, |obj_data| { + cap_type == "SenderCap" && self.sender_cap_id == obj_data.object_id + || cap_type == "RecipientCap" && self.recipient_cap_id == obj_data.object_id + }) + .await? + .ok_or_else(|| { + Error::MissingPermission(format!( + "no owned `{cap_type}` for transfer proposal {}", + self.id.object_id(), + )) + }) + } + + async fn asset_metadata(&self, client: &IotaClient) -> anyhow::Result<(ObjectRef, TypeTag)> { + let res = client + .read_api() + .get_object_with_options(self.asset_id, IotaObjectDataOptions::default().with_type()) + .await?; + let asset_ref = res + .object_ref_if_exists() + .context("missing object reference in response")?; + let param_type = res + .data + .context("missing data") + .and_then(|data| data.type_.context("missing type")) + .and_then(StructTag::try_from) + .and_then(|mut tag| { + if tag.type_params.is_empty() { + anyhow::bail!("no type parameter") + } else { + Ok(tag.type_params.remove(0)) + } + })?; + + Ok((asset_ref, param_type)) + } + + async fn initial_shared_version(&self, client: &IotaClient) -> anyhow::Result { + let owner = client + .read_api() + .get_object_with_options(*self.id.object_id(), IotaObjectDataOptions::default().with_owner()) + .await? + .owner() + .context("missing owner information")?; + match owner { + Owner::Shared { initial_shared_version } => Ok(initial_shared_version), + _ => anyhow::bail!("`TransferProposal` is not a shared object"), + } + } + + /// Accepts this [`TransferProposal`]. + /// # Warning + /// This operation only has an effects when it's invoked by this [`TransferProposal`]'s `recipient`. + pub fn accept(self) -> AcceptTransferTx { + AcceptTransferTx(self) + } + + /// Concludes or cancels this [`TransferProposal`]. + /// # Warning + /// * This operation only has an effects when it's invoked by this [`TransferProposal`]'s `sender`. + /// * Accepting a [`TransferProposal`] **doesn't** consume it from the ledger. This function must be used to correctly + /// consume both [`TransferProposal`] and `SenderCap`. + pub fn conclude_or_cancel(self) -> ConcludeTransferTx { + ConcludeTransferTx(self) + } + + /// Returns this [`TransferProposal`]'s ID. + pub fn id(&self) -> ObjectID { + *self.id.object_id() + } + + /// Returns this [`TransferProposal`]'s `sender`'s address. + pub fn sender(&self) -> IotaAddress { + self.sender_address + } + + /// Returns this [`TransferProposal`]'s `recipient`'s address. + pub fn recipient(&self) -> IotaAddress { + self.recipient_address + } + + /// Returns `true` if this [`TransferProposal`] is concluded. + pub fn is_concluded(&self) -> bool { + self.done + } +} + +/// A [`Transaction`] that updates an [`AuthenticatedAsset`]'s content. +#[derive(Debug)] +pub struct UpdateContentTx<'a, T> { + asset: &'a mut AuthenticatedAsset, + new_content: T, +} + +#[async_trait] +impl<'a, T> Transaction for UpdateContentTx<'a, T> +where + T: MoveType + Serialize + Clone + Send + Sync, +{ + type Output = (); + + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let tx = move_calls::asset::update( + self.asset.object_ref(client).await?, + self.new_content.clone(), + client.package_id(), + )?; + let response = client.execute_transaction(tx, gas_budget).await?; + let tx_status = response + .effects + .as_ref() + .context("transaction had no effects") + .map(|effects| effects.status()) + .map_err(|e| Error::TransactionUnexpectedResponse(e.to_string()))?; + + if let IotaExecutionStatus::Failure { error } = tx_status { + return Err(Error::TransactionUnexpectedResponse(error.clone())); + } + + self.asset.inner = self.new_content; + + Ok(TransactionOutput { output: (), response }) + } +} + +/// A [`Transaction`] that deletes an [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct DeleteAssetTx(AuthenticatedAsset); + +#[async_trait] +impl Transaction for DeleteAssetTx +where + T: MoveType + Send + Sync, +{ + type Output = (); + + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let asset_ref = self.0.object_ref(client).await?; + let tx = move_calls::asset::delete::(asset_ref, client.package_id())?; + + let response = client.execute_transaction(tx, gas_budget).await?; + Ok(TransactionOutput { output: (), response }) + } +} +/// A [`Transaction`] that creates a new [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct CreateAssetTx(AuthenticatedAssetBuilder); + +#[async_trait] +impl Transaction for CreateAssetTx +where + T: MoveType + Serialize + DeserializeOwned + Send, +{ + type Output = AuthenticatedAsset; + + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let AuthenticatedAssetBuilder { + inner, + mutable, + transferable, + deletable, + } = self.0; + let tx = move_calls::asset::new(inner, mutable, transferable, deletable, client.package_id())?; + + let response = client.execute_transaction(tx, gas_budget).await?; + + let created_asset_id = response + .effects + .as_ref() + .ok_or_else(|| Error::TransactionUnexpectedResponse("could not find effects in transaction response".to_owned()))? + .created() + .first() + .ok_or_else(|| Error::TransactionUnexpectedResponse("no object was created in this transaction".to_owned()))? + .object_id(); + + AuthenticatedAsset::get_by_id(created_asset_id, client) + .await + .map(move |output| TransactionOutput { output, response }) + } +} + +/// A [`Transaction`] that proposes the transfer of an [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct TransferAssetTx { + asset: AuthenticatedAsset, + recipient: IotaAddress, +} + +#[async_trait] +impl Transaction for TransferAssetTx +where + T: MoveType + Send + Sync, +{ + type Output = TransferProposal; + + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let tx = move_calls::asset::transfer::( + self.asset.object_ref(client).await?, + self.recipient, + client.package_id(), + )?; + + let tx_result = client.execute_transaction(tx, gas_budget).await?; + let created_obj_ids = tx_result + .effects + .as_ref() + .ok_or_else(|| Error::TransactionUnexpectedResponse("could not find effects in transaction response".to_owned()))? + .created() + .iter() + .map(|obj| obj.reference.object_id); + for id in created_obj_ids { + let object_type = client + .read_api() + .get_object_with_options(id, IotaObjectDataOptions::new().with_type()) + .await? + .data + .context("no data in response") + .and_then(|data| Ok(data.object_type()?.to_string())) + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + + if object_type == TransferProposal::move_type(client.package_id()).to_string() { + return TransferProposal::get_by_id(id, client) + .await + .map(move |proposal| TransactionOutput { + output: proposal, + response: tx_result, + }); + } + } + + Err(Error::TransactionUnexpectedResponse( + "no proposal was created in this transaction".to_owned(), + )) + } +} + +/// A [`Transaction`] that accepts the transfer of an [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct AcceptTransferTx(TransferProposal); + +#[async_trait] +impl Transaction for AcceptTransferTx { + type Output = (); + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + if self.0.done { + return Err(Error::TransactionBuildingFailed( + "the transfer has already been concluded".to_owned(), + )); + } + + let cap = self.0.get_cap("RecipientCap", client).await?; + let (asset_ref, param_type) = self + .0 + .asset_metadata(client) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + let initial_shared_version = self + .0 + .initial_shared_version(client) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + let tx = move_calls::asset::accept_proposal( + (self.0.id(), initial_shared_version), + cap, + asset_ref, + param_type, + client.package_id(), + )?; + + let response = client.execute_transaction(tx, gas_budget).await?; + Ok(TransactionOutput { output: (), response }) + } +} + +/// A [`Transaction`] that concludes the transfer of an [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct ConcludeTransferTx(TransferProposal); + +#[async_trait] +impl Transaction for ConcludeTransferTx { + type Output = (); + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let cap = self.0.get_cap("SenderCap", client).await?; + let (asset_ref, param_type) = self + .0 + .asset_metadata(client) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + let initial_shared_version = self + .0 + .initial_shared_version(client) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + + let tx = move_calls::asset::conclude_or_cancel( + (self.0.id(), initial_shared_version), + cap, + asset_ref, + param_type, + client.package_id(), + )?; + + let response = client.execute_transaction(tx, gas_budget).await?; + Ok(TransactionOutput { output: (), response }) + } +} diff --git a/identity_iota_core/src/rebased/assets/mod.rs b/identity_iota_core/src/rebased/assets/mod.rs new file mode 100644 index 0000000000..ba879d0181 --- /dev/null +++ b/identity_iota_core/src/rebased/assets/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod asset; +mod public_available_vc; + +pub use asset::*; +pub use public_available_vc::*; diff --git a/identity_iota_core/src/rebased/assets/public_available_vc.rs b/identity_iota_core/src/rebased/assets/public_available_vc.rs new file mode 100644 index 0000000000..a3812962a8 --- /dev/null +++ b/identity_iota_core/src/rebased/assets/public_available_vc.rs @@ -0,0 +1,132 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; +use std::str::FromStr; + +use anyhow::Context as _; +use identity_credential::credential::Credential; +use identity_credential::credential::Jwt; +use identity_credential::credential::JwtCredential; +use identity_jose::jwt::JwtHeader; +use identity_jose::jwu; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::transaction::Command; +use iota_sdk::types::transaction::ProgrammableMoveCall; +use iota_sdk::types::TypeTag; +use itertools::Itertools; +use move_core_types::ident_str; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IdentityClientReadOnly; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::transaction::Transaction; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +use super::AuthenticatedAsset; +use super::AuthenticatedAssetBuilder; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct IotaVerifiableCredential { + data: Vec, +} + +impl MoveType for IotaVerifiableCredential { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::from_str(&format!("{package}::public_vc::PublicVc")).expect("valid utf8") + } + + fn try_to_argument( + &self, + ptb: &mut ProgrammableTransactionBuilder, + package: Option, + ) -> Result { + let values = ptb + .pure(&self.data) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + Ok(ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: package.ok_or_else(|| Error::InvalidArgument("missing package ID".to_string()))?, + module: ident_str!("public_vc").into(), + function: ident_str!("new").into(), + type_arguments: vec![], + arguments: vec![values], + })))) + } +} + +#[derive(Debug, Clone)] +pub struct PublicAvailableVC { + asset: AuthenticatedAsset, + credential: Credential, +} + +impl Deref for PublicAvailableVC { + type Target = Credential; + fn deref(&self) -> &Self::Target { + &self.credential + } +} + +impl PublicAvailableVC { + pub fn object_id(&self) -> ObjectID { + self.asset.id() + } + + pub fn jwt(&self) -> Jwt { + String::from_utf8(self.asset.content().data.clone()) + .map(Jwt::new) + .expect("JWT is valid UTF8") + } + + pub async fn new(jwt: Jwt, gas_budget: Option, client: &IdentityClient) -> Result + where + S: Signer + Sync, + { + let jwt_bytes = String::from(jwt).into_bytes(); + let credential = parse_jwt_credential(&jwt_bytes)?; + let asset = AuthenticatedAssetBuilder::new(IotaVerifiableCredential { data: jwt_bytes }) + .transferable(false) + .mutable(true) + .deletable(true) + .finish() + .execute_with_opt_gas(gas_budget, client) + .await? + .output; + + Ok(Self { credential, asset }) + } + + pub async fn get_by_id(id: ObjectID, client: &IdentityClientReadOnly) -> Result { + let asset = client + .get_object_by_id::>(id) + .await?; + Self::try_from_asset(asset).map_err(|e| { + crate::rebased::Error::ObjectLookup(format!( + "object at address {id} is not a valid publicly available VC: {e}" + )) + }) + } + + fn try_from_asset(asset: AuthenticatedAsset) -> Result { + let credential = parse_jwt_credential(&asset.content().data)?; + Ok(Self { asset, credential }) + } +} + +fn parse_jwt_credential(bytes: &[u8]) -> Result { + let [header, payload, _signature]: [Vec; 3] = bytes + .split(|c| *c == b'.') + .map(jwu::decode_b64) + .try_collect::<_, Vec<_>, _>()? + .try_into() + .map_err(|_| anyhow::anyhow!("invalid JWT"))?; + let _header = serde_json::from_slice::(&header)?; + let credential_claims = serde_json::from_slice::(&payload)?; + credential_claims.try_into().context("invalid jwt credential claims") +} diff --git a/identity_iota_core/src/rebased/client/full_client.rs b/identity_iota_core/src/rebased/client/full_client.rs new file mode 100644 index 0000000000..0a43c288bf --- /dev/null +++ b/identity_iota_core/src/rebased/client/full_client.rs @@ -0,0 +1,432 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use crate::IotaDID; +use crate::IotaDocument; +use crate::StateMetadataDocument; +use async_trait::async_trait; +use fastcrypto::ed25519::Ed25519PublicKey; +use fastcrypto::traits::ToFromBytes; +use identity_verification::jwk::Jwk; +use iota_sdk::rpc_types::IotaExecutionStatus; +use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::rpc_types::IotaObjectDataFilter; +use iota_sdk::rpc_types::IotaObjectResponseQuery; +use iota_sdk::rpc_types::IotaTransactionBlockEffects; +use iota_sdk::rpc_types::IotaTransactionBlockEffectsAPI; +use iota_sdk::rpc_types::IotaTransactionBlockEffectsV1; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::rpc_types::IotaTransactionBlockResponseOptions; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::crypto::DefaultHash; +use iota_sdk::types::crypto::Signature; +use iota_sdk::types::crypto::SignatureScheme; +use iota_sdk::types::quorum_driver_types::ExecuteTransactionRequestType; +use iota_sdk::types::transaction::ProgrammableTransaction; +use iota_sdk::types::transaction::Transaction; +use iota_sdk::types::transaction::TransactionData; +use move_core_types::language_storage::StructTag; +use secret_storage::SignatureScheme as SignatureSchemeT; +use secret_storage::Signer; +use serde::de::DeserializeOwned; +use serde::Serialize; +use shared_crypto::intent::Intent; +use shared_crypto::intent::IntentMessage; + +use crate::rebased::assets::AuthenticatedAssetBuilder; +use crate::rebased::migration::Identity; +use crate::rebased::migration::IdentityBuilder; +use crate::rebased::transaction::Transaction as TransactionT; +use crate::rebased::transaction::TransactionOutput; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +use super::get_object_id_from_did; +use super::IdentityClientReadOnly; + +pub struct IotaKeySignature { + pub public_key: Vec, + pub signature: Vec, +} + +impl SignatureSchemeT for IotaKeySignature { + type PublicKey = Vec; + type Signature = Vec; +} + +/// Mirrored types from identity_storage::KeyId +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub struct KeyId(String); + +impl KeyId { + /// Creates a new key identifier from a string. + pub fn new(id: impl Into) -> Self { + Self(id.into()) + } + + /// Returns string representation of the key id. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for KeyId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl From for String { + fn from(value: KeyId) -> Self { + value.0 + } +} + +#[derive(Clone)] +pub struct IdentityClient { + read_client: IdentityClientReadOnly, + address: IotaAddress, + public_key: Vec, + signer: S, +} + +impl Deref for IdentityClient { + type Target = IdentityClientReadOnly; + fn deref(&self) -> &Self::Target { + &self.read_client + } +} + +impl IdentityClient +where + S: Signer, +{ + pub async fn new(client: IdentityClientReadOnly, signer: S) -> Result { + let public_key = signer + .public_key() + .await + .map_err(|e| Error::InvalidKey(e.to_string()))?; + let address = convert_to_address(&public_key)?; + + Ok(Self { + public_key, + address, + read_client: client, + signer, + }) + } + + async fn sign_transaction_data(&self, tx_data: &TransactionData) -> Result { + use fastcrypto::hash::HashFunction; + let sender_public_key = self.sender_public_key(); + + let intent = Intent::iota_transaction(); + let intent_msg = IntentMessage::new(intent, tx_data); + let mut hasher = DefaultHash::default(); + let bcs_bytes = bcs::to_bytes(&intent_msg).map_err(|err| { + Error::TransactionSigningFailed(format!("could not serialize transaction message to bcs; {err}")) + })?; + hasher.update(bcs_bytes); + let digest = hasher.finalize().digest; + + let raw_signature = self + .signer + .sign(&digest) + .await + .map_err(|err| Error::TransactionSigningFailed(format!("could not sign transaction message; {err}")))?; + + let binding = [ + [SignatureScheme::ED25519.flag()].as_slice(), + &raw_signature, + sender_public_key, + ] + .concat(); + let signature_bytes: &[u8] = binding.as_slice(); + + Signature::from_bytes(signature_bytes) + .map_err(|err| Error::TransactionSigningFailed(format!("could not parse signature to IOTA signature; {err}"))) + } + + pub(crate) async fn execute_transaction( + &self, + tx: ProgrammableTransaction, + gas_budget: Option, + ) -> Result { + let gas_budget = match gas_budget { + Some(gas) => gas, + None => self.default_gas_budget(&tx).await?, + }; + let tx_data = self.get_transaction_data(tx, gas_budget).await?; + let signature = self.sign_transaction_data(&tx_data).await?; + + // execute tx + let response = self + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .map_err(Error::TransactionExecutionFailed)?; + + if let Some(IotaTransactionBlockEffects::V1(IotaTransactionBlockEffectsV1 { + status: IotaExecutionStatus::Failure { error }, + .. + })) = &response.effects + { + Err(Error::TransactionUnexpectedResponse(error.to_string())) + } else { + Ok(response) + } + } +} + +impl IdentityClient { + /// Returns the bytes of the sender's public key. + pub fn sender_public_key(&self) -> &[u8] { + &self.public_key + } + + /// Returns this [`IdentityClient`]'s sender address. + pub fn sender_address(&self) -> IotaAddress { + self.address + } + + /// Returns a reference to this [`IdentityClient`]'s [`Signer`]. + pub fn signer(&self) -> &S { + &self.signer + } + + /// Returns a new [`IdentityBuilder`] in order to build a new [`crate::rebased::migration::OnChainIdentity`]. + pub fn create_identity<'a>(&self, iota_document: &'a [u8]) -> IdentityBuilder<'a> { + IdentityBuilder::new(iota_document) + } + + /// Returns a new [`IdentityBuilder`] in order to build a new [`crate::rebased::migration::OnChainIdentity`]. + pub fn create_authenticated_asset(&self, content: T) -> AuthenticatedAssetBuilder + where + T: MoveType + Serialize + DeserializeOwned, + { + AuthenticatedAssetBuilder::new(content) + } + + pub(crate) async fn default_gas_budget(&self, tx: &ProgrammableTransaction) -> Result { + let gas_price = self + .read_api() + .get_reference_gas_price() + .await + .map_err(|e| Error::RpcError(e.to_string()))?; + let gas_coin = self.get_coin_for_transaction().await?; + let tx_data = TransactionData::new_programmable( + self.sender_address(), + vec![gas_coin.object_ref()], + tx.clone(), + 50_000_000_000, + gas_price, + ); + let dry_run_gas_result = self.read_api().dry_run_transaction_block(tx_data).await?.effects; + if dry_run_gas_result.status().is_err() { + let IotaExecutionStatus::Failure { error } = dry_run_gas_result.into_status() else { + unreachable!(); + }; + return Err(Error::TransactionUnexpectedResponse(error)); + } + let gas_summary = dry_run_gas_result.gas_cost_summary(); + let overhead = gas_price * 1000; + let net_used = gas_summary.net_gas_usage(); + let computation = gas_summary.computation_cost; + + let budget = overhead + (net_used.max(0) as u64).max(computation); + Ok(budget) + } + + async fn get_coin_for_transaction(&self) -> Result { + let coins = self + .coin_read_api() + .get_coins(self.sender_address(), None, None, None) + .await + .map_err(|err| Error::GasIssue(format!("could not get coins; {err}")))?; + + coins + .data + .into_iter() + .next() + .ok_or_else(|| Error::GasIssue("could not find coins".to_string())) + } + + async fn get_transaction_data( + &self, + programmable_transaction: ProgrammableTransaction, + gas_budget: u64, + ) -> Result { + let gas_price = self + .read_api() + .get_reference_gas_price() + .await + .map_err(|err| Error::GasIssue(format!("could not get gas price; {err}")))?; + let coin = self.get_coin_for_transaction().await?; + let tx_data = TransactionData::new_programmable( + self.sender_address(), + vec![coin.object_ref()], + programmable_transaction, + gas_budget, + gas_price, + ); + + Ok(tx_data) + } + + /// Query the objects owned by the address wrapped by this client to find the object of type `tag` + /// and that satifies `predicate`. + pub async fn find_owned_ref

(&self, tag: StructTag, predicate: P) -> Result, Error> + where + P: Fn(&IotaObjectData) -> bool, + { + let filter = IotaObjectResponseQuery::new_with_filter(IotaObjectDataFilter::StructType(tag)); + + let mut cursor = None; + loop { + let mut page = self + .read_api() + .get_owned_objects(self.sender_address(), Some(filter.clone()), cursor, None) + .await?; + let obj_ref = std::mem::take(&mut page.data) + .into_iter() + .filter_map(|res| res.data) + .find(|obj| predicate(obj)) + .map(|obj_data| obj_data.object_ref()); + cursor = page.next_cursor; + + if obj_ref.is_some() { + return Ok(obj_ref); + } + if !page.has_next_page { + break; + } + } + + Ok(None) + } +} + +impl IdentityClient +where + S: Signer + Sync, +{ + /// Returns [`Transaction`] [`PublishDidTx`] that - when executed - will publish a new DID Document on chain. + pub fn publish_did_document(&self, document: IotaDocument) -> PublishDidTx { + PublishDidTx(document) + } + + // TODO: define what happens for (legacy|migrated|new) documents + /// Updates a DID Document. + pub async fn publish_did_document_update( + &self, + document: IotaDocument, + gas_budget: u64, + ) -> Result { + let mut oci = + if let Identity::FullFledged(value) = self.get_identity(get_object_id_from_did(document.id())?).await? { + value + } else { + return Err(Error::Identity("only new identities can be updated".to_string())); + }; + + oci + .update_did_document(document.clone()) + .finish(self) + .await? + .execute_with_gas(gas_budget, self) + .await?; + + Ok(document) + } + + /// Deactivates a DID document. + pub async fn deactivate_did_output(&self, did: &IotaDID, gas_budget: u64) -> Result<(), Error> { + let mut oci = if let Identity::FullFledged(value) = self.get_identity(get_object_id_from_did(did)?).await? { + value + } else { + return Err(Error::Identity("only new identities can be deactivated".to_string())); + }; + + oci + .deactivate_did() + .finish(self) + .await? + .execute_with_gas(gas_budget, self) + .await?; + + Ok(()) + } +} + +/// Utility function that returns the key's bytes of a JWK encoded public ed25519 key. +pub fn get_sender_public_key(sender_public_jwk: &Jwk) -> Result, Error> { + let public_key_base_64 = &sender_public_jwk + .try_okp_params() + .map_err(|err| Error::InvalidKey(format!("key not of type `Okp`; {err}")))? + .x; + + identity_jose::jwu::decode_b64(public_key_base_64) + .map_err(|err| Error::InvalidKey(format!("could not decode base64 public key; {err}"))) +} + +/// Utility function to convert a public key's bytes into an [`IotaAddress`]. +pub fn convert_to_address(sender_public_key: &[u8]) -> Result { + let public_key = Ed25519PublicKey::from_bytes(sender_public_key) + .map_err(|err| Error::InvalidKey(format!("could not parse public key to Ed25519 public key; {err}")))?; + + Ok(IotaAddress::from(&public_key)) +} + +/// Publishes a new DID Document on-chain. An [`crate::rebased::migration::OnChainIdentity`] will be created to contain +/// the provided document. +#[derive(Debug)] +pub struct PublishDidTx(IotaDocument); + +#[async_trait] +impl TransactionT for PublishDidTx { + type Output = IotaDocument; + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let packed = self + .0 + .clone() + .pack() + .map_err(|err| Error::DidDocSerialization(format!("could not pack DID document: {err}")))?; + + let TransactionOutput { + output: identity, + response, + } = client + .create_identity(&packed) + .finish() + .execute_with_opt_gas(gas_budget, client) + .await?; + + // replace placeholders in document + let did: IotaDID = IotaDID::new(&identity.id(), client.network()); + let metadata_document: StateMetadataDocument = self.0.into(); + let document_without_placeholders = metadata_document.into_iota_document(&did).map_err(|err| { + Error::DidDocParsingFailed(format!( + "could not replace placeholders in published DID document {did}; {err}" + )) + })?; + + Ok(TransactionOutput { + output: document_without_placeholders, + response, + }) + } +} diff --git a/identity_iota_core/src/rebased/client/mod.rs b/identity_iota_core/src/rebased/client/mod.rs new file mode 100644 index 0000000000..87cccc51ac --- /dev/null +++ b/identity_iota_core/src/rebased/client/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod full_client; +mod read_only; + +pub use full_client::*; +pub use read_only::*; diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs new file mode 100644 index 0000000000..96cb6e800f --- /dev/null +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -0,0 +1,306 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::str::FromStr; + +use crate::IotaDID; +use crate::IotaDocument; +use crate::NetworkName; +use anyhow::Context as _; +use futures::stream::FuturesUnordered; +use futures::TryStreamExt as _; +use iota_sdk::rpc_types::EventFilter; +use iota_sdk::rpc_types::IotaData as _; +use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::rpc_types::IotaObjectDataFilter; +use iota_sdk::rpc_types::IotaObjectDataOptions; +use iota_sdk::rpc_types::IotaObjectResponseQuery; +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::IotaClient; +use move_core_types::language_storage::StructTag; +use serde::de::DeserializeOwned; +use serde::Deserialize; + +use crate::rebased::migration::get_alias; +use crate::rebased::migration::get_identity; +use crate::rebased::migration::lookup; +use crate::rebased::migration::Identity; +use crate::rebased::Error; + +const UNKNOWN_NETWORK_HRP: &str = "unknwn"; + +/// An [`IotaClient`] enriched with identity-related +/// functionalities. +#[derive(Clone)] +pub struct IdentityClientReadOnly { + iota_client: IotaClient, + iota_identity_pkg_id: ObjectID, + migration_registry_id: ObjectID, + network: NetworkName, +} + +impl Deref for IdentityClientReadOnly { + type Target = IotaClient; + fn deref(&self) -> &Self::Target { + &self.iota_client + } +} + +impl IdentityClientReadOnly { + /// Returns `iota_identity`'s package ID. + /// The ID of the packages depends on the network + /// the client is connected to. + pub const fn package_id(&self) -> ObjectID { + self.iota_identity_pkg_id + } + + /// Returns the name of the network the client is + /// currently connected to. + pub const fn network(&self) -> &NetworkName { + &self.network + } + + /// Returns the migration registry's ID. + pub const fn migration_registry_id(&self) -> ObjectID { + self.migration_registry_id + } + + /// Attempts to create a new [`IdentityClientReadOnly`] from + /// the given [`IotaClient`]. + pub async fn new(iota_client: IotaClient, iota_identity_pkg_id: ObjectID) -> Result { + let IdentityPkgMetadata { + migration_registry_id, .. + } = identity_pkg_metadata(&iota_client, iota_identity_pkg_id).await?; + let network = get_client_network(&iota_client).await?; + Ok(Self { + iota_client, + iota_identity_pkg_id, + migration_registry_id, + network, + }) + } + + /// Same as [`Self::new`], but if the network isn't recognized among IOTA's official networks, + /// the provided `network_name` will be used. + pub async fn new_with_network_name( + iota_client: IotaClient, + iota_identity_pkg_id: ObjectID, + network_name: NetworkName, + ) -> Result { + let mut identity_client = Self::new(iota_client, iota_identity_pkg_id).await?; + if identity_client.network.as_ref() == UNKNOWN_NETWORK_HRP { + identity_client.network = network_name; + } + + Ok(identity_client) + } + + /// Resolves a _Move_ Object of ID `id` and parses it to a value of type `T`. + pub async fn get_object_by_id(&self, id: ObjectID) -> Result + where + T: DeserializeOwned, + { + self + .read_api() + .get_object_with_options(id, IotaObjectDataOptions::new().with_content()) + .await + .context("lookup request failed") + .and_then(|res| res.data.context("missing data in response")) + .and_then(|data| data.content.context("missing object content in data")) + .and_then(|content| content.try_into_move().context("not a move object")) + .and_then(|obj| serde_json::from_value(obj.fields.to_json_value()).context("failed to deserialize move object")) + .map_err(|e| Error::ObjectLookup(e.to_string())) + } + + #[allow(dead_code)] + pub(crate) async fn get_object_ref_by_id(&self, obj: ObjectID) -> Result, Error> { + self + .read_api() + .get_object_with_options(obj, IotaObjectDataOptions::default().with_owner()) + .await + .map(|response| { + response.data.map(|obj_data| OwnedObjectRef { + owner: obj_data.owner.expect("requested data"), + reference: obj_data.object_ref().into(), + }) + }) + .map_err(Error::from) + } + + /// Queries the object owned by this sender address and returns the first one + /// that matches `tag` and for which `predicate` returns `true`. + pub async fn find_owned_ref_for_address

( + &self, + address: IotaAddress, + tag: StructTag, + predicate: P, + ) -> Result, Error> + where + P: Fn(&IotaObjectData) -> bool, + { + let filter = IotaObjectResponseQuery::new_with_filter(IotaObjectDataFilter::StructType(tag)); + + let mut cursor = None; + loop { + let mut page = self + .read_api() + .get_owned_objects(address, Some(filter.clone()), cursor, None) + .await?; + let obj_ref = std::mem::take(&mut page.data) + .into_iter() + .filter_map(|res| res.data) + .find(|obj| predicate(obj)) + .map(|obj_data| obj_data.object_ref()); + cursor = page.next_cursor; + + if obj_ref.is_some() { + return Ok(obj_ref); + } + if !page.has_next_page { + break; + } + } + + Ok(None) + } + + /// Queries an [`IotaDocument`] DID Document through its `did`. + pub async fn resolve_did(&self, did: &IotaDID) -> Result { + let identity = get_identity(self, get_object_id_from_did(did)?) + .await? + .ok_or_else(|| Error::DIDResolutionError(format!("call succeeded but could not resolve {did} to object")))?; + + Ok(identity.clone()) + } + + /// Resolves an [`Identity`] from its ID `object_id`. + pub async fn get_identity(&self, object_id: ObjectID) -> Result { + // spawn all checks + let mut all_futures = + FuturesUnordered::, Error>> + Send>>>::new(); + all_futures.push(Box::pin(resolve_new(self, object_id))); + all_futures.push(Box::pin(resolve_migrated(self, object_id))); + all_futures.push(Box::pin(resolve_unmigrated(self, object_id))); + + // use first non-None value as result + let mut identity_outcome: Option = None; + while let Some(result) = all_futures.try_next().await? { + if result.is_some() { + identity_outcome = result; + all_futures.clear(); + break; + } + } + + identity_outcome.ok_or_else(|| Error::DIDResolutionError(format!("could not find DID document for {object_id}"))) + } +} + +#[derive(Debug)] +struct IdentityPkgMetadata { + _package_id: ObjectID, + migration_registry_id: ObjectID, +} + +#[derive(Deserialize)] +struct MigrationRegistryCreatedEvent { + #[allow(dead_code)] + id: ObjectID, +} + +async fn get_client_network(client: &IotaClient) -> Result { + let network_id = client + .read_api() + .get_chain_identifier() + .await + .map_err(|e| Error::RpcError(e.to_string()))?; + + // TODO: add entries when iota_identity package is published to well-known networks. + #[allow(clippy::match_single_binding)] + let network_hrp = match &network_id { + // "89c3eeec" => NetworkName::try_from("iota").unwrap(), + // "fe12a865" => NetworkName::try_from("atoi").unwrap(), + _ => NetworkName::try_from(UNKNOWN_NETWORK_HRP).unwrap(), // Unrecognized network + }; + + Ok(network_hrp) +} + +// TODO: remove argument `package_id` and use `EventFilter::MoveEventField` to find the beacon event and thus the +// package id. +// TODO: authenticate the beacon event with though sender's ID. +async fn identity_pkg_metadata(iota_client: &IotaClient, package_id: ObjectID) -> Result { + // const EVENT_BEACON_PATH: &str = "/beacon"; + // const EVENT_BEACON_VALUE: &[u8] = b"identity.rs_pkg"; + + // let event_filter = EventFilter::MoveEventField { + // path: EVENT_BEACON_PATH.to_string(), + // value: EVENT_BEACON_VALUE.to_json_value().expect("valid json representation"), + // }; + let event_filter = EventFilter::MoveEventType( + StructTag::from_str(&format!("{package_id}::migration_registry::MigrationRegistryCreated")).expect("valid utf8"), + ); + let mut returned_events = iota_client + .event_api() + .query_events(event_filter, None, Some(1), false) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .data; + let event = if !returned_events.is_empty() { + returned_events.swap_remove(0) + } else { + return Err(Error::InvalidConfig( + "no \"iota_identity\" package found on the provided network".to_string(), + )); + }; + + let registry_id = serde_json::from_value::(event.parsed_json) + .map(|e| e.id) + .map_err(|e| { + Error::MigrationRegistryNotFound(crate::rebased::migration::Error::NotFound(format!( + "Malformed \"MigrationRegistryEvent\": {}", + e + ))) + })?; + + Ok(IdentityPkgMetadata { + migration_registry_id: registry_id, + _package_id: package_id, + }) +} + +async fn resolve_new(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { + let onchain_identity = get_identity(client, object_id).await.map_err(|err| { + Error::DIDResolutionError(format!( + "could not get identity document for object id {object_id}; {err}" + )) + })?; + Ok(onchain_identity.map(Identity::FullFledged)) +} + +async fn resolve_migrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { + let onchain_identity = lookup(client, object_id).await.map_err(|err| { + Error::DIDResolutionError(format!( + "failed to look up object_id {object_id} in migration registry; {err}" + )) + })?; + Ok(onchain_identity.map(Identity::FullFledged)) +} + +async fn resolve_unmigrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { + let unmigrated_alias = get_alias(client, object_id) + .await + .map_err(|err| Error::DIDResolutionError(format!("could no query for object id {object_id}; {err}")))?; + Ok(unmigrated_alias.map(Identity::Legacy)) +} + +pub fn get_object_id_from_did(did: &IotaDID) -> Result { + ObjectID::from_str(did.tag_str()) + .map_err(|err| Error::DIDResolutionError(format!("could not parse object id from did {did}; {err}"))) +} diff --git a/identity_iota_core/src/rebased/error.rs b/identity_iota_core/src/rebased/error.rs new file mode 100644 index 0000000000..e537e22a97 --- /dev/null +++ b/identity_iota_core/src/rebased/error.rs @@ -0,0 +1,68 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Errors that may occur for the rebased logic. + +/// This type represents all possible errors that can occur in the library. +#[derive(Debug, thiserror::Error, strum::IntoStaticStr)] +#[non_exhaustive] +pub enum Error { + /// failed to connect to network. + #[error("failed to connect to iota network node; {0:?}")] + Network(String, #[source] iota_sdk::error::Error), + /// could not lookup an object ID. + #[error("failed to lookup an object; {0}")] + ObjectLookup(String), + /// MigrationRegistry error. + #[error(transparent)] + MigrationRegistryNotFound(crate::rebased::migration::Error), + /// Caused by a look failures during resolution. + #[error("DID resolution failed: {0}")] + DIDResolutionError(String), + /// Caused by invalid or missing arguments. + #[error("invalid or missing argument: {0}")] + InvalidArgument(String), + /// Caused by invalid keys. + #[error("invalid key: {0}")] + InvalidKey(String), + /// Caused by issues with paying for transaction. + #[error("issue with gas for transaction: {0}")] + GasIssue(String), + /// Could not parse module, package, etc. + #[error("failed to parse {0}")] + ParsingFailed(String), + /// Could not build transaction. + #[error("failed to build transaction; {0}")] + TransactionBuildingFailed(String), + /// Could not sign transaction. + #[error("failed to sign transaction; {0}")] + TransactionSigningFailed(String), + /// Could not execute transaction. + #[error("transaction execution failed; {0}")] + TransactionExecutionFailed(#[from] iota_sdk::error::Error), + /// Transaction yielded invalid response. This usually means that the transaction was executed but did not produce + /// the expected result. + #[error("transaction returned an unexpected response; {0}")] + TransactionUnexpectedResponse(String), + /// Config is invalid. + #[error("invalid config: {0}")] + InvalidConfig(String), + /// Failed to parse DID document. + #[error("failed to parse DID document; {0}")] + DidDocParsingFailed(String), + /// Failed to serialize DID document. + #[error("failed to serialize DID document; {0}")] + DidDocSerialization(String), + /// Identity related error. + #[error("identity error; {0}")] + Identity(String), + #[error("unexpected state when looking up identity history; {0}")] + /// Unexpected state when looking up identity history. + InvalidIdentityHistory(String), + /// An operation cannot be carried on for a lack of permissions - e.g. missing capability. + #[error("the requested operation cannot be performed for a lack of permissions; {0}")] + MissingPermission(String), + /// An error caused by either a connection issue or an invalid RPC call. + #[error("RPC error: {0}")] + RpcError(String), +} diff --git a/identity_iota_core/src/rebased/migration/alias.rs b/identity_iota_core/src/rebased/migration/alias.rs new file mode 100644 index 0000000000..5d3a0a498c --- /dev/null +++ b/identity_iota_core/src/rebased/migration/alias.rs @@ -0,0 +1,177 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaExecutionStatus; +use iota_sdk::rpc_types::IotaObjectDataOptions; +use iota_sdk::rpc_types::IotaTransactionBlockEffectsAPI; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::id::UID; +use iota_sdk::types::transaction::ProgrammableTransaction; +use iota_sdk::types::TypeTag; +use iota_sdk::types::STARDUST_PACKAGE_ID; +use secret_storage::Signer; +use serde; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IdentityClientReadOnly; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::sui::move_calls; +use crate::rebased::transaction::Transaction; +use crate::rebased::transaction::TransactionOutput; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; +use crate::StateMetadataDocument; + +use super::get_identity; +use super::Identity; +use super::OnChainIdentity; + +/// A legacy IOTA Stardust Output type, used to store DID Documents. +#[derive(Debug, Deserialize, Serialize)] +pub struct UnmigratedAlias { + /// The ID of the Alias = hash of the Output ID that created the Alias Output in Stardust. + /// This is the AliasID from Stardust. + pub id: UID, + + /// The last State Controller address assigned before the migration. + pub legacy_state_controller: Option, + /// A counter increased by 1 every time the alias was state transitioned. + pub state_index: u32, + /// State metadata that can be used to store additional information. + pub state_metadata: Option>, + + /// The sender feature. + pub sender: Option, + /// The metadata feature. pub metadata: Option>, + + /// The immutable issuer feature. + pub immutable_issuer: Option, + /// The immutable metadata feature. + pub immutable_metadata: Option>, +} + +impl MoveType for UnmigratedAlias { + fn move_type(_: ObjectID) -> TypeTag { + format!("{STARDUST_PACKAGE_ID}::alias::Alias") + .parse() + .expect("valid move type") + } +} + +/// Resolves an [`UnmigratedAlias`] given its ID `object_id`. +pub async fn get_alias(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { + match client.get_object_by_id(object_id).await { + Ok(alias) => Ok(Some(alias)), + Err(Error::ObjectLookup(err_msg)) if err_msg.contains("missing data") => Ok(None), + Err(e) => Err(e), + } +} + +impl UnmigratedAlias { + /// Returns a transaction that when executed migrates a legacy `Alias` + /// containing a DID Document to a new [`OnChainIdentity`]. + pub async fn migrate( + self, + client: &IdentityClientReadOnly, + ) -> Result, Error> { + // Try to parse a StateMetadataDocument out of this alias. + let identity = Identity::Legacy(self); + let did_doc = identity.did_document(client)?; + let Identity::Legacy(alias) = identity else { + unreachable!("alias was wrapped by us") + }; + // Get the ID of the `AliasOutput` that owns this `Alias`. + let dynamic_field_wrapper = client + .read_api() + .get_object_with_options(*alias.id.object_id(), IotaObjectDataOptions::new().with_owner()) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .owner() + .expect("owner was requested") + .get_owner_address() + .expect("alias is a dynamic field") + .into(); + let alias_output_id = client + .read_api() + .get_object_with_options(dynamic_field_wrapper, IotaObjectDataOptions::new().with_owner()) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .owner() + .expect("owner was requested") + .get_owner_address() + .expect("alias is owned by an alias_output") + .into(); + // Get alias_output's ref. + let alias_output_ref = client + .read_api() + .get_object_with_options(alias_output_id, IotaObjectDataOptions::default()) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .object_ref_if_exists() + .expect("alias_output exists"); + // Get migration registry ref. + let migration_registry_ref = client + .get_object_ref_by_id(client.migration_registry_id()) + .await? + .expect("migration registry exists"); + + // Extract creation metadata + let created = did_doc + .metadata + .created + // `to_unix` returns the seconds since EPOCH; we need milliseconds. + .map(|timestamp| timestamp.to_unix() as u64 * 1000); + + // Build migration tx. + let tx = + move_calls::migration::migrate_did_output(alias_output_ref, created, migration_registry_ref, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(MigrateLegacyAliasTx(tx)) + } +} + +#[derive(Debug)] +struct MigrateLegacyAliasTx(ProgrammableTransaction); + +#[async_trait] +impl Transaction for MigrateLegacyAliasTx { + type Output = OnChainIdentity; + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let response = self.0.execute_with_opt_gas(gas_budget, client).await?.response; + // Make sure the tx was successfull. + let effects = response + .effects + .as_ref() + .ok_or_else(|| Error::TransactionUnexpectedResponse("transaction had no effects".to_string()))?; + if let IotaExecutionStatus::Failure { error } = effects.status() { + Err(Error::TransactionUnexpectedResponse(error.to_string())) + } else { + let identity_ref = effects + .created() + .iter() + .find(|obj_ref| obj_ref.owner.is_shared()) + .ok_or_else(|| { + Error::TransactionUnexpectedResponse("Identity not found in transaction's results".to_string()) + })?; + + get_identity(client, identity_ref.object_id()) + .await + .map(move |identity| TransactionOutput { + output: identity.expect("identity exists on-chain"), + response, + }) + } + } +} diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs new file mode 100644 index 0000000000..4b7fa47f8a --- /dev/null +++ b/identity_iota_core/src/rebased/migration/identity.rs @@ -0,0 +1,559 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::collections::HashSet; +use std::ops::Deref; +use std::str::FromStr; + +use crate::rebased::sui::types::Number; +use crate::IotaDID; +use crate::IotaDocument; +use crate::StateMetadataDocument; +use async_trait::async_trait; +use identity_core::common::Timestamp; +use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::rpc_types::IotaObjectDataOptions; +use iota_sdk::rpc_types::IotaParsedData; +use iota_sdk::rpc_types::IotaParsedMoveObject; +use iota_sdk::rpc_types::IotaPastObjectResponse; +use iota_sdk::rpc_types::IotaTransactionBlockEffects; +use iota_sdk::rpc_types::IotaTransactionBlockResponseOptions; +use iota_sdk::rpc_types::ObjectChange; +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::base_types::SequenceNumber; +use iota_sdk::types::id::UID; +use iota_sdk::types::object::Owner; +use iota_sdk::types::TypeTag; +use move_core_types::ident_str; +use move_core_types::language_storage::StructTag; +use secret_storage::Signer; +use serde; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IdentityClientReadOnly; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::proposals::BorrowAction; +use crate::rebased::proposals::ConfigChange; +use crate::rebased::proposals::DeactiveDid; +use crate::rebased::proposals::ProposalBuilder; +use crate::rebased::proposals::SendAction; +use crate::rebased::proposals::UpdateDidDocument; +use crate::rebased::sui::move_calls; +use crate::rebased::transaction::Transaction; +use crate::rebased::transaction::TransactionOutput; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +use super::Multicontroller; +use super::UnmigratedAlias; + +const MODULE: &str = "identity"; +const NAME: &str = "Identity"; +const HISTORY_DEFAULT_PAGE_SIZE: usize = 10; + +/// An on-chain object holding a DID Document. +pub enum Identity { + /// A legacy IOTA Stardust's Identity. + Legacy(UnmigratedAlias), + /// An on-chain Identity. + FullFledged(OnChainIdentity), +} + +impl Identity { + /// Returns the [`IotaDocument`] DID Document stored inside this [`Identity`]. + pub fn did_document(&self, client: &IdentityClientReadOnly) -> Result { + let original_did = IotaDID::from_alias_id(self.id().to_string().as_str(), client.network()); + let doc_bytes = self.doc_bytes().ok_or(Error::DidDocParsingFailed( + "legacy alias output does not encode a DID document".to_owned(), + ))?; + + StateMetadataDocument::unpack(doc_bytes) + .and_then(|state_metadata_doc| state_metadata_doc.into_iota_document(&original_did)) + .map_err(|e| Error::DidDocParsingFailed(e.to_string())) + } + + fn id(&self) -> ObjectID { + match self { + Self::Legacy(alias) => *alias.id.object_id(), + Self::FullFledged(identity) => identity.id(), + } + } + + fn doc_bytes(&self) -> Option<&[u8]> { + match self { + Self::FullFledged(identity) => Some(identity.multi_controller.controlled_value().as_ref()), + Self::Legacy(alias) => alias.state_metadata.as_deref(), + } + } +} + +/// An on-chain entity that wraps a DID Document. +#[derive(Debug, Serialize)] +pub struct OnChainIdentity { + id: UID, + multi_controller: Multicontroller>, + did_doc: IotaDocument, +} + +impl Deref for OnChainIdentity { + type Target = IotaDocument; + fn deref(&self) -> &Self::Target { + &self.did_doc + } +} + +impl OnChainIdentity { + /// Returns the [`ObjectID`] of this [`OnChainIdentity`]. + pub fn id(&self) -> ObjectID { + *self.id.object_id() + } + + /// Returns true if this [`OnChainIdentity`] is shared between multiple controllers. + pub fn is_shared(&self) -> bool { + self.multi_controller.controllers().len() > 1 + } + + /// Returns this [`OnChainIdentity`]'s list of active proposals. + pub fn proposals(&self) -> &HashSet { + self.multi_controller.proposals() + } + + /// Returns this [`OnChainIdentity`]'s controllers as the map: `controller_id -> controller_voting_power`. + pub fn controllers(&self) -> &HashMap { + self.multi_controller.controllers() + } + + /// Returns the threshold required by this [`OnChainIdentity`] for executing a proposal. + pub fn threshold(&self) -> u64 { + self.multi_controller.threshold() + } + + /// Returns the voting power of controller with ID `controller_id`, if any. + pub fn controller_voting_power(&self, controller_id: ObjectID) -> Option { + self.multi_controller.controller_voting_power(controller_id) + } + + pub(crate) fn multicontroller(&self) -> &Multicontroller> { + &self.multi_controller + } + + pub(crate) async fn get_controller_cap(&self, client: &IdentityClient) -> Result { + let controller_cap_tag = StructTag::from_str(&format!("{}::multicontroller::ControllerCap", client.package_id())) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + client + .find_owned_ref(controller_cap_tag, |obj_data| { + self.multi_controller.has_member(obj_data.object_id) + }) + .await? + .ok_or_else(|| Error::Identity("this address has no control over the requested identity".to_string())) + } + + /// Updates this [`OnChainIdentity`]'s DID Document. + pub fn update_did_document(&mut self, updated_doc: IotaDocument) -> ProposalBuilder<'_, UpdateDidDocument> { + ProposalBuilder::new(self, UpdateDidDocument::new(updated_doc)) + } + + /// Updates this [`OnChainIdentity`]'s configuration. + pub fn update_config(&mut self) -> ProposalBuilder<'_, ConfigChange> { + ProposalBuilder::new(self, ConfigChange::default()) + } + + /// Deactivates the DID Document represented by this [`OnChainIdentity`]. + pub fn deactivate_did(&mut self) -> ProposalBuilder<'_, DeactiveDid> { + ProposalBuilder::new(self, DeactiveDid::new()) + } + + /// Sends assets owned by this [`OnChainIdentity`] to other addresses. + pub fn send_assets(&mut self) -> ProposalBuilder { + ProposalBuilder::new(self, SendAction::default()) + } + + /// Borrows assets owned by this [`OnChainIdentity`] to use them in a custom transaction. + /// # Notes + /// Make sure to call [`super::Proposal::with_intent`] before executing the proposal. + /// Failing to do so will make [`crate::proposals::ProposalT::execute`] return an error. + pub fn borrow_assets(&mut self) -> ProposalBuilder { + ProposalBuilder::new(self, BorrowAction::default()) + } + + /// Returns historical data for this [`OnChainIdentity`]. + pub async fn get_history( + &self, + client: &IdentityClientReadOnly, + last_version: Option<&IotaObjectData>, + page_size: Option, + ) -> Result, Error> { + let identity_ref = client + .get_object_ref_by_id(self.id()) + .await? + .ok_or_else(|| Error::InvalidIdentityHistory("no reference to identity loaded".to_string()))?; + let object_id = identity_ref.object_id(); + + let mut history: Vec = vec![]; + let mut current_version = if let Some(last_version_value) = last_version { + // starting version given, this will be skipped in paging + last_version_value.clone() + } else { + // no version given, this version will be included in history + let version = identity_ref.version(); + let response = get_past_object(client, object_id, version).await?; + let latest_version = if let IotaPastObjectResponse::VersionFound(response_value) = response { + response_value + } else { + return Err(Error::InvalidIdentityHistory(format!( + "could not find current version {version} of object {object_id}, response {response:?}" + ))); + }; + history.push(latest_version.clone()); // include current version in history if we start from now + latest_version + }; + + // limit lookup count to prevent locking on large histories + let page_size = page_size.unwrap_or(HISTORY_DEFAULT_PAGE_SIZE); + while history.len() < page_size { + let lookup = get_previous_version(client, current_version).await?; + if let Some(value) = lookup { + current_version = value; + history.push(current_version.clone()); + } else { + break; + } + } + + Ok(history) + } +} + +pub fn has_previous_version(history_item: &IotaObjectData) -> Result { + if let Some(Owner::Shared { initial_shared_version }) = history_item.owner { + Ok(history_item.version != initial_shared_version) + } else { + Err(Error::InvalidIdentityHistory(format!( + "provided history item does not seem to be a valid identity; {history_item}" + ))) + } +} + +async fn get_past_object( + client: &IdentityClientReadOnly, + object_id: ObjectID, + version: SequenceNumber, +) -> Result { + client + .read_api() + .try_get_parsed_past_object(object_id, version, IotaObjectDataOptions::full_content()) + .await + .map_err(|err| { + Error::InvalidIdentityHistory(format!("could not look up object {object_id} version {version}; {err}")) + }) +} + +async fn get_previous_version( + client: &IdentityClientReadOnly, + iod: IotaObjectData, +) -> Result, Error> { + // try to get digest of previous tx + // if we requested the prev tx and it isn't returned, this should be the oldest state + let prev_tx_digest = if let Some(value) = iod.previous_transaction { + value + } else { + return Ok(None); + }; + + // resolve previous tx + let prev_tx_response = client + .read_api() + .get_transaction_with_options( + prev_tx_digest, + IotaTransactionBlockResponseOptions::new().with_object_changes(), + ) + .await + .map_err(|err| { + Error::InvalidIdentityHistory(format!("could not get previous transaction {prev_tx_digest}; {err}")) + })?; + + // check for updated/created changes + let (created, other_changes): (Vec, _) = prev_tx_response + .clone() + .object_changes + .ok_or_else(|| { + Error::InvalidIdentityHistory(format!( + "could not find object changes for object {} in transaction {prev_tx_digest}", + iod.object_id + )) + })? + .into_iter() + .filter(|elem| iod.object_id.eq(&elem.object_id())) + .partition(|elem| matches!(elem, ObjectChange::Created { .. })); + + // previous tx contain create tx, so there is no previous version + if created.len() == 1 { + return Ok(None); + } + + let mut previous_versions: Vec = other_changes + .iter() + .filter_map(|elem| match elem { + ObjectChange::Mutated { previous_version, .. } => Some(*previous_version), + _ => None, + }) + .collect(); + + previous_versions.sort(); + + let earliest_previous = if let Some(value) = previous_versions.first() { + value + } else { + return Ok(None); // no mutations in prev tx, so no more versions can be found + }; + + let past_obj_response = get_past_object(client, iod.object_id, *earliest_previous).await?; + match past_obj_response { + IotaPastObjectResponse::VersionFound(value) => Ok(Some(value)), + _ => Err(Error::InvalidIdentityHistory(format!( + "could not find previous version, past object response: {past_obj_response:?}" + ))), + } +} + +/// Returns the [`OnChainIdentity`] having ID `object_id`, if it exists. +pub async fn get_identity( + client: &IdentityClientReadOnly, + object_id: ObjectID, +) -> Result, Error> { + let response = client + .read_api() + .get_object_with_options(object_id, IotaObjectDataOptions::new().with_content()) + .await + .map_err(|err| { + Error::ObjectLookup(format!( + "Could not get object with options for this object_id {object_id}; {err}" + )) + })?; + + // no issues with call but + let Some(data) = response.data else { + // call was successful but not data for alias id + return Ok(None); + }; + + let content = data + .content + .ok_or_else(|| Error::ObjectLookup(format!("no content in retrieved object in object id {object_id}")))?; + + let IotaParsedData::MoveObject(value) = content else { + return Err(Error::ObjectLookup(format!( + "found data at object id {object_id} is not an object" + ))); + }; + + if !is_identity(&value) { + return Ok(None); + } + + #[derive(Deserialize)] + struct TempOnChainIdentity { + id: UID, + did_doc: Multicontroller>, + created: Number, + updated: Number, + } + + let TempOnChainIdentity { + id, + did_doc: multi_controller, + created, + updated, + } = serde_json::from_value::(value.fields.to_json_value()).map_err(|err| { + Error::ObjectLookup(format!( + "could not parse identity document with object id {object_id}; {err}" + )) + })?; + let original_did = IotaDID::from_alias_id(id.object_id().to_string().as_str(), client.network()); + let controlled_value = multi_controller.controlled_value(); + // Parse DID document timestamps + let created = { + let timestamp_ms: u64 = created.try_into().expect("Move string-encoded u64 are valid u64"); + // `Timestamp` requires a timestamp expessed in seconds. + Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produses valid timestamps") + }; + let updated = { + let timestamp_ms: u64 = updated.try_into().expect("Move string-encoded u64 are valid u64"); + // `Timestamp` requires a timestamp expessed in seconds. + Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produses valid timestamps") + }; + + // check if DID has been deactivated + let mut did_doc = if controlled_value.is_empty() { + // DID has been deactivated by setting controlled value empty, therefore craft an empty document + let mut empty_document = IotaDocument::new_with_id(original_did.clone()); + empty_document.metadata.deactivated = Some(true); + + empty_document + } else { + // we have a value, therefore unpack it + StateMetadataDocument::unpack(controlled_value) + .and_then(|state_metadata_doc| state_metadata_doc.into_iota_document(&original_did)) + .map_err(|e| Error::DidDocParsingFailed(e.to_string()))? + }; + + // Overwrite `created` and `updated` with trusted value coming from the on-chain `Identity` object. + did_doc.metadata.created = Some(created); + did_doc.metadata.updated = Some(updated); + + Ok(Some(OnChainIdentity { + id, + multi_controller, + did_doc, + })) +} + +fn is_identity(value: &IotaParsedMoveObject) -> bool { + // if available we might also check if object stems from expected module + // but how would this act upon package updates? + value.type_.module.as_ident_str().as_str() == MODULE && value.type_.name.as_ident_str().as_str() == NAME +} + +/// Builder-style struct to create a new [`OnChainIdentity`]. +#[derive(Debug)] +pub struct IdentityBuilder<'a> { + did_doc: &'a [u8], + threshold: Option, + controllers: HashMap, +} + +impl<'a> IdentityBuilder<'a> { + /// Initializes a new builder for an [`OnChainIdentity`], where the passed `did_doc` will be + /// used as the identity's DID Document. + /// ## Warning + /// Validation of `did_doc` is deferred to [`CreateIdentityTx`]. + pub fn new(did_doc: &'a [u8]) -> Self { + Self { + did_doc, + threshold: None, + controllers: HashMap::new(), + } + } + + /// Gives `address` the capability to act as a controller with voting power `voting_power`. + pub fn controller(mut self, address: IotaAddress, voting_power: u64) -> Self { + self.controllers.insert(address, voting_power); + self + } + + /// Sets the identity's threshold. + pub fn threshold(mut self, threshold: u64) -> Self { + self.threshold = Some(threshold); + self + } + + /// Sets multiple controllers in a single step. See [`IdentityBuilder::controller`]. + pub fn controllers(self, controllers: I) -> Self + where + I: IntoIterator, + { + controllers + .into_iter() + .fold(self, |builder, (addr, vp)| builder.controller(addr, vp)) + } + + /// Turns this builder into a [`Transaction`], ready to be executed. + pub fn finish(self) -> CreateIdentityTx<'a> { + CreateIdentityTx(self) + } +} + +impl MoveType for OnChainIdentity { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::Struct(Box::new(StructTag { + address: package.into(), + module: ident_str!("identity").into(), + name: ident_str!("Identity").into(), + type_params: vec![], + })) + } +} + +/// A [`Transaction`] for creating a new [`OnChainIdentity`] from an [`IdentityBuilder`]. +#[derive(Debug)] +pub struct CreateIdentityTx<'a>(IdentityBuilder<'a>); + +#[async_trait] +impl<'a> Transaction for CreateIdentityTx<'a> { + type Output = OnChainIdentity; + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let IdentityBuilder { + did_doc, + threshold, + controllers, + } = self.0; + let programmable_transaction = if controllers.is_empty() { + move_calls::identity::new(did_doc, client.package_id())? + } else { + let threshold = match threshold { + Some(t) => t, + None if controllers.len() == 1 => *controllers + .values() + .next() + .ok_or_else(|| Error::Identity("could not get controller".to_string()))?, + None => { + return Err(Error::TransactionBuildingFailed( + "Missing field `threshold` in identity creation".to_owned(), + )) + } + }; + move_calls::identity::new_with_controllers(did_doc, controllers, threshold, client.package_id())? + }; + + let response = client.execute_transaction(programmable_transaction, gas_budget).await?; + + let created = match response.clone().effects { + Some(IotaTransactionBlockEffects::V1(effects)) => effects.created, + _ => { + return Err(Error::TransactionUnexpectedResponse(format!( + "could not find effects in transaction response: {response:?}" + ))); + } + }; + let new_identities: Vec = created + .into_iter() + .filter(|elem| { + matches!( + elem.owner, + Owner::Shared { + initial_shared_version: _, + } + ) + }) + .collect(); + let new_identity_id = match &new_identities[..] { + [value] => value.object_id(), + _ => { + return Err(Error::TransactionUnexpectedResponse(format!( + "could not find new identity in response: {response:?}" + ))); + } + }; + + get_identity(client, new_identity_id) + .await + .and_then(|identity| identity.ok_or_else(|| Error::ObjectLookup(new_identity_id.to_string()))) + .map(move |identity| TransactionOutput { + output: identity, + response, + }) + } +} diff --git a/identity_iota_core/src/rebased/migration/mod.rs b/identity_iota_core/src/rebased/migration/mod.rs new file mode 100644 index 0000000000..ae5470c042 --- /dev/null +++ b/identity_iota_core/src/rebased/migration/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod alias; +mod identity; +mod multicontroller; +mod registry; + +pub use alias::*; +pub use identity::*; +pub use multicontroller::*; +pub use registry::*; diff --git a/identity_iota_core/src/rebased/migration/multicontroller.rs b/identity_iota_core/src/rebased/migration/multicontroller.rs new file mode 100644 index 0000000000..b401924748 --- /dev/null +++ b/identity_iota_core/src/rebased/migration/multicontroller.rs @@ -0,0 +1,195 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::collections::HashSet; + +use crate::rebased::sui::types::Bag; +use crate::rebased::sui::types::Number; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::collection_types::Entry; +use iota_sdk::types::collection_types::VecMap; +use iota_sdk::types::collection_types::VecSet; +use iota_sdk::types::id::UID; +use serde::Deserialize; +use serde::Serialize; + +/// A [`Multicontroller`]'s proposal for changes. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde( + try_from = "IotaProposal::", + into = "IotaProposal::", + bound(serialize = "T: Serialize + Clone") +)] +pub struct Proposal { + id: UID, + expiration_epoch: Option, + votes: u64, + voters: HashSet, + pub(crate) action: T, +} + +impl Proposal { + /// Returns this [Proposal]'s ID. + pub fn id(&self) -> ObjectID { + *self.id.object_id() + } + + /// Returns the votes received by this [`Proposal`]. + pub fn votes(&self) -> u64 { + self.votes + } + + pub(crate) fn votes_mut(&mut self) -> &mut u64 { + &mut self.votes + } + + /// Returns a reference to the action contained by this [`Proposal`]. + pub fn action(&self) -> &T { + &self.action + } + + /// Consumes the [`Proposal`] returning its action. + pub fn into_action(self) -> T { + self.action + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct IotaProposal { + id: UID, + expiration_epoch: Option>, + votes: Number, + voters: VecSet, + action: T, +} + +impl TryFrom> for Proposal { + type Error = >>::Error; + fn try_from(proposal: IotaProposal) -> Result { + let IotaProposal { + id, + expiration_epoch, + votes, + voters, + action, + } = proposal; + let expiration_epoch = expiration_epoch.map(TryInto::try_into).transpose()?; + let votes = votes.try_into()?; + let voters = voters.contents.into_iter().collect(); + + Ok(Self { + id, + expiration_epoch, + votes, + voters, + action, + }) + } +} + +impl From> for IotaProposal { + fn from(value: Proposal) -> Self { + let Proposal { + id, + expiration_epoch, + votes, + voters, + action, + } = value; + let contents = voters.into_iter().collect(); + IotaProposal { + id, + expiration_epoch: expiration_epoch.map(Into::into), + votes: votes.into(), + voters: VecSet { contents }, + action, + } + } +} + +/// Representation of `identity.rs`'s `multicontroller::Multicontroller` Move type. +#[derive(Debug, Serialize, Deserialize)] +#[serde(try_from = "IotaMulticontroller::")] +pub struct Multicontroller { + controlled_value: T, + controllers: HashMap, + threshold: u64, + active_proposals: HashSet, + proposals: Bag, +} + +impl Multicontroller { + /// Returns a reference to the value that is shared between many controllers. + pub fn controlled_value(&self) -> &T { + &self.controlled_value + } + + /// Returns this [`Multicontroller`]'s threshold. + pub fn threshold(&self) -> u64 { + self.threshold + } + + /// Returns the lists of active [`Proposal`]s for this [`Multicontroller`]. + pub fn proposals(&self) -> &HashSet { + &self.active_proposals + } + + pub(crate) fn proposals_bag_id(&self) -> ObjectID { + *self.proposals.id.object_id() + } + + /// Returns the voting power for controller with ID `controller_cap_id`, if any. + pub fn controller_voting_power(&self, controller_cap_id: ObjectID) -> Option { + self.controllers.get(&controller_cap_id).copied() + } + + /// Consumes this [`Multicontroller`], returning the wrapped value. + pub fn into_inner(self) -> T { + self.controlled_value + } + + pub(crate) fn controllers(&self) -> &HashMap { + &self.controllers + } + + /// Returns `true` if `cap_id` is among this [`Multicontroller`]'s controllers' IDs. + pub fn has_member(&self, cap_id: ObjectID) -> bool { + self.controllers.contains_key(&cap_id) + } +} + +impl TryFrom> for Multicontroller { + type Error = >>::Error; + fn try_from(value: IotaMulticontroller) -> Result { + let IotaMulticontroller { + controlled_value, + controllers, + threshold, + active_proposals, + proposals, + } = value; + let controllers = controllers + .contents + .into_iter() + .map(|Entry { key: id, value: vp }| (u64::try_from(vp).map(|vp| (id, vp)))) + .collect::>()?; + + Ok(Multicontroller { + controlled_value, + controllers, + threshold: threshold.try_into()?, + active_proposals, + proposals, + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct IotaMulticontroller { + controlled_value: T, + controllers: VecMap>, + threshold: Number, + active_proposals: HashSet, + proposals: Bag, +} diff --git a/identity_iota_core/src/rebased/migration/registry.rs b/identity_iota_core/src/rebased/migration/registry.rs new file mode 100644 index 0000000000..41b94a543d --- /dev/null +++ b/identity_iota_core/src/rebased/migration/registry.rs @@ -0,0 +1,60 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::rpc_types::IotaData; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::id::ID; + +use crate::rebased::client::IdentityClientReadOnly; + +use super::get_identity; +use super::OnChainIdentity; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + ClientError(anyhow::Error), + #[error("could not locate MigrationRegistry object: {0}")] + NotFound(String), + #[error("malformed MigrationRegistry's entry: {0}")] + Malformed(String), +} + +/// Lookup a legacy `alias_id` into the migration registry +/// to get the UID of the corresponding migrated DID document if any. +pub async fn lookup( + iota_client: &IdentityClientReadOnly, + alias_id: ObjectID, +) -> Result, Error> { + let dynamic_field_name = serde_json::from_value(serde_json::json!({ + "type": "0x2::object::ID", + "value": alias_id.to_string() + })) + .expect("valid move value"); + + let identity_id = iota_client + .read_api() + .get_dynamic_field_object(iota_client.migration_registry_id(), dynamic_field_name) + .await + .map_err(|e| Error::ClientError(e.into()))? + .data + .map(|data| { + data + .content + .and_then(|content| content.try_into_move()) + .and_then(|move_object| move_object.fields.to_json_value().get_mut("value").map(std::mem::take)) + .and_then(|value| serde_json::from_value::(value).map(|id| id.bytes).ok()) + .ok_or(Error::Malformed( + "invalid MigrationRegistry's Entry encoding".to_string(), + )) + }) + .transpose()?; + + if let Some(id) = identity_id { + get_identity(iota_client, id) + .await + .map_err(|e| Error::ClientError(e.into())) + } else { + Ok(None) + } +} diff --git a/identity_iota_core/src/rebased/mod.rs b/identity_iota_core/src/rebased/mod.rs new file mode 100644 index 0000000000..334d16e393 --- /dev/null +++ b/identity_iota_core/src/rebased/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod assets; +pub mod client; +mod error; +pub mod migration; +pub mod proposals; +mod sui; +pub mod transaction; +pub mod utils; + +pub use assets::*; +pub use error::Error; diff --git a/identity_iota_core/src/rebased/proposals/borrow.rs b/identity_iota_core/src/rebased/proposals/borrow.rs new file mode 100644 index 0000000000..60d309e7b0 --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/borrow.rs @@ -0,0 +1,255 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::marker::PhantomData; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::migration::Proposal; +use crate::rebased::sui::move_calls; +use crate::rebased::transaction::ProtoTransaction; +use crate::rebased::transaction::Transaction; +use crate::rebased::transaction::TransactionOutput; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::OnChainIdentity; +use super::ProposalBuilder; +use super::ProposalT; + +pub(crate) type IntentFn = Box) + Send>; + +/// Action used to borrow in transaction [OnChainIdentity]'s assets. +#[derive(Default, Deserialize, Serialize)] +pub struct BorrowAction { + objects: Vec, + #[serde(skip)] + intent: Option, +} + +/// A [`BorrowAction`] coupled with a user-provided function to describe how +/// the borrowed assets shall be used. +pub struct BorrowActionWithIntent +where + F: FnOnce(&mut Ptb, &HashMap), +{ + action: BorrowAction, + intent_fn: F, +} + +impl MoveType for BorrowAction { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::borrow_proposal::Borrow")).expect("valid move type") + } +} + +impl BorrowAction { + /// Adds an object to the lists of objects that will be borrowed when executing + /// this action in a proposal. + pub fn borrow_object(&mut self, object_id: ObjectID) { + self.objects.push(object_id); + } + + /// Adds many objects. See [`BorrowAction::borrow_object`] for more details. + pub fn borrow_objects(&mut self, objects: I) + where + I: IntoIterator, + { + objects.into_iter().for_each(|obj_id| self.borrow_object(obj_id)); + } +} + +impl<'i> ProposalBuilder<'i, BorrowAction> { + /// Adds an object to the list of objects that will be borrowed when executing this action. + pub fn borrow(mut self, object_id: ObjectID) -> Self { + self.borrow_object(object_id); + self + } + /// Adds many objects. See [`BorrowAction::borrow_object`] for more details. + pub fn borrow_objects(self, objects: I) -> Self + where + I: IntoIterator, + { + objects.into_iter().fold(self, |builder, obj| builder.borrow(obj)) + } +} + +impl Proposal { + /// Defines how the borrowed assets should be used. + pub fn with_intent(mut self, intent_fn: F) -> Self + where + F: FnOnce(&mut Ptb, &HashMap) + Send + 'static, + { + self.action.intent = Some(Box::new(intent_fn)); + self + } +} + +#[async_trait] +impl ProposalT for Proposal { + type Action = BorrowAction; + type Output = (); + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let tx = move_calls::identity::propose_borrow( + identity_ref, + controller_cap_ref, + action.objects, + expiration, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + // Borrow proposals cannot be chain-executed as they have to be driven. + chained_execution: false, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + _: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let proposal_id = self.id(); + let borrow_action = self.into_action(); + + Ok(ExecuteBorrowTx { + identity, + proposal_id, + borrow_action, + }) + } + + fn parse_tx_effects(_tx_response: &IotaTransactionBlockResponse) -> Result { + Ok(()) + } +} + +pub struct ExecuteBorrowTx<'i, B> { + identity: &'i mut OnChainIdentity, + borrow_action: B, + proposal_id: ObjectID, +} + +impl<'i> ExecuteBorrowTx<'i, BorrowAction> { + /// Defines how the borrowed assets should be used. + pub fn with_intent(self, intent_fn: F) -> ExecuteBorrowTx<'i, BorrowActionWithIntent> + where + F: FnOnce(&mut Ptb, &HashMap), + { + let ExecuteBorrowTx { + identity, + borrow_action, + proposal_id, + } = self; + ExecuteBorrowTx { + identity, + proposal_id, + borrow_action: BorrowActionWithIntent { + action: borrow_action, + intent_fn, + }, + } + } +} + +impl<'i> ProtoTransaction for ExecuteBorrowTx<'i, BorrowAction> { + type Input = IntentFn; + type Tx = ExecuteBorrowTx<'i, BorrowActionWithIntent>; + + fn with(self, input: Self::Input) -> Self::Tx { + self.with_intent(input) + } +} + +#[async_trait] +impl<'i, F> Transaction for ExecuteBorrowTx<'i, BorrowActionWithIntent> +where + F: FnOnce(&mut Ptb, &HashMap) + Send, +{ + type Output = (); + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let Self { + identity, + borrow_action, + proposal_id, + } = self; + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. + let object_data_list = { + let mut object_data_list = vec![]; + for obj_id in borrow_action.action.objects { + let object_data = super::obj_data_for_id(client, obj_id) + .await + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + object_data_list.push(object_data); + } + object_data_list + }; + + let tx = move_calls::identity::execute_borrow( + identity_ref, + controller_cap_ref, + proposal_id, + object_data_list, + borrow_action.intent_fn, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + ExecuteProposalTx { + identity, + tx, + _action: PhantomData::, + } + .execute_with_opt_gas(gas_budget, client) + .await + } +} diff --git a/identity_iota_core/src/rebased/proposals/config_change.rs b/identity_iota_core/src/rebased/proposals/config_change.rs new file mode 100644 index 0000000000..891eef86f8 --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/config_change.rs @@ -0,0 +1,288 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::collections::HashSet; +use std::marker::PhantomData; +use std::ops::DerefMut as _; +use std::str::FromStr as _; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::migration::Proposal; +use crate::rebased::sui::move_calls; +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::collection_types::Entry; +use iota_sdk::types::collection_types::VecMap; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::sui::types::Number; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::ProposalBuilder; +use super::ProposalT; + +/// [`Proposal`] action that modifies an [`OnChainIdentity`]'s configuration - e.g: +/// - remove controllers +/// - add controllers +/// - update controllers voting powers +/// - update threshold +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(try_from = "Modify")] +pub struct ConfigChange { + threshold: Option, + controllers_to_add: HashMap, + controllers_to_remove: HashSet, + controllers_voting_power: HashMap, +} + +impl MoveType for ConfigChange { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::from_str(&format!("{package}::config_proposal::Modify")).expect("valid type tag") + } +} + +impl<'i> ProposalBuilder<'i, ConfigChange> { + /// Sets a new value for the identity's threshold. + pub fn threshold(mut self, threshold: u64) -> Self { + self.set_threshold(threshold); + self + } + + /// Makes address `address` a new controller with voting power `voting_power`. + pub fn add_controller(mut self, address: IotaAddress, voting_power: u64) -> Self { + self.deref_mut().add_controller(address, voting_power); + self + } + + /// Adds multiple controllers. See [`ProposalBuilder::add_controller`]. + pub fn add_multiple_controllers(mut self, controllers: I) -> Self + where + I: IntoIterator, + { + self.deref_mut().add_multiple_controllers(controllers); + self + } + + /// Removes an existing controller. + pub fn remove_controller(mut self, controller_id: ObjectID) -> Self { + self.deref_mut().remove_controller(controller_id); + self + } + + /// Removes many controllers. + pub fn remove_multiple_controllers(mut self, controllers: I) -> Self + where + I: IntoIterator, + { + self.deref_mut().remove_multiple_controllers(controllers); + self + } +} + +impl ConfigChange { + pub fn new() -> Self { + Self::default() + } + + /// Sets the new threshold. + pub fn set_threshold(&mut self, new_threshold: u64) { + self.threshold = Some(new_threshold); + } + + /// Adds a controller. + pub fn add_controller(&mut self, address: IotaAddress, voting_power: u64) { + self.controllers_to_add.insert(address, voting_power); + } + + /// Adds many controllers. + pub fn add_multiple_controllers(&mut self, controllers: I) + where + I: IntoIterator, + { + for (addr, vp) in controllers { + self.add_controller(addr, vp) + } + } + + /// Removes an existing controller. + pub fn remove_controller(&mut self, controller_id: ObjectID) { + self.controllers_to_remove.insert(controller_id); + } + + /// Removes many controllers. + pub fn remove_multiple_controllers(&mut self, controllers: I) + where + I: IntoIterator, + { + for controller in controllers { + self.remove_controller(controller) + } + } + + fn validate(&self, identity: &OnChainIdentity) -> Result<(), Error> { + let new_threshold = self.threshold.unwrap_or(identity.threshold()); + let mut controllers = identity.controllers().clone(); + // check if update voting powers is valid + for (controller, new_vp) in &self.controllers_voting_power { + match controllers.get_mut(controller) { + Some(vp) => *vp = *new_vp, + None => { + return Err(Error::InvalidConfig(format!( + "object \"{controller}\" is not among identity \"{}\"'s controllers", + identity.id() + ))) + } + } + } + // check if deleting controllers is valid + for controller in &self.controllers_to_remove { + if controllers.remove(controller).is_none() { + return Err(Error::InvalidConfig(format!( + "object \"{controller}\" is not among identity \"{}\"'s controllers", + identity.id() + ))); + } + } + // check if adding controllers is valid + for (controller, vp) in &self.controllers_to_add { + if controllers.insert((*controller).into(), *vp).is_some() { + return Err(Error::InvalidConfig(format!( + "object \"{controller}\" is already among identity \"{}\"'s controllers", + identity.id() + ))); + } + } + // check whether the new threshold allows to interact with the identity + if new_threshold > controllers.values().sum() { + return Err(Error::InvalidConfig( + "the resulting configuration will result in an unaccessible identity".to_string(), + )); + } + Ok(()) + } +} + +#[async_trait] +impl ProposalT for Proposal { + type Action = ConfigChange; + type Output = (); + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + // Check the validity of the proposed changes. + action.validate(identity)?; + + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let sender_vp = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller exists"); + let chained_execution = sender_vp >= identity.threshold(); + let tx = move_calls::identity::propose_config_change( + identity_ref, + controller_cap_ref, + expiration, + action.threshold, + action.controllers_to_add, + action.controllers_to_remove, + action.controllers_voting_power, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let proposal_id = self.id(); + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let tx = + move_calls::identity::execute_config_change(identity_ref, controller_cap_ref, proposal_id, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects(_tx_response: &IotaTransactionBlockResponse) -> Result { + Ok(()) + } +} + +#[derive(Debug, Deserialize)] +struct Modify { + threshold: Option>, + controllers_to_add: VecMap>, + controllers_to_remove: HashSet, + controllers_to_update: VecMap>, +} + +impl TryFrom for ConfigChange { + type Error = >>::Error; + fn try_from(value: Modify) -> Result { + let Modify { + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + } = value; + let threshold = threshold.map(|num| num.try_into()).transpose()?; + let controllers_to_add = controllers_to_add + .contents + .into_iter() + .map(|Entry { key, value }| value.try_into().map(|n| (key, n))) + .collect::>()?; + let controllers_to_update = controllers_to_update + .contents + .into_iter() + .map(|Entry { key, value }| value.try_into().map(|n| (key, n))) + .collect::>()?; + Ok(Self { + threshold, + controllers_to_add, + controllers_to_remove, + controllers_voting_power: controllers_to_update, + }) + } +} diff --git a/identity_iota_core/src/rebased/proposals/deactive_did.rs b/identity_iota_core/src/rebased/proposals/deactive_did.rs new file mode 100644 index 0000000000..31eede5ee0 --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/deactive_did.rs @@ -0,0 +1,108 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::sui::move_calls; +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::migration::Proposal; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::ProposalT; + +/// Action for deactivating a DID Document inside an Identity. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub struct DeactiveDid; + +impl DeactiveDid { + pub const fn new() -> Self { + Self + } +} + +impl MoveType for DeactiveDid { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::identity::DeactivateDid")).expect("valid utf8") + } +} + +#[async_trait] +impl ProposalT for Proposal { + type Action = DeactiveDid; + type Output = (); + + async fn create<'i, S>( + _action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let sender_vp = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller exists"); + let chained_execution = sender_vp >= identity.threshold(); + let tx = + move_calls::identity::propose_deactivation(identity_ref, controller_cap_ref, expiration, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let proposal_id = self.id(); + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let tx = + move_calls::identity::execute_deactivation(identity_ref, controller_cap_ref, proposal_id, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects(_tx_response: &IotaTransactionBlockResponse) -> Result { + Ok(()) + } +} diff --git a/identity_iota_core/src/rebased/proposals/mod.rs b/identity_iota_core/src/rebased/proposals/mod.rs new file mode 100644 index 0000000000..1869b76c1a --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/mod.rs @@ -0,0 +1,333 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod borrow; +mod config_change; +mod deactive_did; +mod send; +mod update_did_doc; + +use std::marker::PhantomData; +use std::ops::Deref; +use std::ops::DerefMut; + +use crate::rebased::client::IdentityClientReadOnly; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::migration::get_identity; +use crate::rebased::sui::move_calls; +use crate::rebased::transaction::ProtoTransaction; +use async_trait::async_trait; +pub use borrow::*; +pub use config_change::*; +pub use deactive_did::*; +use iota_sdk::rpc_types::IotaExecutionStatus; +use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::rpc_types::IotaObjectDataOptions; +use iota_sdk::rpc_types::IotaTransactionBlockEffectsAPI; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::base_types::ObjectType; +use iota_sdk::types::transaction::ProgrammableTransaction; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +pub use send::*; +use serde::de::DeserializeOwned; +pub use update_did_doc::*; + +use crate::rebased::client::IdentityClient; +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::migration::Proposal; +use crate::rebased::transaction::Transaction; +use crate::rebased::transaction::TransactionOutput; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +/// Interface that allows the creation and execution of an [`OnChainIdentity`]'s [`Proposal`]s. +#[async_trait] +pub trait ProposalT { + /// The [`Proposal`] action's type. + type Action; + type Output; + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync; + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result + where + S: Signer + Sync; + + fn parse_tx_effects(tx_response: &IotaTransactionBlockResponse) -> Result; +} + +impl Proposal { + pub fn approve<'i>(&mut self, identity: &'i OnChainIdentity) -> ApproveProposalTx<'_, 'i, A> { + ApproveProposalTx { + proposal: self, + identity, + } + } +} + +#[derive(Debug)] +pub struct ProposalBuilder<'i, A> { + identity: &'i mut OnChainIdentity, + expiration: Option, + action: A, +} + +impl<'i, A> Deref for ProposalBuilder<'i, A> { + type Target = A; + fn deref(&self) -> &Self::Target { + &self.action + } +} + +impl<'i, A> DerefMut for ProposalBuilder<'i, A> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.action + } +} + +impl<'i, A> ProposalBuilder<'i, A> { + pub(crate) fn new(identity: &'i mut OnChainIdentity, action: A) -> Self { + Self { + identity, + expiration: None, + action, + } + } + + pub fn expiration_epoch(mut self, exp: u64) -> Self { + self.expiration = Some(exp); + self + } + + /// Creates a [`Proposal`] with the provided arguments. If `forbid_chained_execution` is set to `true`, + /// the [`Proposal`] won't be executed even if creator alone has enough voting power. + pub async fn finish(self, client: &IdentityClient) -> Result, Error> + where + Proposal: ProposalT, + S: Signer + Sync, + { + let Self { + action, + expiration, + identity, + } = self; + Proposal::::create(action, expiration, identity, client).await + } +} + +#[derive(Debug)] +/// The result of creating a [`Proposal`]. When a [`Proposal`] is executed +/// in the same transaction as its creation, a [`ProposalResult::Executed`] is +/// returned. [`ProposalResult::Pending`] otherwise. +pub enum ProposalResult { + /// A [`Proposal`] that has yet to be executed. + Pending(P), + /// A [`Proposal`]'s execution output. + Executed(P::Output), +} + +#[derive(Debug)] +pub struct CreateProposalTx<'i, A> { + identity: &'i mut OnChainIdentity, + tx: ProgrammableTransaction, + chained_execution: bool, + _action: PhantomData, +} + +#[async_trait] +impl<'i, A> Transaction for CreateProposalTx<'i, A> +where + Proposal: ProposalT + DeserializeOwned, + A: Send, +{ + type Output = ProposalResult>; + + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result>>, Error> + where + S: Signer + Sync, + { + let Self { + identity, + tx, + chained_execution, + .. + } = self; + let tx_response = client.execute_transaction(tx, gas_budget).await?; + let tx_effects_status = tx_response + .effects + .as_ref() + .ok_or_else(|| Error::TransactionUnexpectedResponse("missing transaction's effects".to_string()))? + .status(); + + if let IotaExecutionStatus::Failure { error } = tx_effects_status { + return Err(Error::TransactionUnexpectedResponse(error.clone())); + } + + // Identity has been changed regardless of whether the proposal has been executed + // or simply created. Refetch it, to sync it with its on-chain state. + *identity = get_identity(client, identity.id()) + .await? + .expect("identity exists on-chain"); + + if chained_execution { + // The proposal has been created and executed right-away. Parse its effects. + Proposal::::parse_tx_effects(&tx_response).map(ProposalResult::Executed) + } else { + // 2 objects are created, one is the Bag's Field and the other is our Proposal. Proposal is not owned by the bag, + // but the field is. + let proposals_bag_id = identity.multicontroller().proposals_bag_id(); + let proposal_id = tx_response + .effects + .as_ref() + .ok_or_else(|| Error::TransactionUnexpectedResponse("transaction had no effects".to_string()))? + .created() + .iter() + .find(|obj_ref| obj_ref.owner != proposals_bag_id) + .expect("tx was successful") + .object_id(); + + client.get_object_by_id(proposal_id).await.map(ProposalResult::Pending) + } + .map(move |output| TransactionOutput { + output, + response: tx_response, + }) + } +} + +#[derive(Debug)] +pub struct ExecuteProposalTx<'i, A> { + tx: ProgrammableTransaction, + identity: &'i mut OnChainIdentity, + _action: PhantomData, +} + +#[async_trait] +impl<'i, A> Transaction for ExecuteProposalTx<'i, A> +where + Proposal: ProposalT, + A: Send, +{ + type Output = as ProposalT>::Output; + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let Self { identity, tx, .. } = self; + let tx_response = client.execute_transaction(tx, gas_budget).await?; + let tx_effects_status = tx_response + .effects + .as_ref() + .ok_or_else(|| Error::TransactionUnexpectedResponse("missing effects".to_string()))?; + + if let IotaExecutionStatus::Failure { error } = tx_effects_status.status() { + Err(Error::TransactionUnexpectedResponse(error.clone())) + } else { + *identity = get_identity(client, identity.id()) + .await? + .expect("identity exists on-chain"); + + Proposal::::parse_tx_effects(&tx_response).map(move |output| TransactionOutput { + output, + response: tx_response, + }) + } + } +} + +#[derive(Debug)] +pub struct ApproveProposalTx<'p, 'i, A> { + proposal: &'p mut Proposal, + identity: &'i OnChainIdentity, +} + +#[async_trait] +impl<'p, 'i, A> Transaction for ApproveProposalTx<'p, 'i, A> +where + Proposal: ProposalT, + A: MoveType + Send, +{ + type Output = (); + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let Self { proposal, identity, .. } = self; + let identity_ref = client.get_object_ref_by_id(identity.id()).await?.unwrap(); + let controller_cap = identity.get_controller_cap(client).await?; + let tx = move_calls::identity::proposal::approve::( + identity_ref.clone(), + controller_cap, + proposal.id(), + client.package_id(), + )?; + + let response = client.execute_transaction(tx, gas_budget).await?; + let tx_effects_status = response + .effects + .as_ref() + .ok_or_else(|| Error::TransactionUnexpectedResponse("missing effects".to_string()))?; + + if let IotaExecutionStatus::Failure { error } = tx_effects_status.status() { + return Err(Error::TransactionUnexpectedResponse(error.clone())); + } + + let vp = identity + .controller_voting_power(controller_cap.0) + .expect("is identity's controller"); + *proposal.votes_mut() = proposal.votes() + vp; + + Ok(TransactionOutput { output: (), response }) + } +} + +async fn obj_data_for_id(client: &IdentityClientReadOnly, obj_id: ObjectID) -> anyhow::Result { + use anyhow::Context; + + client + .read_api() + .get_object_with_options(obj_id, IotaObjectDataOptions::default().with_type().with_owner()) + .await? + .into_object() + .context("no iota object in response") +} + +async fn obj_ref_and_type_for_id( + client: &IdentityClientReadOnly, + obj_id: ObjectID, +) -> anyhow::Result<(ObjectRef, TypeTag)> { + let res = obj_data_for_id(client, obj_id).await?; + let obj_ref = res.object_ref(); + let obj_type = match res.object_type().expect("object type is requested") { + ObjectType::Package => anyhow::bail!("a move package cannot be sent"), + ObjectType::Struct(type_) => type_.into(), + }; + + Ok((obj_ref, obj_type)) +} diff --git a/identity_iota_core/src/rebased/proposals/send.rs b/identity_iota_core/src/rebased/proposals/send.rs new file mode 100644 index 0000000000..ba1147908c --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/send.rs @@ -0,0 +1,185 @@ +use std::marker::PhantomData; +use std::ops::Deref; + +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::sui::move_calls; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::Proposal; +use super::ProposalBuilder; +use super::ProposalT; + +/// An action used to transfer [`crate::migration::OnChainIdentity`]-owned assets to other addresses. +#[derive(Debug, Clone, Deserialize, Default, Serialize)] +#[serde(from = "IotaSendAction", into = "IotaSendAction")] +pub struct SendAction(Vec<(ObjectID, IotaAddress)>); + +impl MoveType for SendAction { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::transfer_proposal::Send")).expect("valid move type") + } +} + +impl Deref for SendAction { + type Target = [(ObjectID, IotaAddress)]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl SendAction { + /// Adds to the list of object to send the object with ID `object_id` and send it to address `recipient`. + pub fn send_object(&mut self, object_id: ObjectID, recipient: IotaAddress) { + self.0.push((object_id, recipient)); + } + + /// Adds multiple objects to the list of objects to send. + pub fn send_objects(&mut self, objects: I) + where + I: IntoIterator, + { + objects + .into_iter() + .for_each(|(obj_id, recp)| self.send_object(obj_id, recp)); + } +} + +impl<'i> ProposalBuilder<'i, SendAction> { + /// Adds one object to the list of objects to send. + pub fn object(mut self, object_id: ObjectID, recipient: IotaAddress) -> Self { + self.send_object(object_id, recipient); + self + } + + /// Adds multiple objects to the list of objects to send. + pub fn objects(mut self, objects: I) -> Self + where + I: IntoIterator, + { + self.send_objects(objects); + self + } +} + +#[async_trait] +impl ProposalT for Proposal { + type Action = SendAction; + type Output = (); + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let tx = move_calls::identity::propose_send( + identity_ref, + controller_cap_ref, + action.0, + expiration, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + // Send proposals cannot be chain-executed as they have to be driven. + chained_execution: false, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let proposal_id = self.id(); + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. + let object_type_list = { + let ids = self.into_action().0.into_iter().map(|(obj_id, _rcp)| obj_id); + let mut object_and_type_list = vec![]; + for obj_id in ids { + let ref_and_type = super::obj_ref_and_type_for_id(client, obj_id) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + object_and_type_list.push(ref_and_type); + } + object_and_type_list + }; + + let tx = move_calls::identity::execute_send( + identity_ref, + controller_cap_ref, + proposal_id, + object_type_list, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects(_tx_response: &IotaTransactionBlockResponse) -> Result { + Ok(()) + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct IotaSendAction { + objects: Vec, + recipients: Vec, +} + +impl From for SendAction { + fn from(value: IotaSendAction) -> Self { + let IotaSendAction { objects, recipients } = value; + let transfer_map = objects.into_iter().zip(recipients).collect(); + SendAction(transfer_map) + } +} + +impl From for IotaSendAction { + fn from(action: SendAction) -> Self { + let (objects, recipients) = action.0.into_iter().unzip(); + Self { objects, recipients } + } +} diff --git a/identity_iota_core/src/rebased/proposals/update_did_doc.rs b/identity_iota_core/src/rebased/proposals/update_did_doc.rs new file mode 100644 index 0000000000..8d1af3be6b --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/update_did_doc.rs @@ -0,0 +1,131 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::sui::move_calls; +use crate::IotaDocument; +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::migration::Proposal; +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::ProposalT; + +/// Proposal's action for updating a DID Document. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(into = "UpdateValue::>", from = "UpdateValue::>")] +pub struct UpdateDidDocument(Vec); + +impl MoveType for UpdateDidDocument { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::update_value_proposal::UpdateValue>")).expect("valid TypeTag") + } +} + +impl UpdateDidDocument { + pub fn new(document: IotaDocument) -> Self { + Self(document.pack().expect("a valid IotaDocument is packable")) + } +} + +#[async_trait] +impl ProposalT for Proposal { + type Action = UpdateDidDocument; + type Output = (); + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let sender_vp = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller exists"); + let chained_execution = sender_vp >= identity.threshold(); + let tx = move_calls::identity::propose_update( + identity_ref, + controller_cap_ref, + action.0, + expiration, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let proposal_id = self.id(); + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let tx = move_calls::identity::execute_update(identity_ref, controller_cap_ref, proposal_id, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects(_tx_response: &IotaTransactionBlockResponse) -> Result { + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct UpdateValue { + new_value: V, +} + +impl From for UpdateValue> { + fn from(value: UpdateDidDocument) -> Self { + Self { new_value: value.0 } + } +} + +impl From>> for UpdateDidDocument { + fn from(value: UpdateValue>) -> Self { + UpdateDidDocument(value.new_value) + } +} diff --git a/identity_iota_core/src/rebased/sui/mod.rs b/identity_iota_core/src/rebased/sui/mod.rs new file mode 100644 index 0000000000..2daa1c8490 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod move_calls; +pub(crate) mod types; diff --git a/identity_iota_core/src/rebased/sui/move_calls/asset/create.rs b/identity_iota_core/src/rebased/sui/move_calls/asset/create.rs new file mode 100644 index 0000000000..57397a4907 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/asset/create.rs @@ -0,0 +1,39 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Command; +use iota_sdk::types::transaction::ProgrammableMoveCall; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; +use serde::Serialize; + +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +pub(crate) fn new( + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let inner = inner.try_to_argument(&mut ptb, Some(package))?; + let mutable = ptb.pure(mutable).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let transferable = ptb + .pure(transferable) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let deletable = ptb.pure(deletable).map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module: ident_str!("asset").into(), + function: ident_str!("new_with_config").into(), + type_arguments: vec![T::move_type(package)], + arguments: vec![inner, mutable, transferable, deletable], + }))); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/asset/delete.rs b/identity_iota_core/src/rebased/sui/move_calls/asset/delete.rs new file mode 100644 index 0000000000..715e06dbee --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/asset/delete.rs @@ -0,0 +1,34 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Command; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; + +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +pub(crate) fn delete(asset: ObjectRef, package: ObjectID) -> Result +where + T: MoveType, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("delete").into(), + vec![T::move_type(package)], + vec![asset], + )); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/asset/mod.rs b/identity_iota_core/src/rebased/sui/move_calls/asset/mod.rs new file mode 100644 index 0000000000..4a5d6683e3 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/asset/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod create; +mod delete; +mod transfer; +mod update; + +pub(crate) use create::*; +pub(crate) use delete::*; +pub(crate) use transfer::*; +pub(crate) use update::*; diff --git a/identity_iota_core/src/rebased/sui/move_calls/asset/transfer.rs b/identity_iota_core/src/rebased/sui/move_calls/asset/transfer.rs new file mode 100644 index 0000000000..b5e54b7bd1 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/asset/transfer.rs @@ -0,0 +1,99 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::base_types::SequenceNumber; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Command; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use iota_sdk::types::TypeTag; +use move_core_types::ident_str; + +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +pub(crate) fn transfer( + asset: ObjectRef, + recipient: IotaAddress, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let recipient = ptb.pure(recipient).map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("transfer").into(), + vec![T::move_type(package)], + vec![asset, recipient], + )); + + Ok(ptb.finish()) +} + +fn make_tx( + proposal: (ObjectID, SequenceNumber), + cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, + function_name: &'static str, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let proposal = ptb + .obj(ObjectArg::SharedObject { + id: proposal.0, + initial_shared_version: proposal.1, + mutable: true, + }) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let cap = ptb + .obj(ObjectArg::ImmOrOwnedObject(cap)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let asset = ptb + .obj(ObjectArg::Receiving(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!(function_name).into(), + vec![asset_type_param], + vec![proposal, cap, asset], + )); + + Ok(ptb.finish()) +} + +pub(crate) fn accept_proposal( + proposal: (ObjectID, SequenceNumber), + recipient_cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, +) -> Result { + make_tx(proposal, recipient_cap, asset, asset_type_param, package, "accept") +} + +pub(crate) fn conclude_or_cancel( + proposal: (ObjectID, SequenceNumber), + sender_cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, +) -> Result { + make_tx( + proposal, + sender_cap, + asset, + asset_type_param, + package, + "conclude_or_cancel", + ) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/asset/update.rs b/identity_iota_core/src/rebased/sui/move_calls/asset/update.rs new file mode 100644 index 0000000000..6e1e921a31 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/asset/update.rs @@ -0,0 +1,38 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Command; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; +use serde::Serialize; + +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +pub(crate) fn update(asset: ObjectRef, new_content: T, package: ObjectID) -> Result +where + T: MoveType + Serialize, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let new_content = ptb + .pure(new_content) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("set_content").into(), + vec![T::move_type(package)], + vec![asset, new_content], + )); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs new file mode 100644 index 0000000000..2e5268e5ea --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/borrow_asset.rs @@ -0,0 +1,119 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::base_types::ObjectType; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; + +use crate::rebased::proposals::BorrowAction; +use crate::rebased::sui::move_calls::utils; +use crate::rebased::utils::MoveType; + +pub(crate) fn propose_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + let objects_arg = ptb.pure(objects)?; + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_borrow").into(), + vec![], + vec![identity_arg, cap_arg, exp_arg, objects_arg], + ); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec, + intent_fn: F, + package: ObjectID, +) -> Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &HashMap), +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let proposal_id = ptb.pure(proposal_id)?; + + // Get the proposal's action as argument. + let borrow_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![BorrowAction::move_type(package)], + vec![identity, controller_cap, proposal_id], + ); + + // Borrow all the objects specified in the action. + let obj_arg_map = objects + .into_iter() + .map(|obj_data| { + let obj_ref = obj_data.object_ref(); + let ObjectType::Struct(obj_type) = obj_data.object_type()? else { + unreachable!("move packages cannot be borrowed to begin with"); + }; + let recv_obj = ptb.obj(ObjectArg::Receiving(obj_ref))?; + + let obj_arg = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_borrow").into(), + vec![obj_type.into()], + vec![identity, borrow_action, recv_obj], + ); + + Ok((obj_ref.0, (obj_arg, obj_data))) + }) + .collect::>()?; + + // Apply the user-defined operation. + intent_fn(&mut ptb, &obj_arg_map); + + // Put back all the objects. + obj_arg_map.into_values().for_each(|(obj_arg, obj_data)| { + let ObjectType::Struct(obj_type) = obj_data.object_type().expect("checked above") else { + unreachable!("move packages cannot be borrowed to begin with"); + }; + ptb.programmable_move_call( + package, + ident_str!("borrow_proposal").into(), + ident_str!("put_back").into(), + vec![obj_type.into()], + vec![borrow_action, obj_arg], + ); + }); + + // Consume the now empty borrow_action + ptb.programmable_move_call( + package, + ident_str!("borrow_proposal").into(), + ident_str!("conclude_borrow").into(), + vec![], + vec![borrow_action], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs new file mode 100644 index 0000000000..26a4d8d94a --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/config.rs @@ -0,0 +1,115 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashSet; +use std::str::FromStr; + +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::object::Owner; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use iota_sdk::types::TypeTag; +use move_core_types::ident_str; + +use super::super::utils; + +#[allow(clippy::too_many_arguments)] +pub(crate) fn propose_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + expiration: Option, + threshold: Option, + controllers_to_add: I1, + controllers_to_remove: HashSet, + controllers_to_update: I2, + package: ObjectID, +) -> anyhow::Result +where + I1: IntoIterator, + I2: IntoIterator, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + + let controllers_to_add = { + let (addresses, vps): (Vec, Vec) = controllers_to_add.into_iter().unzip(); + let addresses = ptb.pure(addresses)?; + let vps = ptb.pure(vps)?; + + ptb.programmable_move_call( + package, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![addresses, vps], + ) + }; + let controllers_to_update = { + let (ids, vps): (Vec, Vec) = controllers_to_update.into_iter().unzip(); + let ids = ptb.pure(ids)?; + let vps = ptb.pure(vps)?; + + ptb.programmable_move_call( + package, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::from_str("0x2::object::ID").expect("valid utf8"), TypeTag::U64], + vec![ids, vps], + ) + }; + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(controller_cap))?; + let expiration = utils::option_to_move(expiration, &mut ptb, package)?; + let threshold = utils::option_to_move(threshold, &mut ptb, package)?; + let controllers_to_remove = ptb.pure(controllers_to_remove)?; + + let _proposal_id = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("propose_config_change").into(), + vec![], + vec![ + identity, + controller_cap, + expiration, + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + ], + ); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, +) -> anyhow::Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + + let Owner::Shared { initial_shared_version } = identity.owner else { + anyhow::bail!("identity \"{}\" is a not shared object", identity.reference.object_id); + }; + let identity = ptb.obj(ObjectArg::SharedObject { + id: identity.reference.object_id, + initial_shared_version, + mutable: true, + })?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(controller_cap))?; + let proposal_id = ptb.pure(proposal_id)?; + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_config_change").into(), + vec![], + vec![identity, controller_cap, proposal_id], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs new file mode 100644 index 0000000000..32be404e0b --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/create.rs @@ -0,0 +1,93 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Command; +use iota_sdk::types::transaction::ProgrammableMoveCall; +use iota_sdk::types::transaction::ProgrammableTransaction; +use iota_sdk::types::TypeTag; +use iota_sdk::types::IOTA_FRAMEWORK_PACKAGE_ID; +use move_core_types::ident_str; + +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::sui::move_calls::utils; + +use crate::rebased::utils::MoveType; +use crate::rebased::Error; + +/// Build a transaction that creates a new on-chain Identity containing `did_doc`. +pub(crate) fn new(did_doc: &[u8], package_id: ObjectID) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let doc_arg = utils::ptb_pure(&mut ptb, "did_doc", did_doc)?; + let clock = utils::get_clock_ref(&mut ptb); + + // Create a new identity, sending its capability to the tx's sender. + let identity_res = ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: package_id, + module: ident_str!("identity").into(), + function: ident_str!("new").into(), + type_arguments: vec![], + arguments: vec![doc_arg, clock], + }))); + + // Share the resulting identity. + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: IOTA_FRAMEWORK_PACKAGE_ID, + module: ident_str!("transfer").into(), + function: ident_str!("public_share_object").into(), + type_arguments: vec![OnChainIdentity::move_type(package_id)], + arguments: vec![identity_res], + }))); + + Ok(ptb.finish()) +} + +pub(crate) fn new_with_controllers( + did_doc: &[u8], + controllers: C, + threshold: u64, + package_id: ObjectID, +) -> Result +where + C: IntoIterator, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + + let controllers = { + let (ids, vps): (Vec, Vec) = controllers.into_iter().unzip(); + let ids = ptb.pure(ids).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let vps = ptb.pure(vps).map_err(|e| Error::InvalidArgument(e.to_string()))?; + ptb.programmable_move_call( + package_id, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![ids, vps], + ) + }; + let doc_arg = ptb.pure(did_doc).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let threshold_arg = ptb.pure(threshold).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let clock = utils::get_clock_ref(&mut ptb); + + // Create a new identity, sending its capabilities to the specified controllers. + let identity_res = ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: package_id, + module: ident_str!("identity").into(), + function: ident_str!("new_with_controllers").into(), + type_arguments: vec![], + arguments: vec![doc_arg, controllers, threshold_arg, clock], + }))); + + // Share the resulting identity. + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: IOTA_FRAMEWORK_PACKAGE_ID, + module: ident_str!("transfer").into(), + function: ident_str!("public_share_object").into(), + type_arguments: vec![OnChainIdentity::move_type(package_id)], + arguments: vec![identity_res], + }))); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs new file mode 100644 index 0000000000..bb1c7767f3 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/deactivate.rs @@ -0,0 +1,58 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; + +use crate::rebased::sui::move_calls::utils; + +pub(crate) fn propose_deactivation( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + let clock = utils::get_clock_ref(&mut ptb); + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_deactivation").into(), + vec![], + vec![identity_arg, cap_arg, exp_arg, clock], + ); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_deactivation( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let proposal_id = ptb.pure(proposal_id)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let clock = utils::get_clock_ref(&mut ptb); + + let _ = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("execute_deactivation").into(), + vec![], + vec![identity_arg, cap_arg, proposal_id, clock], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs new file mode 100644 index 0000000000..6c55b44b30 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod borrow_asset; +mod config; +mod create; +mod deactivate; +pub(crate) mod proposal; +mod send_asset; +mod update; + +pub(crate) use borrow_asset::*; +pub(crate) use config::*; +pub(crate) use create::*; +pub(crate) use deactivate::*; +pub(crate) use send_asset::*; +pub(crate) use update::*; diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs new file mode 100644 index 0000000000..4335bc1943 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/proposal.rs @@ -0,0 +1,51 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::rebased::utils::MoveType; +use crate::rebased::Error; +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::object::Owner; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; + +pub(crate) fn approve( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let Owner::Shared { initial_shared_version } = identity.owner else { + return Err(Error::TransactionBuildingFailed(format!( + "Identity \"{}\" is not a shared object", + identity.object_id() + ))); + }; + let identity = ptb + .obj(ObjectArg::SharedObject { + id: identity.object_id(), + initial_shared_version, + mutable: true, + }) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let controller_cap = ptb + .obj(ObjectArg::ImmOrOwnedObject(controller_cap)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let proposal_id = ptb + .pure(proposal_id) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("approve_proposal").into(), + vec![T::move_type(package)], + vec![identity, controller_cap, proposal_id], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs new file mode 100644 index 0000000000..19c1a9f758 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/send_asset.rs @@ -0,0 +1,93 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use iota_sdk::types::TypeTag; +use move_core_types::ident_str; + +use crate::rebased::proposals::SendAction; +use crate::rebased::sui::move_calls; +use crate::rebased::utils::MoveType; + +pub(crate) fn propose_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let identity_arg = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = move_calls::utils::option_to_move(expiration, &mut ptb, package_id)?; + let (objects, recipients) = { + let (objects, recipients): (Vec<_>, Vec<_>) = transfer_map.into_iter().unzip(); + let objects = ptb.pure(objects)?; + let recipients = ptb.pure(recipients)?; + + (objects, recipients) + }; + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_send").into(), + vec![], + vec![identity_arg, cap_arg, exp_arg, objects, recipients], + ); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let proposal_id = ptb.pure(proposal_id)?; + + // Get the proposal's action as argument. + let send_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![SendAction::move_type(package)], + vec![identity, controller_cap, proposal_id], + ); + + // Send each object in this send action. + // Traversing the map in reverse reduces the number of operations on the move side. + for (obj, obj_type) in objects.into_iter().rev() { + let recv_obj = ptb.obj(ObjectArg::Receiving(obj))?; + + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_send").into(), + vec![obj_type], + vec![identity, send_action, recv_obj], + ); + } + + // Consume the now empty send_action + ptb.programmable_move_call( + package, + ident_str!("transfer_proposal").into(), + ident_str!("complete_send").into(), + vec![], + vec![send_action], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs b/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs new file mode 100644 index 0000000000..93603b1da1 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/identity/update.rs @@ -0,0 +1,60 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::ObjectRef; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::transaction::ProgrammableTransaction; +use move_core_types::ident_str; + +use crate::rebased::sui::move_calls::utils; + +pub(crate) fn propose_update( + identity: OwnedObjectRef, + capability: ObjectRef, + did_doc: impl AsRef<[u8]>, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + let doc_arg = ptb.pure(did_doc.as_ref())?; + let clock = utils::get_clock_ref(&mut ptb); + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_update").into(), + vec![], + vec![identity_arg, cap_arg, doc_arg, exp_arg, clock], + ); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_update( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let proposal_id = ptb.pure(proposal_id)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let clock = utils::get_clock_ref(&mut ptb); + + let _ = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("execute_update").into(), + vec![], + vec![identity_arg, cap_arg, proposal_id, clock], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/migration.rs b/identity_iota_core/src/rebased/sui/move_calls/migration.rs new file mode 100644 index 0000000000..7c17218482 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/migration.rs @@ -0,0 +1,44 @@ +use super::utils; +use iota_sdk::{ + rpc_types::OwnedObjectRef, + types::{ + base_types::{ObjectID, ObjectRef}, + programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb, + transaction::{ObjectArg, ProgrammableTransaction}, + IOTA_FRAMEWORK_PACKAGE_ID, + }, +}; +use move_core_types::ident_str; + +pub(crate) fn migrate_did_output( + did_output: ObjectRef, + creation_timestamp: Option, + migration_registry: OwnedObjectRef, + package: ObjectID, +) -> anyhow::Result { + let mut ptb = Ptb::new(); + let did_output = ptb.obj(ObjectArg::ImmOrOwnedObject(did_output))?; + let migration_registry = utils::owned_ref_to_shared_object_arg(migration_registry, &mut ptb, true)?; + let clock = utils::get_clock_ref(&mut ptb); + + let creation_timestamp = match creation_timestamp { + Some(timestamp) => ptb.pure(timestamp)?, + _ => ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("clock").into(), + ident_str!("timestamp_ms").into(), + vec![], + vec![clock], + ), + }; + + ptb.programmable_move_call( + package, + ident_str!("migration").into(), + ident_str!("migrate_alias_output").into(), + vec![], + vec![did_output, migration_registry, creation_timestamp, clock], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/sui/move_calls/mod.rs b/identity_iota_core/src/rebased/sui/move_calls/mod.rs new file mode 100644 index 0000000000..ce4ee5dd92 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/mod.rs @@ -0,0 +1,9 @@ +/// Predefined `AuthenticatedAsset`-related PTBs. +pub(crate) mod asset; +/// Predefined `OnChainIdentity`-related PTBs. +pub(crate) mod identity; +/// Predefined PTBs used to migrate a legacy Stardust's AliasOutput +/// to an `OnChainIdentity`. +pub(crate) mod migration; + +mod utils; diff --git a/identity_iota_core/src/rebased/sui/move_calls/utils.rs b/identity_iota_core/src/rebased/sui/move_calls/utils.rs new file mode 100644 index 0000000000..c35ee65520 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/move_calls/utils.rs @@ -0,0 +1,88 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::rebased::utils::MoveType; +use crate::rebased::Error; +use iota_sdk::rpc_types::OwnedObjectRef; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::STD_OPTION_MODULE_NAME; +use iota_sdk::types::object::Owner; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::IOTA_CLOCK_OBJECT_ID; +use iota_sdk::types::IOTA_CLOCK_OBJECT_SHARED_VERSION; +use iota_sdk::types::MOVE_STDLIB_PACKAGE_ID; +use move_core_types::ident_str; +use serde::Serialize; + +/// Adds a reference to the on-chain clock to `ptb`'s arguments. +pub(crate) fn get_clock_ref(ptb: &mut Ptb) -> Argument { + ptb + .obj(ObjectArg::SharedObject { + id: IOTA_CLOCK_OBJECT_ID, + initial_shared_version: IOTA_CLOCK_OBJECT_SHARED_VERSION, + mutable: false, + }) + .expect("network has a singleton clock instantiated") +} + +pub(crate) fn owned_ref_to_shared_object_arg( + owned_ref: OwnedObjectRef, + ptb: &mut Ptb, + mutable: bool, +) -> anyhow::Result { + let Owner::Shared { initial_shared_version } = owned_ref.owner else { + anyhow::bail!("Identity \"{}\" is not a shared object", owned_ref.object_id()); + }; + ptb.obj(ObjectArg::SharedObject { + id: owned_ref.object_id(), + initial_shared_version, + mutable, + }) +} + +pub(crate) fn option_to_move( + option: Option, + ptb: &mut Ptb, + package: ObjectID, +) -> Result { + let arg = if let Some(t) = option { + let t = ptb.pure(t)?; + ptb.programmable_move_call( + MOVE_STDLIB_PACKAGE_ID, + STD_OPTION_MODULE_NAME.into(), + ident_str!("some").into(), + vec![T::move_type(package)], + vec![t], + ) + } else { + ptb.programmable_move_call( + MOVE_STDLIB_PACKAGE_ID, + STD_OPTION_MODULE_NAME.into(), + ident_str!("none").into(), + vec![T::move_type(package)], + vec![], + ) + }; + + Ok(arg) +} + +pub(crate) fn ptb_pure(ptb: &mut Ptb, name: &str, value: T) -> Result +where + T: Serialize + core::fmt::Debug, +{ + ptb.pure(&value).map_err(|err| { + Error::InvalidArgument(format!( + r"could not serialize pure value {name} with value {value:?}; {err}" + )) + }) +} + +#[allow(dead_code)] +pub(crate) fn ptb_obj(ptb: &mut Ptb, name: &str, value: ObjectArg) -> Result { + ptb + .obj(value) + .map_err(|err| Error::InvalidArgument(format!("could not serialize object {name} {value:?}; {err}"))) +} diff --git a/identity_iota_core/src/rebased/sui/types/mod.rs b/identity_iota_core/src/rebased/sui/types/mod.rs new file mode 100644 index 0000000000..3937289194 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/types/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod number; + +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::id::UID; +pub(crate) use number::*; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct Bag { + pub id: UID, + #[serde(deserialize_with = "serde_aux::field_attributes::deserialize_number_from_string")] + pub size: u64, +} + +impl Default for Bag { + fn default() -> Self { + Self { + id: UID::new(ObjectID::ZERO), + size: 0, + } + } +} diff --git a/identity_iota_core/src/rebased/sui/types/number.rs b/identity_iota_core/src/rebased/sui/types/number.rs new file mode 100644 index 0000000000..2e561d0043 --- /dev/null +++ b/identity_iota_core/src/rebased/sui/types/number.rs @@ -0,0 +1,47 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; +use serde::Serialize; +use std::str::FromStr; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum Number { + Num(N), + Str(String), +} + +macro_rules! impl_conversions { + ($t:ty) => { + impl TryFrom> for $t { + type Error = <$t as FromStr>::Err; + fn try_from(value: Number<$t>) -> Result<$t, Self::Error> { + match value { + Number::Num(n) => Ok(n), + Number::Str(s) => s.parse(), + } + } + } + + impl From<$t> for Number<$t> { + fn from(value: $t) -> Number<$t> { + Number::Num(value) + } + } + }; +} + +impl_conversions!(u8); +impl_conversions!(u16); +impl_conversions!(u32); +impl_conversions!(u64); +impl_conversions!(u128); +impl_conversions!(usize); + +impl_conversions!(i8); +impl_conversions!(i16); +impl_conversions!(i32); +impl_conversions!(i64); +impl_conversions!(i128); +impl_conversions!(isize); diff --git a/identity_iota_core/src/rebased/transaction.rs b/identity_iota_core/src/rebased/transaction.rs new file mode 100644 index 0000000000..659b45d1e2 --- /dev/null +++ b/identity_iota_core/src/rebased/transaction.rs @@ -0,0 +1,106 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use async_trait::async_trait; +use iota_sdk::rpc_types::IotaTransactionBlockResponse; +use iota_sdk::types::transaction::ProgrammableTransaction; +use secret_storage::Signer; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IotaKeySignature; +use crate::rebased::Error; + +/// The output type of a [`Transaction`]. +#[derive(Debug, Clone)] +pub struct TransactionOutput { + /// The parsed Transaction output. See [`Transaction::Output`]. + pub output: T, + /// The "raw" transaction execution response received. + pub response: IotaTransactionBlockResponse, +} + +impl Deref for TransactionOutput { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.output + } +} + +/// Interface for operations that interact with the ledger through transactions. +#[async_trait] +pub trait Transaction: Sized { + /// The result of performing the operation. + type Output; + + /// Executes this operation using the given `client` and an optional `gas_budget`. + /// If no value for `gas_budget` is provided, an estimated value will be used. + async fn execute_with_opt_gas + Sync>( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error>; + + /// Executes this operation using `client`. + async fn execute + Sync>( + self, + client: &IdentityClient, + ) -> Result, Error> { + self.execute_with_opt_gas(None, client).await + } + + /// Executes this operation using `client` and a well defined `gas_budget`. + async fn execute_with_gas + Sync>( + self, + gas_budget: u64, + client: &IdentityClient, + ) -> Result, Error> { + self.execute_with_opt_gas(Some(gas_budget), client).await + } +} + +#[async_trait] +impl Transaction for ProgrammableTransaction { + type Output = (); + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + Sync, + { + let response = client.execute_transaction(self, gas_budget).await?; + Ok(TransactionOutput { output: (), response }) + } +} + +/// Interface to describe an operation that can eventually +/// be turned into a [`Transaction`], given the right input. +pub trait ProtoTransaction { + /// The input required by this operation. + type Input; + /// This operation's next state. Can either be another [`ProtoTransaction`] + /// or a whole [`Transaction`] ready to be executed. + type Tx: ProtoTransaction; + + /// Feed this operation with its required input, advancing its + /// state to another [`ProtoTransaction`] that may or may not + /// be ready for execution. + fn with(self, input: Self::Input) -> Self::Tx; +} + +// Every Transaction is a QuasiTransaction that requires no input +// and that has itself as its next state. +impl ProtoTransaction for T +where + T: Transaction, +{ + type Input = (); + type Tx = Self; + + fn with(self, _: Self::Input) -> Self::Tx { + self + } +} diff --git a/identity_iota_core/src/rebased/utils.rs b/identity_iota_core/src/rebased/utils.rs new file mode 100644 index 0000000000..57c1dce437 --- /dev/null +++ b/identity_iota_core/src/rebased/utils.rs @@ -0,0 +1,110 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context as _; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::TypeTag; +use serde::Serialize; +use tokio::process::Command; + +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::IotaClient; +use iota_sdk::IotaClientBuilder; + +use crate::rebased::Error; + +pub const LOCAL_NETWORK: &str = "http://127.0.0.1:9000"; + +pub async fn get_client(network: &str) -> Result { + let client = IotaClientBuilder::default() + .build(network) + .await + .map_err(|err| Error::Network(format!("failed to connect to {network}"), err))?; + + Ok(client) +} + +pub async fn request_funds(address: &IotaAddress) -> anyhow::Result<()> { + let output = Command::new("iota") + .arg("client") + .arg("faucet") + .arg("--address") + .arg(address.to_string()) + .arg("--url") + .arg("http://127.0.0.1:9123/gas") + .arg("--json") + .output() + .await + .context("Failed to execute command")?; + + if !output.status.success() { + anyhow::bail!( + "Failed to request funds from faucet: {}", + std::str::from_utf8(&output.stderr).unwrap() + ); + } + + Ok(()) +} + +pub trait MoveType: Serialize { + fn move_type(package: ObjectID) -> TypeTag; + + fn try_to_argument( + &self, + ptb: &mut ProgrammableTransactionBuilder, + _package: Option, + ) -> Result { + ptb.pure(self).map_err(|e| Error::InvalidArgument(e.to_string())) + } +} + +impl MoveType for u8 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U8 + } +} + +impl MoveType for u16 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U16 + } +} + +impl MoveType for u32 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U32 + } +} + +impl MoveType for u64 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U64 + } +} + +impl MoveType for u128 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U128 + } +} + +impl MoveType for bool { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::Bool + } +} + +impl MoveType for IotaAddress { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::Address + } +} + +impl MoveType for Vec { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::Vector(Box::new(T::move_type(package))) + } +} diff --git a/identity_iota_core/tests/e2e/asset.rs b/identity_iota_core/tests/e2e/asset.rs new file mode 100644 index 0000000000..1d7347b372 --- /dev/null +++ b/identity_iota_core/tests/e2e/asset.rs @@ -0,0 +1,244 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use crate::common::get_client as get_test_client; +use crate::common::TEST_DOC; +use crate::common::TEST_GAS_BUDGET; +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_credential::credential::CredentialBuilder; +use identity_credential::validator::FailFast; +use identity_credential::validator::JwtCredentialValidationOptions; +use identity_credential::validator::JwtCredentialValidator; +use identity_document::document::CoreDocument; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota_core::rebased::transaction::Transaction; +use identity_iota_core::rebased::utils::MoveType as _; +use identity_iota_core::rebased::AuthenticatedAsset; +use identity_iota_core::rebased::PublicAvailableVC; +use identity_iota_core::rebased::TransferProposal; +use identity_iota_core::IotaDID; +use identity_storage::JwkDocumentExt; +use identity_storage::JwsSignatureOptions; +use identity_verification::VerificationMethod; +use iota_sdk::types::TypeTag; +use itertools::Itertools as _; +use move_core_types::language_storage::StructTag; + +#[tokio::test] +async fn creating_authenticated_asset_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let alice_client = test_client.new_user_client().await?; + + let asset = alice_client + .create_authenticated_asset::(42) + .finish() + .execute(&alice_client) + .await? + .output; + assert_eq!(asset.content(), &42); + + Ok(()) +} + +#[tokio::test] +async fn transferring_asset_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let alice_client = test_client.new_user_client().await?; + let bob_client = test_client.new_user_client().await?; + + // Alice creates a new asset. + let asset = alice_client + .create_authenticated_asset::(42) + .transferable(true) + .finish() + .execute(&alice_client) + .await? + .output; + let asset_id = asset.id(); + + // Alice propose to Bob the transfer of the asset. + let proposal = asset + .transfer(bob_client.sender_address())? + .execute(&alice_client) + .await? + .output; + let proposal_id = proposal.id(); + // Bob accepts the transfer. + proposal.accept().execute(&bob_client).await?; + let TypeTag::Struct(asset_type) = AuthenticatedAsset::::move_type(test_client.package_id()) else { + unreachable!("asset is a struct"); + }; + let bob_owns_asset = bob_client + .find_owned_ref(*asset_type, |obj| obj.object_id == asset_id) + .await? + .is_some(); + assert!(bob_owns_asset); + + // Alice concludes the transfer. + let proposal = TransferProposal::get_by_id(proposal_id, &alice_client).await?; + assert!(proposal.is_concluded()); + proposal.conclude_or_cancel().execute(&alice_client).await?; + + // After the transfer is concluded all capabilities as well as the proposal bound to the transfer are deleted. + let alice_has_sender_cap = alice_client + .find_owned_ref( + StructTag::from_str(&format!("{}::asset::SenderCap", test_client.package_id()))?, + |_| true, + ) + .await? + .is_some(); + assert!(!alice_has_sender_cap); + let bob_has_recipient_cap = bob_client + .find_owned_ref( + StructTag::from_str(&format!("{}::asset::RecipientCap", test_client.package_id()))?, + |_| true, + ) + .await? + .is_some(); + assert!(!bob_has_recipient_cap); + + Ok(()) +} + +#[tokio::test] +async fn accepting_the_transfer_of_an_asset_requires_capability() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let alice_client = test_client.new_user_client().await?; + let bob_client = test_client.new_user_client().await?; + let caty_client = test_client.new_user_client().await?; + + // Alice creates a new asset. + let asset = alice_client + .create_authenticated_asset::(42) + .transferable(true) + .finish() + .execute(&alice_client) + .await? + .output; + + // Alice propose to Bob the transfer of the asset. + let proposal = asset + .transfer(bob_client.sender_address())? + .execute(&alice_client) + .await? + .output; + + // Caty attempts to accept the transfer instead of Bob but gets an error + let error = proposal.accept().execute(&caty_client).await.unwrap_err(); + assert!(matches!( + error, + identity_iota_core::rebased::Error::MissingPermission(_) + )); + + Ok(()) +} + +#[tokio::test] +async fn modifying_mutable_asset_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let alice_client = test_client.new_user_client().await?; + + let mut asset = alice_client + .create_authenticated_asset::(42) + .mutable(true) + .finish() + .execute(&alice_client) + .await? + .output; + + asset.set_content(420)?.execute(&alice_client).await?; + assert_eq!(asset.content(), &420); + + Ok(()) +} + +#[tokio::test] +async fn deleting_asset_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let alice_client = test_client.new_user_client().await?; + + let asset = alice_client + .create_authenticated_asset::(42) + .deletable(true) + .finish() + .execute(&alice_client) + .await? + .output; + let asset_id = asset.id(); + + asset.delete()?.execute(&alice_client).await?; + let alice_owns_asset = alice_client + .read_api() + .get_owned_objects(alice_client.sender_address(), None, None, None) + .await? + .data + .into_iter() + .map(|obj| obj.object_id().unwrap()) + .contains(&asset_id); + assert!(!alice_owns_asset); + + Ok(()) +} + +#[tokio::test] +async fn hosting_vc_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let newly_created_identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await? + .output; + let object_id = newly_created_identity.id(); + let did = { IotaDID::parse(format!("did:iota:{object_id}"))? }; + + test_client + .store_key_id_for_verification_method(identity_client.clone(), did.clone()) + .await?; + let did_doc = CoreDocument::builder(Object::default()) + .id(did.clone().into()) + .verification_method(VerificationMethod::new_from_jwk( + did.clone(), + identity_client.signer().public_key().clone(), + Some(identity_client.signer().key_id().as_str()), + )?) + .build()?; + let credential = CredentialBuilder::new(Object::default()) + .id(Url::parse("http://example.com/credentials/42")?) + .issuance_date(Timestamp::now_utc()) + .issuer(Url::parse(did.to_string())?) + .subject(serde_json::from_value(serde_json::json!({ + "id": did, + "type": ["VerifiableCredential", "ExampleCredential"], + "value": 3 + }))?) + .build()?; + let credential_jwt = did_doc + .create_credential_jwt( + &credential, + identity_client.signer().storage(), + identity_client.signer().key_id().as_str(), + &JwsSignatureOptions::default(), + None, + ) + .await?; + + let vc = PublicAvailableVC::new(credential_jwt.clone(), Some(TEST_GAS_BUDGET), &identity_client).await?; + assert_eq!(credential_jwt, vc.jwt()); + + let validator = JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); + validator.validate::<_, Object>( + &credential_jwt, + &did_doc, + &JwtCredentialValidationOptions::default(), + FailFast::FirstError, + )?; + + Ok(()) +} diff --git a/identity_iota_core/tests/e2e/client.rs b/identity_iota_core/tests/e2e/client.rs new file mode 100644 index 0000000000..515eeb2455 --- /dev/null +++ b/identity_iota_core/tests/e2e/client.rs @@ -0,0 +1,40 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::get_client as get_test_client; +use crate::common::TEST_DOC; +use identity_iota_core::rebased::migration; +use identity_iota_core::rebased::transaction::Transaction; + +#[tokio::test] +async fn can_create_an_identity() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let _identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute(&identity_client) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn can_resolve_a_new_identity() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let new_identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute(&identity_client) + .await? + .output; + + let identity = migration::get_identity(&identity_client, new_identity.id()).await?; + + assert!(identity.is_some()); + + Ok(()) +} \ No newline at end of file diff --git a/identity_iota_core/tests/e2e/common.rs b/identity_iota_core/tests/e2e/common.rs new file mode 100644 index 0000000000..de724abac0 --- /dev/null +++ b/identity_iota_core/tests/e2e/common.rs @@ -0,0 +1,313 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +#![allow(dead_code)] + +use std::io::Write; +use std::ops::Deref; +use std::sync::Arc; +use std::sync::LazyLock; + +use anyhow::anyhow; +use anyhow::Context; +use identity_iota_core::rebased::client::IdentityClient; +use identity_iota_core::rebased::client::IdentityClientReadOnly; +use identity_iota_core::rebased::client::IotaKeySignature; +use identity_iota_core::rebased::transaction::Transaction; +use identity_iota_core::rebased::utils::request_funds; +use identity_iota_core::IotaDID; +use identity_jose::jwk::Jwk; +use identity_jose::jws::JwsAlgorithm; +use identity_storage::JwkMemStore; +use identity_storage::JwkStorage; +use identity_storage::KeyId; +use identity_storage::KeyIdMemstore; +use identity_storage::KeyIdStorage; +use identity_storage::KeyType; +use identity_storage::MethodDigest; +use identity_storage::Storage; +use identity_storage::StorageSigner; +use identity_verification::VerificationMethod; +use iota_sdk::rpc_types::IotaTransactionBlockEffectsAPI; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::TypeTag; +use iota_sdk::types::IOTA_FRAMEWORK_PACKAGE_ID; +use iota_sdk::IotaClient; +use iota_sdk::IotaClientBuilder; +use jsonpath_rust::JsonPathQuery; +use move_core_types::ident_str; +use move_core_types::language_storage::StructTag; +use secret_storage::Signer; +use serde_json::Value; +use tokio::process::Command; +use tokio::sync::OnceCell; + +pub type MemStorage = Storage; +pub type MemSigner<'s> = StorageSigner<'s, JwkMemStore, KeyIdMemstore>; + +static PACKAGE_ID: OnceCell = OnceCell::const_new(); +static CLIENT: OnceCell = OnceCell::const_new(); +const SCRIPT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/"); +const CACHED_PKG_ID: &str = "/tmp/iota_identity_pkg_id.txt"; + +pub const TEST_GAS_BUDGET: u64 = 50_000_000; +pub const TEST_DOC: &[u8] = &[ + 68, 73, 68, 1, 0, 131, 1, 123, 34, 100, 111, 99, 34, 58, 123, 34, 105, 100, 34, 58, 34, 100, 105, 100, 58, 48, 58, + 48, 34, 44, 34, 118, 101, 114, 105, 102, 105, 99, 97, 116, 105, 111, 110, 77, 101, 116, 104, 111, 100, 34, 58, 91, + 123, 34, 105, 100, 34, 58, 34, 100, 105, 100, 58, 48, 58, 48, 35, 79, 115, 55, 95, 66, 100, 74, 120, 113, 86, 119, + 101, 76, 107, 56, 73, 87, 45, 76, 71, 83, 111, 52, 95, 65, 115, 52, 106, 70, 70, 86, 113, 100, 108, 74, 73, 99, 48, + 45, 100, 50, 49, 73, 34, 44, 34, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114, 34, 58, 34, 100, 105, 100, 58, 48, + 58, 48, 34, 44, 34, 116, 121, 112, 101, 34, 58, 34, 74, 115, 111, 110, 87, 101, 98, 75, 101, 121, 34, 44, 34, 112, + 117, 98, 108, 105, 99, 75, 101, 121, 74, 119, 107, 34, 58, 123, 34, 107, 116, 121, 34, 58, 34, 79, 75, 80, 34, 44, + 34, 97, 108, 103, 34, 58, 34, 69, 100, 68, 83, 65, 34, 44, 34, 107, 105, 100, 34, 58, 34, 79, 115, 55, 95, 66, 100, + 74, 120, 113, 86, 119, 101, 76, 107, 56, 73, 87, 45, 76, 71, 83, 111, 52, 95, 65, 115, 52, 106, 70, 70, 86, 113, 100, + 108, 74, 73, 99, 48, 45, 100, 50, 49, 73, 34, 44, 34, 99, 114, 118, 34, 58, 34, 69, 100, 50, 53, 53, 49, 57, 34, 44, + 34, 120, 34, 58, 34, 75, 119, 99, 54, 89, 105, 121, 121, 65, 71, 79, 103, 95, 80, 116, 118, 50, 95, 49, 67, 80, 71, + 52, 98, 86, 87, 54, 102, 89, 76, 80, 83, 108, 115, 57, 112, 122, 122, 99, 78, 67, 67, 77, 34, 125, 125, 93, 125, 44, + 34, 109, 101, 116, 97, 34, 58, 123, 34, 99, 114, 101, 97, 116, 101, 100, 34, 58, 34, 50, 48, 50, 52, 45, 48, 53, 45, + 50, 50, 84, 49, 50, 58, 49, 52, 58, 51, 50, 90, 34, 44, 34, 117, 112, 100, 97, 116, 101, 100, 34, 58, 34, 50, 48, 50, + 52, 45, 48, 53, 45, 50, 50, 84, 49, 50, 58, 49, 52, 58, 51, 50, 90, 34, 125, 125, +]; +pub static TEST_COIN_TYPE: LazyLock = LazyLock::new(|| "0x2::coin::Coin".parse().unwrap()); + +pub async fn get_client() -> anyhow::Result { + let client = IotaClientBuilder::default().build_localnet().await?; + let package_id = PACKAGE_ID.get_or_try_init(|| init(&client)).await.copied()?; + let address = get_active_address().await?; + + request_funds(&address).await?; + + let storage = Arc::new(Storage::new(JwkMemStore::new(), KeyIdMemstore::new())); + let identity_client = IdentityClientReadOnly::new(client, package_id).await?; + + Ok(TestClient { + client: identity_client, + package_id, + address, + storage, + }) +} + +async fn init(iota_client: &IotaClient) -> anyhow::Result { + let network_id = iota_client.read_api().get_chain_identifier().await?; + let address = get_active_address().await?; + + if let Ok(id) = std::env::var("IOTA_IDENTITY_PKG_ID").or(get_cached_id(&network_id).await) { + std::env::set_var("IOTA_IDENTITY_PKG_ID", id.clone()); + id.parse().context("failed to parse object id from str") + } else { + publish_package(address).await + } +} + +async fn get_cached_id(network_id: &str) -> anyhow::Result { + let cache = tokio::fs::read_to_string(CACHED_PKG_ID).await?; + let (cached_id, cached_network_id) = cache.split_once(';').ok_or(anyhow!("Invalid or empty cached data"))?; + + if cached_network_id == network_id { + Ok(cached_id.to_owned()) + } else { + Err(anyhow!("A network change has invalidated the cached data")) + } +} + +async fn get_active_address() -> anyhow::Result { + Command::new("iota") + .arg("client") + .arg("active-address") + .arg("--json") + .output() + .await + .context("Failed to execute command") + .and_then(|output| Ok(serde_json::from_slice::(&output.stdout)?)) +} + +async fn publish_package(active_address: IotaAddress) -> anyhow::Result { + let output = Command::new("sh") + .current_dir(SCRIPT_DIR) + .arg("publish_identity_package.sh") + .output() + .await?; + + if !output.status.success() { + anyhow::bail!( + "Failed to publish move package: \n\n{}\n\n{}", + std::str::from_utf8(&output.stdout).unwrap(), + std::str::from_utf8(&output.stderr).unwrap() + ); + } + + let publish_result = { + let output_str = std::str::from_utf8(&output.stdout).unwrap(); + let start_of_json = output_str.find('{').ok_or(anyhow!("No json in output"))?; + serde_json::from_str::(output_str[start_of_json..].trim())? + }; + + let package_id = publish_result + .path("$.objectChanges[?(@.type == 'published')].packageId") + .map_err(|e| anyhow!("Failed to parse JSONPath: {e}")) + .and_then(|value| Ok(serde_json::from_value::>(value)?))? + .first() + .copied() + .ok_or_else(|| anyhow!("Failed to parse package ID after publishing"))?; + + // Persist package ID in order to avoid publishing the package for every test. + let package_id_str = package_id.to_string(); + std::env::set_var("IDENTITY_IOTA_PKG_ID", package_id_str.as_str()); + let mut file = std::fs::File::create(CACHED_PKG_ID)?; + write!(&mut file, "{};{}", package_id_str, active_address)?; + + Ok(package_id) +} + +pub async fn get_key_data() -> Result<(Storage, KeyId, Jwk, Vec), anyhow::Error> { + let storage = Storage::::new(JwkMemStore::new(), KeyIdMemstore::new()); + let generate = storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await?; + let public_key_jwk = generate.jwk.to_public().expect("public components should be derivable"); + let public_key_bytes = get_public_key_bytes(&public_key_jwk)?; + // let sender_address = convert_to_address(&public_key_bytes)?; + + Ok((storage, generate.key_id, public_key_jwk, public_key_bytes)) +} + +fn get_public_key_bytes(sender_public_jwk: &Jwk) -> Result, anyhow::Error> { + let public_key_base_64 = &sender_public_jwk + .try_okp_params() + .map_err(|err| anyhow!("key not of type `Okp`; {err}"))? + .x; + + identity_jose::jwu::decode_b64(public_key_base_64).map_err(|err| anyhow!("could not decode base64 public key; {err}")) +} + +#[derive(Clone)] +pub struct TestClient { + client: IdentityClientReadOnly, + package_id: ObjectID, + #[allow(dead_code)] + address: IotaAddress, + storage: Arc, +} + +impl Deref for TestClient { + type Target = IdentityClientReadOnly; + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl TestClient { + // Sets the current address to the address controller by this client. + async fn switch_address(&self) -> anyhow::Result<()> { + let output = Command::new("iota") + .arg("client") + .arg("switch") + .arg("--address") + .arg(self.address.to_string()) + .output() + .await?; + + if !output.status.success() { + anyhow::bail!( + "Failed to switch address: {}", + std::str::from_utf8(&output.stderr).unwrap() + ); + } + + Ok(()) + } + + pub fn package_id(&self) -> ObjectID { + self.package_id + } + + pub async fn new_address(&self) -> anyhow::Result { + let output = Command::new("iota") + .arg("client") + .arg("new-address") + .arg("ed25519") + .arg("--json") + .output() + .await?; + let new_address = { + let output_str = std::str::from_utf8(&output.stdout).unwrap(); + let start_of_json = output_str.find('{').ok_or(anyhow!("No json in output"))?; + let json_result = serde_json::from_str::(output_str[start_of_json..].trim())?; + let address_json = json_result + .path("$.address") + .map_err(|e| anyhow!("failed to parse json output: {e}"))?; + serde_json::from_value::(address_json)? + }; + + request_funds(&new_address).await?; + + let mut new_client = self.clone(); + new_client.address = new_address; + Ok(new_client) + } + + pub async fn new_user_client(&self) -> anyhow::Result> { + let generate = self + .storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await?; + let public_key_jwk = generate.jwk.to_public().expect("public components should be derivable"); + let signer = StorageSigner::new(&self.storage, generate.key_id, public_key_jwk); + + let user_client = IdentityClient::new(self.client.clone(), signer).await?; + + request_funds(&user_client.sender_address()).await?; + + Ok(user_client) + } + + pub async fn store_key_id_for_verification_method( + &self, + identity_client: IdentityClient>, + did: IotaDID, + ) -> anyhow::Result<()> { + let public_key = identity_client.signer().public_key(); + let key_id = identity_client.signer().key_id(); + let fragment = key_id.as_str(); + let method = VerificationMethod::new_from_jwk(did, public_key.clone(), Some(fragment))?; + let method_digest: MethodDigest = MethodDigest::new(&method)?; + + self + .storage + .key_id_storage() + .insert_key_id(method_digest, key_id.clone()) + .await?; + + Ok(()) + } +} + +pub async fn get_test_coin(recipient: IotaAddress, client: &IdentityClient) -> anyhow::Result +where + S: Signer + Sync, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + let coin = ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("coin").into(), + ident_str!("zero").into(), + vec![TypeTag::Bool], + vec![], + ); + ptb.transfer_args(recipient, vec![coin]); + ptb + .finish() + .execute(client) + .await? + .response + .effects + .expect("tx should have had effects") + .created() + .first() + .map(|obj| obj.object_id()) + .context("no coins were created") +} diff --git a/identity_iota_core/tests/e2e/identity.rs b/identity_iota_core/tests/e2e/identity.rs new file mode 100644 index 0000000000..7075128d2f --- /dev/null +++ b/identity_iota_core/tests/e2e/identity.rs @@ -0,0 +1,357 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use crate::common; +use crate::common::get_client as get_test_client; +use crate::common::get_key_data; +use crate::common::TEST_COIN_TYPE; +use crate::common::TEST_DOC; +use crate::common::TEST_GAS_BUDGET; +use identity_iota_core::rebased::client::get_object_id_from_did; +use identity_iota_core::rebased::migration::has_previous_version; +use identity_iota_core::rebased::migration::Identity; +use identity_iota_core::rebased::proposals::ProposalResult; +use identity_iota_core::rebased::proposals::ProposalT as _; +use identity_iota_core::rebased::transaction::Transaction; +use identity_iota_core::IotaDID; +use identity_iota_core::IotaDocument; +use identity_verification::MethodScope; +use identity_verification::VerificationMethod; +use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::types::base_types::SequenceNumber; +use iota_sdk::types::TypeTag; +use iota_sdk::types::IOTA_FRAMEWORK_PACKAGE_ID; +use move_core_types::ident_str; +use move_core_types::language_storage::StructTag; + +#[tokio::test] +async fn identity_deactivation_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute(&identity_client) + .await? + .output; + + identity + .deactivate_did() + .finish(&identity_client) + .await? + .execute(&identity_client) + .await?; + + assert!(identity.metadata.deactivated == Some(true)); + + Ok(()) +} + +#[tokio::test] +async fn updating_onchain_identity_did_doc_with_single_controller_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut newly_created_identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await? + .output; + + let updated_did_doc = { + let did = IotaDID::parse(format!("did:iota:{}", newly_created_identity.id()))?; + let mut doc = IotaDocument::new_with_id(did.clone()); + doc.insert_method( + VerificationMethod::new_from_jwk( + did, + identity_client.signer().public_key().clone(), + Some(identity_client.signer().key_id().as_str()), + )?, + MethodScope::VerificationMethod, + )?; + doc + }; + + newly_created_identity + .update_did_document(updated_did_doc) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn approving_proposal_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let alice_client = test_client.new_user_client().await?; + let bob_client = test_client.new_user_client().await?; + + let mut identity = alice_client + .create_identity(TEST_DOC) + .controller(alice_client.sender_address(), 1) + .controller(bob_client.sender_address(), 1) + .threshold(2) + .finish() + .execute(&alice_client) + .await? + .output; + + let did_doc = { + let did = IotaDID::parse(format!("did:iota:{}", identity.id()))?; + let mut doc = IotaDocument::new_with_id(did.clone()); + doc.insert_method( + VerificationMethod::new_from_jwk( + did, + alice_client.signer().public_key().clone(), + Some(alice_client.signer().key_id().as_str()), + )?, + MethodScope::VerificationMethod, + )?; + doc + }; + let ProposalResult::Pending(mut proposal) = identity + .update_did_document(did_doc) + .finish(&alice_client) + .await? + .execute(&alice_client) + .await? + .output + else { + anyhow::bail!("the proposal is executed"); + }; + + proposal.approve(&identity).execute(&bob_client).await?; + + assert_eq!(proposal.votes(), 2); + + Ok(()) +} + +#[tokio::test] +async fn adding_controller_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let alice_client = test_client.new_user_client().await?; + let bob_client = test_client.new_user_client().await?; + + let mut identity = alice_client + .create_identity(TEST_DOC) + .finish() + .execute(&alice_client) + .await? + .output; + + // Alice proposes to add Bob as a controller. Since Alice has enough voting power the proposal + // is executed directly after creation. + identity + .update_config() + .add_controller(bob_client.sender_address(), 1) + .finish(&alice_client) + .await? + .execute(&alice_client) + .await?; + + let cap = bob_client + .find_owned_ref( + StructTag::from_str(&format!("{}::multicontroller::ControllerCap", test_client.package_id())).unwrap(), + |_| true, + ) + .await?; + + assert!(cap.is_some()); + + Ok(()) +} + +#[tokio::test] +async fn can_get_historical_identity_data() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut newly_created_identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await? + .output; + + let did = IotaDID::parse(format!("did:iota:{}", newly_created_identity.id()))?; + let updated_did_doc = { + let mut doc = IotaDocument::new_with_id(did.clone()); + let (_, key_id, public_key_jwk, _) = get_key_data().await?; + doc.insert_method( + VerificationMethod::new_from_jwk(did.clone(), public_key_jwk, Some(key_id.as_str()))?, + MethodScope::VerificationMethod, + )?; + doc + }; + + newly_created_identity + .update_did_document(updated_did_doc) + .finish(&identity_client) + .await? + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await?; + + let Identity::FullFledged(updated_identity) = identity_client.get_identity(get_object_id_from_did(&did)?).await? + else { + anyhow::bail!("resolved identity should be an onchain identity"); + }; + + let history = updated_identity.get_history(&identity_client, None, None).await?; + + // test check for previous version + let has_previous_version_responses: Vec = history + .iter() + .map(has_previous_version) + .collect::, identity_iota_core::rebased::Error>>()?; + assert_eq!(has_previous_version_responses, vec![true, false]); + + let versions: Vec = history.iter().map(|elem| elem.version).collect(); + let version_numbers: Vec = versions.iter().map(|v| (*v).into()).collect(); + let oldest_version: usize = *version_numbers.last().unwrap(); + let version_diffs: Vec = version_numbers.iter().map(|v| v - oldest_version).collect(); + assert_eq!(version_diffs, vec![1, 0],); + + // paging: + // you can either loop until no result is returned + let mut result_index = 0; + let mut current_item: Option<&IotaObjectData> = None; + let mut history: Vec; + loop { + history = updated_identity + .get_history(&identity_client, current_item, Some(1)) + .await?; + if history.is_empty() { + break; + } + current_item = history.first(); + assert_eq!(current_item.unwrap().version, *versions.get(result_index).unwrap()); + result_index += 1; + } + + // or check before fetching next page + let mut result_index = 0; + let mut current_item: Option<&IotaObjectData> = None; + let mut history: Vec; + loop { + history = updated_identity + .get_history(&identity_client, current_item, Some(1)) + .await?; + + current_item = history.first(); + assert_eq!(current_item.unwrap().version, *versions.get(result_index).unwrap()); + result_index += 1; + + if !has_previous_version(current_item.unwrap())? { + break; + } + } + + Ok(()) +} + +#[tokio::test] +async fn send_proposal_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await? + .output; + let identity_address = identity.id().into(); + + // Let's give the identity 2 coins in order to have something to move. + let coin1 = common::get_test_coin(identity_address, &identity_client).await?; + let coin2 = common::get_test_coin(identity_address, &identity_client).await?; + + // Let's propose the send of those two caps to the identity_client's address. + let ProposalResult::Pending(send_proposal) = identity + .send_assets() + .object(coin1, identity_client.sender_address()) + .object(coin2, identity_client.sender_address()) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await? + .output + else { + panic!("send proposal cannot be chain-executed!"); + }; + + send_proposal + .into_tx(&mut identity, &identity_client) + .await? + .execute(&identity_client) + .await?; + + // Assert that identity_client's address now owns those coins. + identity_client + .find_owned_ref(TEST_COIN_TYPE.clone(), |obj| obj.object_id == coin1) + .await? + .expect("coin1 was transfered to this address"); + + identity_client + .find_owned_ref(TEST_COIN_TYPE.clone(), |obj| obj.object_id == coin2) + .await? + .expect("coin2 was transfered to this address"); + + Ok(()) +} + +#[tokio::test] +async fn borrow_proposal_works() -> anyhow::Result<()> { + let test_client = get_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(TEST_DOC) + .finish() + .execute(&identity_client) + .await? + .output; + let identity_address = identity.id().into(); + + let coin1 = common::get_test_coin(identity_address, &identity_client).await?; + let coin2 = common::get_test_coin(identity_address, &identity_client).await?; + + // Let's propose the borrow of those two coins to the identity_client's address. + let ProposalResult::Pending(borrow_proposal) = identity + .borrow_assets() + .borrow(coin1) + .borrow(coin2) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await? + .output + else { + panic!("borrow proposal cannot be chain-executed!"); + }; + + borrow_proposal + .into_tx(&mut identity, &identity_client) + .await? + // this doesn't really do anything but if it doesn't fail it means coin1 was properly borrowed. + .with_intent(move |ptb, objs| { + ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("coin").into(), + ident_str!("value").into(), + vec![TypeTag::Bool], + vec![objs.get(&coin1).expect("coin1 data is borrowed").0], + ); + }) + .execute(&identity_client) + .await?; + + Ok(()) +} diff --git a/identity_iota_core/tests/e2e/main.rs b/identity_iota_core/tests/e2e/main.rs new file mode 100644 index 0000000000..47cff143e4 --- /dev/null +++ b/identity_iota_core/tests/e2e/main.rs @@ -0,0 +1,8 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod asset; +mod client; +pub mod common; +mod identity; +mod migration; diff --git a/identity_iota_core/tests/e2e/migration.rs b/identity_iota_core/tests/e2e/migration.rs new file mode 100644 index 0000000000..7bdcb4210d --- /dev/null +++ b/identity_iota_core/tests/e2e/migration.rs @@ -0,0 +1,17 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::get_client; +use identity_iota_core::rebased::migration; +use iota_sdk::types::base_types::ObjectID; + +#[tokio::test] +async fn migration_registry_is_found() -> anyhow::Result<()> { + let client = get_client().await?; + let random_alias_id = ObjectID::random(); + + let doc = migration::lookup(&client, random_alias_id).await?; + assert!(doc.is_none()); + + Ok(()) +} diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index 73a7fa3cdb..8da1e2ec5a 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -18,14 +18,12 @@ iota-crypto = { version = "0.23.2", default-features = false, features = ["std", json-proof-token.workspace = true serde.workspace = true serde_json = { version = "1.0", default-features = false, features = ["std"] } -subtle = { version = "2.5", default-features = false } thiserror.workspace = true zeroize = { version = "1.6", default-features = false, features = ["std", "zeroize_derive"] } [dev-dependencies] -anyhow = "1" -iota-crypto = { version = "0.23.2", features = ["ed25519", "random", "hmac"] } -p256 = { version = "0.12.0", default-features = false, features = ["std", "ecdsa", "ecdsa-core"] } +iota-crypto = { version = "0.23", features = ["ed25519", "random", "hmac"] } +p256 = { version = "0.13.0", default-features = false, features = ["std", "ecdsa", "ecdsa-core"] } signature = { version = "2", default-features = false } [[example]] diff --git a/identity_jose/src/error.rs b/identity_jose/src/error.rs index 9a4afd4cb1..5d8a77367f 100644 --- a/identity_jose/src/error.rs +++ b/identity_jose/src/error.rs @@ -41,7 +41,7 @@ pub enum Error { #[error("attempt to parse an unregistered jws algorithm")] JwsAlgorithmParsingError, /// Caused by an error during signature verification. - #[error("signature verification error")] + #[error("signature verification error; {0}")] SignatureVerificationError(#[source] crate::jws::SignatureVerificationError), /// Caused by a mising header. #[error("missing header")] diff --git a/identity_jose/src/tests/es256.rs b/identity_jose/src/tests/es256.rs index 7b8e343728..b6a2c68339 100644 --- a/identity_jose/src/tests/es256.rs +++ b/identity_jose/src/tests/es256.rs @@ -24,7 +24,7 @@ pub(crate) fn expand_p256_jwk(jwk: &Jwk) -> (SecretKey, PublicKey) { } let sk_bytes = params.d.as_ref().map(jwu::decode_b64).unwrap().unwrap(); - let sk = SecretKey::from_be_bytes(&sk_bytes).unwrap(); + let sk = SecretKey::from_slice(&sk_bytes).unwrap(); // Transformation according to section 2.3.3 from http://www.secg.org/sec1-v2.pdf. let pk_bytes: Vec = [0x04] diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index d99158835d..8346aede14 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -40,6 +40,7 @@ default = ["revocation-bitmap", "iota"] revocation-bitmap = ["identity_credential/revocation-bitmap", "identity_iota_core?/revocation-bitmap"] # Enables the IOTA integration for the resolver. iota = ["dep:identity_iota_core"] +kinesis = ["identity_iota_core/kinesis-client"] [lints] workspace = true diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index 228a65582b..caf216b387 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -264,7 +264,7 @@ impl + 'static> Resolver> { } } -#[cfg(feature = "iota")] +#[cfg(any(feature = "iota", feature = "kinesis"))] mod iota_handler { use crate::ErrorCause; @@ -272,81 +272,170 @@ mod iota_handler { use identity_document::document::CoreDocument; use identity_iota_core::IotaDID; use identity_iota_core::IotaDocument; - use identity_iota_core::IotaIdentityClientExt; - use std::collections::HashMap; use std::sync::Arc; - impl Resolver - where - DOC: From + AsRef + 'static, - { - /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs. - /// - /// See also [`attach_handler`](Self::attach_handler). - pub fn attach_iota_handler(&mut self, client: CLI) + #[cfg(feature = "iota")] + mod iota_specific { + use identity_iota_core::IotaIdentityClientExt; + use std::collections::HashMap; + + use super::*; + + impl Resolver where - CLI: IotaIdentityClientExt + Send + Sync + 'static, + DOC: From + AsRef + 'static, { - let arc_client: Arc = Arc::new(client); - - let handler = move |did: IotaDID| { - let future_client = arc_client.clone(); - async move { future_client.resolve_did(&did).await } - }; + /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs. + /// + /// See also [`attach_handler`](Self::attach_handler). + pub fn attach_iota_handler(&mut self, client: CLI) + where + CLI: IotaIdentityClientExt + Send + Sync + 'static, + { + let arc_client: Arc = Arc::new(client); + + let handler = move |did: IotaDID| { + let future_client = arc_client.clone(); + async move { future_client.resolve_did(&did).await } + }; + + self.attach_handler(IotaDID::METHOD.to_owned(), handler); + } - self.attach_handler(IotaDID::METHOD.to_owned(), handler); + /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs + /// on multiple networks. + /// + /// + /// # Arguments + /// + /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its + /// corresponding client. + /// + /// # Examples + /// + /// ```ignore + /// // Assume `smr_client` and `iota_client` are instances IOTA clients `iota_sdk::client::Client`. + /// attach_multiple_iota_handlers(vec![("smr", smr_client), ("iota", iota_client)]); + /// ``` + /// + /// # See Also + /// - [`attach_handler`](Self::attach_handler). + /// + /// # Note + /// + /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added + /// clients. + /// - This function does not validate the provided configuration. Ensure that the provided network name + /// corresponds with the client, possibly by using `client.network_name()`. + pub fn attach_multiple_iota_handlers(&mut self, clients: I) + where + CLI: IotaIdentityClientExt + Send + Sync + 'static, + I: IntoIterator, + { + let arc_clients = Arc::new(clients.into_iter().collect::>()); + + let handler = move |did: IotaDID| { + let future_client = arc_clients.clone(); + async move { + let did_network = did.network_str(); + let client: &CLI = + future_client + .get(did_network) + .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( + did_network.to_string(), + )))?; + client + .resolve_did(&did) + .await + .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) + } + }; + + self.attach_handler(IotaDID::METHOD.to_owned(), handler); + } } + } + + #[cfg(feature = "kinesis")] + mod kinesis_specific { + use std::collections::HashMap; + + use identity_iota_core::rebased::client::IdentityClientReadOnly; + + use super::*; - /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs - /// on multiple networks. - /// - /// - /// # Arguments - /// - /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its - /// corresponding client. - /// - /// # Examples - /// - /// ```ignore - /// // Assume `smr_client` and `iota_client` are instances IOTA clients `iota_sdk::client::Client`. - /// attach_multiple_iota_handlers(vec![("smr", smr_client), ("iota", iota_client)]); - /// ``` - /// - /// # See Also - /// - [`attach_handler`](Self::attach_handler). - /// - /// # Note - /// - /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added - /// clients. - /// - This function does not validate the provided configuration. Ensure that the provided network name corresponds - /// with the client, possibly by using `client.network_name()`. - pub fn attach_multiple_iota_handlers(&mut self, clients: I) + impl Resolver where - CLI: IotaIdentityClientExt + Send + Sync + 'static, - I: IntoIterator, + DOC: From + AsRef + 'static, { - let arc_clients = Arc::new(clients.into_iter().collect::>()); - - let handler = move |did: IotaDID| { - let future_client = arc_clients.clone(); - async move { - let did_network = did.network_str(); - let client: &CLI = - future_client - .get(did_network) - .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( - did_network.to_string(), - )))?; - client - .resolve_did(&did) - .await - .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) - } - }; - - self.attach_handler(IotaDID::METHOD.to_owned(), handler); + /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs via kinesis. + /// + /// See also [`attach_handler`](Self::attach_handler) + pub fn attach_kinesis_iota_handler(&mut self, client: IdentityClientReadOnly) { + let arc_client = Arc::new(client); + + let handler = move |did: IotaDID| { + let future_client = arc_client.clone(); + async move { future_client.resolve_did(&did).await } + }; + + self.attach_handler(IotaDID::METHOD.to_owned(), handler); + } + + /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs + /// on multiple networks. + /// + /// + /// # Arguments + /// + /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its + /// corresponding client. + /// + /// # Examples + /// + /// ```ignore + /// // Assume `testnet_client` and `iota_client` are instances IOTA clients `iota_sdk::client::Client`. + /// attach_multiple_iota_handlers(vec![("testnet", testnet_client), ("iota", iota_client)]); + /// ``` + /// + /// # See Also + /// - [`attach_handler`](Self::attach_handler). + /// + /// # Note + /// + /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added + /// clients. + /// - This function does not validate the provided configuration. Ensure that the provided network name + /// corresponds with the client, possibly by using `client.network_name()`. + pub fn attach_multiple_kinesis_iota_handlers(&mut self, clients: I) + where + I: IntoIterator, + { + let arc_clients = Arc::new( + clients + .into_iter() + .collect::>(), + ); + + let handler = move |did: IotaDID| { + let future_client = arc_clients.clone(); + async move { + let did_network = did.network_str(); + let client: &IdentityClientReadOnly = + future_client + .get(did_network) + .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( + did_network.to_string(), + )))?; + client + .resolve_did(&did) + .await + .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) + } + }; + + self.attach_handler(IotaDID::METHOD.to_owned(), handler); + } } } } diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 5331dc725f..8ea31b0c85 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -12,20 +12,21 @@ rust-version.workspace = true description = "Abstractions over storage for cryptographic keys used in DID Documents" [dependencies] -anyhow = "1.0.82" +anyhow = { version = "1.0.82" } async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } futures = { version = "0.3.27", default-features = false, features = ["async-await"] } identity_core = { version = "=1.4.0", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.4.0", path = "../identity_credential", default-features = false, features = ["credential", "presentation", "revocation-bitmap"] } +identity_credential = { version = "=1.4.0", path = "../identity_credential", default-features = false, features = ["credential", "presentation"] } identity_did = { version = "=1.4.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.4.0", path = "../identity_document", default-features = false } identity_iota_core = { version = "=1.4.0", path = "../identity_iota_core", default-features = false, optional = true } identity_verification = { version = "=1.4.0", path = "../identity_verification", default-features = false } -iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519", "random"], optional = true } +iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default-features = false } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", branch = "main", optional = true } serde.workspace = true serde_json.workspace = true thiserror.workspace = true @@ -43,9 +44,11 @@ default = ["iota-document", "memstore"] # Exposes in-memory implementations of the storage traits intended exclusively for testing. memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto"] # Enables `Send` + `Sync` bounds for the storage traits. -send-sync-storage = [] +send-sync-storage = ["identity_iota_core?/send-sync-client-ext"] # Implements the JwkStorageDocumentExt trait for IotaDocument iota-document = ["dep:identity_iota_core"] +# enables support to sign via storage +storage-signer = ["identity_iota_core?/kinesis-client", "dep:secret-storage"] # Enables JSON Proof Token & BBS+ related features jpt-bbs-plus = [ "identity_credential/jpt-bbs-plus", diff --git a/identity_storage/src/storage/mod.rs b/identity_storage/src/storage/mod.rs index 7643c41a95..a1fff2c089 100644 --- a/identity_storage/src/storage/mod.rs +++ b/identity_storage/src/storage/mod.rs @@ -12,6 +12,8 @@ mod signature_options; #[cfg(feature = "jpt-bbs-plus")] mod timeframe_revocation_ext; +#[cfg(feature = "storage-signer")] +mod storage_signer; #[cfg(all(test, feature = "memstore"))] pub(crate) mod tests; @@ -21,6 +23,8 @@ pub use jwk_document_ext::*; #[cfg(feature = "jpt-bbs-plus")] pub use jwp_document_ext::*; pub use signature_options::*; +#[cfg(feature = "storage-signer")] +pub use storage_signer::*; #[cfg(feature = "jpt-bbs-plus")] pub use timeframe_revocation_ext::*; diff --git a/identity_storage/src/storage/storage_signer.rs b/identity_storage/src/storage/storage_signer.rs new file mode 100644 index 0000000000..13ab3a1ecc --- /dev/null +++ b/identity_storage/src/storage/storage_signer.rs @@ -0,0 +1,93 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use identity_iota_core::rebased::client::IotaKeySignature; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParams; +use identity_verification::jwu; +use secret_storage::Error as SecretStorageError; +use secret_storage::SignatureScheme; +use secret_storage::Signer; + +use crate::JwkStorage; +use crate::KeyId; +use crate::KeyIdStorage; +use crate::KeyStorageErrorKind; +use crate::Storage; + +/// Signer that offers signing capabilities for `Signer` trait from `secret_storage`. +/// `Storage` is used to sign. +pub struct StorageSigner<'a, K, I> { + key_id: KeyId, + public_key: Jwk, + storage: &'a Storage, +} + +impl<'a, K, I> Clone for StorageSigner<'a, K, I> { + fn clone(&self) -> Self { + StorageSigner { + key_id: self.key_id.clone(), + public_key: self.public_key.clone(), + storage: self.storage, + } + } +} + +impl<'a, K, I> StorageSigner<'a, K, I> { + /// Creates new `StorageSigner` with reference to a `Storage` instance. + pub fn new(storage: &'a Storage, key_id: KeyId, public_key: Jwk) -> Self { + Self { + key_id, + public_key, + storage, + } + } + + /// Returns a reference to the [`KeyId`] of the key used by this [`Signer`]. + pub fn key_id(&self) -> &KeyId { + &self.key_id + } + + /// Returns this [`Signer`]'s public key as [`Jwk`]. + pub fn public_key(&self) -> &Jwk { + &self.public_key + } + + /// Returns a reference to this [`Signer`]'s [`Storage`]. + pub fn storage(&self) -> &Storage { + self.storage + } +} + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl<'a, K, I> Signer for StorageSigner<'a, K, I> +where + K: JwkStorage + Sync, + I: KeyIdStorage + Sync, +{ + type KeyId = KeyId; + fn key_id(&self) -> &KeyId { + &self.key_id + } + async fn public_key(&self) -> Result<::PublicKey, SecretStorageError> { + match self.public_key.params() { + JwkParams::Okp(params) => jwu::decode_b64(¶ms.x) + .map_err(|e| SecretStorageError::Other(anyhow::anyhow!("could not base64 decode key {}; {e}", self.key_id()))), + _ => todo!("add support for other key types"), + } + } + async fn sign(&self, data: &[u8]) -> Result<::Signature, SecretStorageError> { + self + .storage + .key_storage() + .sign(&self.key_id, data, &self.public_key) + .await + .map_err(|e| match e.kind() { + KeyStorageErrorKind::KeyNotFound => SecretStorageError::KeyNotFound(e.to_string()), + KeyStorageErrorKind::RetryableIOFailure => SecretStorageError::StoreDisconnected(e.to_string()), + _ => SecretStorageError::Other(anyhow::anyhow!(e)), + }) + } +}