Skip to content

Commit

Permalink
Re-add Resolver API for WASM builds (#1526)
Browse files Browse the repository at this point in the history
* add 2_resolve_did example

* add custom resolver example

* add include custom resolution example in main

* fix outdated wording in examples

* update examples to use a more natural order of calls, remove unused variables

* update resolver logic

- resolver can be created with read-only and with write capable clients
- `IotaDocument` now serialize to full serialized `IotaDocument` intead of just the `CoreDocument` part when calling `.to_json()`/`.toJSON()`
- this affects the documents resolved via `Resolver` as well
- `Resolver` class now accepts generic type parameter to specify the type of the resolved documents

* fix missing imports in examples

* update `actions/cache` version

see https://github.blog/changelog/2024-12-05-notice-of-upcoming-releases-and-breaking-changes-for-github-actions/#actions-cache-v1-v2-and-actions-toolkit-cache-package-closing-down

* Update bindings/wasm/identity_wasm/examples/src/0_basic/2_resolve_did.ts

Co-authored-by: Enrico Marconi <[email protected]>

* fix typo found in review

---------

Co-authored-by: Enrico Marconi <[email protected]>
  • Loading branch information
wulfraem and UMR1352 authored Feb 19, 2025
1 parent 634d5be commit d679051
Show file tree
Hide file tree
Showing 21 changed files with 220 additions and 101 deletions.
6 changes: 3 additions & 3 deletions .github/actions/rust/rust-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ runs:
rustup show
- name: Cache cargo
uses: actions/cache@v2.1.7
uses: actions/cache@v4
if: inputs.cargo-cache-enabled == 'true'
with:
# https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci
Expand All @@ -115,7 +115,7 @@ runs:
shell: bash

- name: Cache build target
uses: actions/cache@v2.1.7
uses: actions/cache@v4
if: inputs.target-cache-enabled == 'true'
with:
path: ${{ inputs.target-cache-path }}
Expand All @@ -127,7 +127,7 @@ runs:
${{ inputs.os }}-target-
- name: Cache sccache
uses: actions/cache@v2.1.7
uses: actions/cache@v4
if: inputs.sccache-enabled == 'true'
with:
path: ${{ inputs.sccache-path }}
Expand Down
4 changes: 0 additions & 4 deletions bindings/wasm/identity_wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,3 @@ empty_docs = "allow"
[features]
default = ["dummy-client"]
dummy-client = ["dep:iota_interaction_ts"]
# As identity_iota::resolver is currently not available for wasm32 this temporary flag is used
# to switch of all code depending on identity_iota::resolver
# TODO: Remove this feature flag after identity_iota::resolver is available for wasm32 platforms
wasm-resolver = []
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@ export async function createIdentity(): Promise<void> {
const iotaClient = new IotaClient({ url: NETWORK_URL });
const network = await iotaClient.getChainIdentifier();

const storage = getMemstorage();
// TODO: check if we can update storage implementation to a non-owning variant
// order is important here as wrapped storage will be set to a null pointer after passing it to a client
const [unpublished] = await createDocumentForNetwork(storage, network);
// create new client that offers identity related functions
const storage = getMemstorage();
const identityClient = await getClientAndCreateAccount(storage);

// create new unpublished document
const [unpublished] = await createDocumentForNetwork(storage, network);
console.log(`Unpublished DID document: ${JSON.stringify(unpublished, null, 2)}`);
let did: IotaDID;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export async function updateIdentity() {
const iotaClient = new IotaClient({ url: NETWORK_URL });
const network = await iotaClient.getChainIdentifier();
const storage = getMemstorage();
const [unpublished, vmFragment1] = await createDocumentForNetwork(storage, network);
const identityClient = await getClientAndCreateAccount(storage);
const [unpublished, vmFragment1] = await createDocumentForNetwork(storage, network);

// create new identity for this account and publish document for it
const { output: identity } = await identityClient
Expand Down
76 changes: 52 additions & 24 deletions bindings/wasm/identity_wasm/examples/src/0_basic/2_resolve_did.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,84 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

// TODO:
// - [ ] clarify if we need/want a resolver example
// - [ ] clarify if we need/want the AliasOutput -> ObjectID example


import {
CoreDocument,
DIDJwk,
IdentityClientReadOnly,
IotaDID,
IotaDocument,
IotaIdentityClient,
IToCoreDocument,
JwkMemStore,
KeyIdMemStore,
Resolver,
Storage,
} from "@iota/identity-wasm/node";
import { AliasOutput, } from "@iota/sdk-wasm/node";
import { API_ENDPOINT, createDid } from "../util";
import { createDidDocument, getClientAndCreateAccount, getMemstorage } from "../utils_alpha";
import { IotaClient } from "@iota/iota-sdk/client";
import {
createDocumentForNetwork,
getClientAndCreateAccount,
getMemstorage,
IDENTITY_IOTA_PACKAGE_ID,
NETWORK_URL,
} from '../utils_alpha';

const DID_JWK: string =
"did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9";

/** Demonstrates how to resolve an existing DID in an Alias Output. */
export async function resolveIdentity() {
// create new client to interact with chain and get funded account with keys
// create new clients and create new account
const iotaClient = new IotaClient({ url: NETWORK_URL });
const network = await iotaClient.getChainIdentifier();
const storage = getMemstorage();
const identityClient = await getClientAndCreateAccount(storage);

// create new DID document and publish it
let [document] = await createDidDocument(identityClient, storage);
let did = document.id();
const [unpublished] = await createDocumentForNetwork(storage, network);

// create new identity for this account and publish document for it
const { output: identity } = await identityClient
.createIdentity(unpublished)
.finish()
.execute(identityClient);
const did = IotaDID.fromAliasId(identity.id(), identityClient.network());

// Resolve the associated Alias Output and extract the DID document from it.
const resolved: IotaDocument = await identityClient.resolveDid(did);
const resolved = await identityClient.resolveDid(did);
console.log("Resolved DID document:", JSON.stringify(resolved, null, 2));

// We can also resolve the Object ID reictly
const aliasOutput: AliasOutput = await identityClient.resolveDidOutput(did);
console.log("The Alias Output holds " + aliasOutput.getAmount() + " tokens");
// We can resolve the Object ID directly
const resolvedIdentity = await identityClient.getIdentity(identity.id());
console.dir(resolvedIdentity);
console.log(`Resolved identity has object ID ${resolvedIdentity.toFullFledged()?.id()}`);

// Or we can resolve it via the `Resolver` api:

// did:jwk can be resolved as well.
// While at it, define a custom resolver for jwk DIDs as well.
const handlers = new Map<string, (did: string) => Promise<CoreDocument | IToCoreDocument>>();
handlers.set("jwk", didJwkHandler);
const resolver = new Resolver({ handlers });

// Create new `Resolver` instance with the client with write capabilities we already have at hand
const resolver = new Resolver({ client: identityClient, handlers });

// and resolve identity DID with it.
const resolverResolved = await resolver.resolve(did.toString());
console.log(`resolverResolved ${did.toString()} resolves to:\n ${JSON.stringify(resolverResolved, null, 2)}`);

// We can also resolve via the custom resolver defined before:
const did_jwk_resolved_doc = await resolver.resolve(DID_JWK);
console.log(`DID ${DID_JWK} resolves to:\n ${JSON.stringify(did_jwk_resolved_doc, null, 2)}`);

// We can also create a resolver with a read-only client
const identityClientReadOnly = await IdentityClientReadOnly.createWithPkgId(iotaClient, IDENTITY_IOTA_PACKAGE_ID);
// In this case we will only be resolving `IotaDocument` instances, as we don't pass a `handler` configuration.
// Therefore we can limit the type of the resolved documents to `IotaDocument` when creating the new resolver as well.
const resolverWithReadOnlyClient = new Resolver<IotaDocument>({ client: identityClientReadOnly });

// And resolve as before.
const resolvedViaReadOnly = await resolverWithReadOnlyClient.resolve(did.toString());
console.log(`resolverWithReadOnlyClient ${did.toString()} resolves to:\n ${JSON.stringify(resolvedViaReadOnly, null, 2)}`);

// As our `Resolver<IotaDocument>` instance will only return `IotaDocument` instances, we can directly work with them, e.g.
console.log(`${did.toString()}'s metadata is ${resolvedViaReadOnly.metadata()}`);
}

const didJwkHandler = async (did: string) => {
let did_jwk = DIDJwk.parse(did);
return CoreDocument.expandDIDJwk(did_jwk);
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export async function deactivateIdentity() {
const iotaClient = new IotaClient({ url: NETWORK_URL });
const network = await iotaClient.getChainIdentifier();
const storage = getMemstorage();
const [unpublished, vmFragment1] = await createDocumentForNetwork(storage, network);
const identityClient = await getClientAndCreateAccount(storage);
const [unpublished] = await createDocumentForNetwork(storage, network);

// create new identity for this account and publish document for it
const { output: identity } = await identityClient
Expand Down
Original file line number Diff line number Diff line change
@@ -1,75 +1,82 @@
import {
CoreDocument,
IotaDID,
IotaDocument,
IotaIdentityClient,
JwkMemStore,
KeyIdMemStore,
Resolver,
Storage,
} from "@iota/identity-wasm/node";
import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node";
import { API_ENDPOINT, createDid } from "../util";
import { IotaClient } from "@iota/iota-sdk/client";
import {
createDocumentForNetwork,
getClientAndCreateAccount,
getMemstorage,
NETWORK_URL,
} from '../utils_alpha';

// Use this external package to avoid implementing the entire did:key method in this example.
import * as ed25519 from "@transmute/did-key-ed25519";

type KeyDocument = { customProperty: String } & CoreDocument;

function isKeyDocument(doc: object): doc is KeyDocument {
return 'customProperty' in doc;
}

/** Demonstrates how to set up a resolver using custom handlers.
*/
export async function customResolution() {
// Set up a handler for resolving Ed25519 did:key
const keyHandler = async function(didKey: string): Promise<CoreDocument> {
const keyHandler = async function(didKey: string): Promise<KeyDocument> {
let document = await ed25519.resolve(
didKey,
{ accept: "application/did+ld+json" },
);
return CoreDocument.fromJSON(document.didDocument);

// for demo purposes we'll just inject the custom property into a core document
// instead of creating a proper instance
let coreDocument = CoreDocument.fromJSON(document.didDocument);
(coreDocument as unknown as KeyDocument).customProperty = "foobar";
return coreDocument as unknown as KeyDocument;
};

// Create a new Client to interact with the IOTA ledger.
const client = new Client({
primaryNode: API_ENDPOINT,
localPow: true,
});
const didClient = new IotaIdentityClient(client);
// create new clients and create new account
const iotaClient = new IotaClient({ url: NETWORK_URL });
const network = await iotaClient.getChainIdentifier();
const storage = getMemstorage();
const identityClient = await getClientAndCreateAccount(storage);
const [unpublished] = await createDocumentForNetwork(storage, network);

// create new identity for this account and publish document for it, DID of it will be resolved later on
const { output: identity } = await identityClient
.createIdentity(unpublished)
.finish()
.execute(identityClient);
const did = IotaDID.fromAliasId(identity.id(), identityClient.network());

// Construct a Resolver capable of resolving the did:key and iota methods.
let handlerMap: Map<string, (did: string) => Promise<IotaDocument | CoreDocument>> = new Map();
let handlerMap: Map<string, (did: string) => Promise<IotaDocument | KeyDocument>> = new Map();
handlerMap.set("key", keyHandler);

const resolver = new Resolver(
const resolver = new Resolver<IotaDocument | KeyDocument>(
{
client: didClient,
client: identityClient,
handlers: handlerMap,
},
);

// A valid Ed25519 did:key value taken from https://w3c-ccg.github.io/did-method-key/#example-1-a-simple-ed25519-did-key-value.
const didKey = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";

// Generate a random mnemonic for our wallet.
const secretManager: MnemonicSecretManager = {
mnemonic: Utils.generateMnemonic(),
};

// Creates a new wallet and identity for us to resolve (see "0_create_did" example).
const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore());
let { document } = await createDid(
client,
secretManager,
storage,
);
const did = document.id();

// Resolve didKey into a DID document.
const didKeyDoc = await resolver.resolve(didKey);

// Resolve the DID we created on the IOTA ledger.
// Resolve the DID we created on the IOTA network.
const didIotaDoc = await resolver.resolve(did.toString());

// Check that the types of the resolved documents match our expectations:

if (didKeyDoc instanceof CoreDocument) {
if (isKeyDocument(didKeyDoc)) {
console.log("Resolved DID Key document:", JSON.stringify(didKeyDoc, null, 2));
console.log(`Resolved DID Key document has a custom property with the value '${didKeyDoc.customProperty}'`);
} else {
throw new Error(
"the resolved document type should match the output type of keyHandler",
Expand Down
12 changes: 6 additions & 6 deletions bindings/wasm/identity_wasm/examples/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { testApiCall } from "./0_basic/-1_test_api_call";
import { createIdentity } from "./0_basic/0_create_did";
import { updateIdentity } from "./0_basic/1_update_did";
// import { resolveIdentity } from "./0_basic/2_resolve_did";
import { resolveIdentity } from "./0_basic/2_resolve_did";
import { deactivateIdentity } from "./0_basic/3_deactivate_did";
// import { deleteIdentity } from "./0_basic/4_delete_did";
// import { createVC } from "./0_basic/5_create_vc";
Expand All @@ -14,7 +14,7 @@ import { deactivateIdentity } from "./0_basic/3_deactivate_did";
// import { didIssuesNft } from "./1_advanced/1_did_issues_nft";
// import { nftOwnsDid } from "./1_advanced/2_nft_owns_did";
// import { didIssuesTokens } from "./1_advanced/3_did_issues_tokens";
// import { customResolution } from "./1_advanced/4_custom_resolution";
import { customResolution } from "./1_advanced/4_custom_resolution";
// import { domainLinkage } from "./1_advanced/5_domain_linkage";
// import { sdJwt } from "./1_advanced/6_sd_jwt";
// import { statusList2021 } from "./1_advanced/7_status_list_2021";
Expand All @@ -35,8 +35,8 @@ async function main() {
return await createIdentity();
case "1_update_did":
return await updateIdentity();
// case "2_resolve_did":
// return await resolveIdentity();
case "2_resolve_did":
return await resolveIdentity();
case "3_deactivate_did":
return await deactivateIdentity();
// case "4_delete_did":
Expand All @@ -55,8 +55,8 @@ async function main() {
// return await nftOwnsDid();
// case "3_did_issues_tokens":
// return await didIssuesTokens();
// case "4_custom_resolution":
// return await customResolution();
case "4_custom_resolution":
return await customResolution();
// case "5_domain_linkage":
// return await domainLinkage();
// case "6_sd_jwt":
Expand Down
3 changes: 3 additions & 0 deletions bindings/wasm/identity_wasm/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ export * from "./jwk_storage";
export * from "./key_id_storage";

export * from "~identity_wasm";

// keep this export last to override the original `Resolver` from `identity_wasm` in the exports
export { Resolver } from "./resolver";
37 changes: 37 additions & 0 deletions bindings/wasm/identity_wasm/lib/resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2021-2025 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { CoreDocument, IToCoreDocument, Resolver as ResolverInner } from "~identity_wasm";

// `Resolver` type below acts the same as the "normal" resolver from `~identity_wasm`
// with the difference being that the `Resolver` here allows to pass generic type params to
// the constructor to specify the types expected to be returned by the `resolve` function.

/**
* Convenience type for resolving DID documents from different DID methods.
*
* DID documents resolved with `resolve` will have the type specified as generic type parameter T.
* With the default being `CoreDocument | IToCoreDocument`.
*
* Also provides methods for resolving DID Documents associated with
* verifiable {@link Credential}s and {@link Presentation}s.
*
* # Configuration
*
* The resolver will only be able to resolve DID documents for methods it has been configured for in the constructor.
*/
export class Resolver<T extends (CoreDocument | IToCoreDocument)> extends ResolverInner {
/**
* Fetches the DID Document of the given DID.
*
* ### Errors
*
* Errors if the resolver has not been configured to handle the method
* corresponding to the given DID or the resolution process itself fails.
* @param {string} did
* @returns {Promise<T>}
*/
async resolve(did: string): Promise<T> {
return super.resolve(did) as unknown as T;
}
}
2 changes: 0 additions & 2 deletions bindings/wasm/identity_wasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0

use identity_iota::credential::CompoundJwtPresentationValidationError;
#[cfg(feature = "wasm-resolver")]
use identity_iota::resolver;
use identity_iota::storage::key_id_storage::KeyIdStorageError;
use identity_iota::storage::key_id_storage::KeyIdStorageErrorKind;
Expand Down Expand Up @@ -159,7 +158,6 @@ impl<'a, E: std::error::Error> Display for ErrorMessage<'a, E> {
}
}

#[cfg(feature = "wasm-resolver")]
impl From<resolver::Error> for WasmError<'_> {
fn from(error: resolver::Error) -> Self {
Self {
Expand Down
Loading

0 comments on commit d679051

Please sign in to comment.