diff --git a/examples/add-example-Web3Id/index.html b/examples/add-example-Web3Id/index.html
index b1c511e1..0ff7c91e 100644
--- a/examples/add-example-Web3Id/index.html
+++ b/examples/add-example-Web3Id/index.html
@@ -37,7 +37,7 @@
'ConcordiumVerifiableCredential',
'UniversityDegreeCredential',
],
- issuer: 'did:ccd:testnet:sci:5463:0/issuer',
+ issuer: 'did:ccd:testnet:sci:' + issuerIndex.value + ':0/issuer',
credentialSubject: {
attributes: values,
},
@@ -97,6 +97,8 @@
Account address:
value="https://raw.githubusercontent.com/Concordium/concordium-web3id/credential-metadata-example/examples/json-schemas/metadata/credential-metadata.json"
/>
+ issuer Index:
+
Attribute values:
degreeType:
diff --git a/packages/browser-wallet-api-helpers/README.md b/packages/browser-wallet-api-helpers/README.md
index ba9866f2..b3270f18 100644
--- a/packages/browser-wallet-api-helpers/README.md
+++ b/packages/browser-wallet-api-helpers/README.md
@@ -235,20 +235,20 @@ const provider = await detectConcordiumProvider();
await provider.requestIdProof('2za2yAXbFiaB151oYqTteZfqiBzibHXizwjNbpdU8hodq9SfEk', ['AA', 'BB'], '1399', '0');
```
-### Add WebId Credentials
+### Add Web3Id Credentials
To add a Web3IdCredential, use the `addWeb3IdCredential` endpoint.
-The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and the signature on the commitments and credentialHolderId. If the callback does return a valid signature, the credential is not added to the wallet.
+The credential itself and the url for the metadata must be provided. In addition, the function takes a callback function that takes a DID for the credentialHolderId as input, and which should return the randomness used to create the commitments on the values/properties in the credential, and the signature on the commitments and credentialHolderId. If the callback does return a valid signature, the credential is not added to the wallet.
-// TODO Add example.
+Note that the id fields of the credential are omitted, and added by the wallet itself, as they require the credentialHolderId.
-### Request Verifiable Presentation
-
-It is possible to request a veriable presentation on a number statements about accounts and web3IdCredentials. The function takes 2 arguments. The statements to be proved and a challenge to ensure that the proof was not generated for a different context.
-
-To build a statement, the Web3StatementBuilder, from the web-sdk, can be used. (// TODO link to it)
-
-If the wallet is locked, or you have not connected with the wallet (or previously been allowlisted) or if the user rejects proving the statement, the `Promise` will reject.
+```typescript
+provider.addWeb3IdCredential(credential, metadataUrl, async (id) => {
+ const randomness = createRandomness(attributes); // Choose some randomness for the attribute commitments.
+ const proof = createSignature(credential, id, randomness); // Create a signature to prove that the commitments are created by the issuer.
+ return { proof, randomness };
+});
+```
## Events
diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts
index e7635f65..59374799 100644
--- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts
+++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts
@@ -18,18 +18,26 @@ export interface MetadataUrl {
hash?: string;
}
+interface CredentialSchema {
+ id: string;
+ type: string;
+}
+
+/**
+ * The expected form of a Web3IdCredential, with the id fields omitted.
+ */
export interface APIVerifiableCredential {
$schema: string;
type: string[];
issuer: string;
issuanceDate: string;
credentialSubject: Omit;
- credentialSchema: {
- id: string;
- type: string;
- };
+ credentialSchema: CredentialSchema;
}
+/**
+ * Expected format for the proof that the Web3IdCredential's attribute commitments are valid
+ */
export interface CredentialProof {
proofPurpose: 'assertionMethod';
proofValue: HexString;
@@ -200,14 +208,14 @@ interface MainWalletApi {
* Note that this will throw an error if the dApp is not allowlisted, locked, or if the user rejects adding the credential.
* @param credential the web3IdCredential that should be added to the wallet
* @param metadataUrl the url where the metadata, to display the credential, is located.
- * @param createSignature a callback function, which takes the credentialId as input and must return the randomness used for the commitment of the values and signature on the commitments and credentialId.
- * @returns the credentialId, containing the publicKey that will be associated with the credential.
+ * @param createSignature a callback function, which takes a DID identifier for the credentialHolderId as input and must return the randomness used for the commitment of the values and signature on the commitments and credentialId.
+ * @returns the DID identifier for the credentialHolderId, i.e. the publicKey that will be associated with the credential.
*/
addWeb3IdCredential(
credential: APIVerifiableCredential,
metadataUrl: MetadataUrl,
createSignature: (
- credentialId: string
+ credentialHolderIdDID: string
) => Promise<{ randomness: Record; proof: CredentialProof }>
): Promise;
}
diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts
index 300b40a7..752d1bb3 100644
--- a/packages/browser-wallet-api/src/wallet-api.ts
+++ b/packages/browser-wallet-api/src/wallet-api.ts
@@ -251,7 +251,7 @@ class WalletApi extends EventEmitter implements IWalletApi {
credential: APIVerifiableCredential,
metadataUrl: MetadataUrl,
createSignature: (
- credentialId: string
+ credentialHolderIdDID: string
) => Promise<{ randomness: Record; proof: CredentialProof }>
): Promise {
const res = await this.messageHandler.sendMessage>(
@@ -266,14 +266,14 @@ class WalletApi extends EventEmitter implements IWalletApi {
throw new Error(res.message);
}
- const credentialId = res.result;
+ const credentialHolderIdDID = res.result;
- const { proof, randomness } = await createSignature(credentialId);
+ const { proof, randomness } = await createSignature(credentialHolderIdDID);
const saveSignatureResult = await this.messageHandler.sendMessage>(
- MessageType.AddWeb3IdCredentialGiveSignature,
+ MessageType.AddWeb3IdCredentialFinish,
{
- credentialId,
+ credentialHolderIdDID,
proof,
randomness,
}
@@ -283,7 +283,7 @@ class WalletApi extends EventEmitter implements IWalletApi {
throw new Error(saveSignatureResult.message);
}
- return credentialId;
+ return credentialHolderIdDID;
}
}
diff --git a/packages/browser-wallet-message-hub/src/message.ts b/packages/browser-wallet-message-hub/src/message.ts
index 5f37e832..4204a9e7 100644
--- a/packages/browser-wallet-message-hub/src/message.ts
+++ b/packages/browser-wallet-message-hub/src/message.ts
@@ -18,7 +18,7 @@ export enum MessageType {
IdProof = 'M_IdProof',
ConnectAccounts = 'M_ConnectAccounts',
AddWeb3IdCredential = 'M_AddWeb3IdCredential',
- AddWeb3IdCredentialGiveSignature = 'M_AddWeb3IdCredentialGiveSignature',
+ AddWeb3IdCredentialFinish = 'M_AddWeb3IdCredentialFinish',
}
/**
diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json
index cbeae286..5867c6ef 100644
--- a/packages/browser-wallet/package.json
+++ b/packages/browser-wallet/package.json
@@ -1,6 +1,6 @@
{
"name": "@concordium/browser-wallet",
- "version": "1.1.0.0",
+ "version": "1.1.0",
"description": "Browser extension wallet for the Concordium blockchain",
"author": "Concordium Software",
"license": "Apache-2.0",
diff --git a/packages/browser-wallet/src/background/index.ts b/packages/browser-wallet/src/background/index.ts
index ef00f1a1..9507700f 100644
--- a/packages/browser-wallet/src/background/index.ts
+++ b/packages/browser-wallet/src/background/index.ts
@@ -1,45 +1,35 @@
import {
createMessageTypeFilter,
- InternalMessageType,
- MessageType,
ExtensionMessageHandler,
+ InternalMessageType,
MessageStatusWrapper,
+ MessageType,
} from '@concordium/browser-wallet-message-hub';
+import { deserializeTypeValue, HttpProvider } from '@concordium/web-sdk';
import {
- createConcordiumClient,
- deserializeTypeValue,
- HttpProvider,
- verifyWeb3IdCredentialSignature,
-} from '@concordium/web-sdk';
-import {
- storedSelectedAccount,
- storedCurrentNetwork,
- sessionPasscode,
+ getGenesisHash,
sessionOpenPrompt,
+ sessionPasscode,
storedAcceptedTerms,
- getGenesisHash,
storedAllowlist,
- storedVerifiableCredentials,
- sessionVerifiableCredentials,
- useIndexedStorage,
+ storedCurrentNetwork,
+ storedSelectedAccount,
} from '@shared/storage/access';
-import JSONBig from 'json-bigint';
-import { ChromeStorageKey, NetworkConfiguration, VerifiableCredential } from '@shared/storage/types';
-import { buildURLwithSearchParameters } from '@shared/utils/url-helpers';
+import { mainnet, stagenet, testnet } from '@shared/constants/networkConfiguration';
+import { ChromeStorageKey, NetworkConfiguration } from '@shared/storage/types';
import { getTermsAndConditionsConfig } from '@shared/utils/network-helpers';
-import { Buffer } from 'buffer/';
-import { BackgroundSendTransactionPayload } from '@shared/utils/types';
import { parsePayload } from '@shared/utils/payload-helpers';
-import { GRPCTIMEOUT, mainnet, stagenet, testnet } from '@shared/constants/networkConfiguration';
-import { addToList, web3IdCredentialLock } from '@shared/storage/update';
-import { CredentialProof } from '@concordium/browser-wallet-api-helpers';
-import {
- getCredentialRegistryContractAddress,
- getCredentialRegistryIssuerKey,
- getPublicKeyfromPublicKeyIdentifierDID,
-} from '@shared/utils/verifiable-credential-helpers';
+import { BackgroundSendTransactionPayload } from '@shared/utils/types';
+import { buildURLwithSearchParameters } from '@shared/utils/url-helpers';
+import { Buffer } from 'buffer/';
+import JSONBig from 'json-bigint';
+import { startMonitoringPendingStatus } from './confirmation';
+import { sendCredentialHandler } from './credential-deployment';
+import { createIdProofHandler, runIfValidProof } from './id-proof';
+import { addIdpListeners, identityIssuanceHandler } from './identity-issuance';
import bgMessageHandler from './message-handler';
+import { setupRecoveryHandler, startRecovery } from './recovery';
import {
forwardToPopup,
HandleMessage,
@@ -49,11 +39,7 @@ import {
setPopupSize,
testPopupOpen,
} from './window-management';
-import { addIdpListeners, identityIssuanceHandler } from './identity-issuance';
-import { startMonitoringPendingStatus } from './confirmation';
-import { sendCredentialHandler } from './credential-deployment';
-import { startRecovery, setupRecoveryHandler } from './recovery';
-import { createIdProofHandler, runIfValidProof } from './id-proof';
+import { web3IdAddCredentialFinishHandler } from './web3Id';
const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites';
const walletLockedMessage = 'The wallet is locked';
@@ -515,75 +501,14 @@ const getSelectedChainHandler: ExtensionMessageHandler = (_msg, sender, respond)
bgMessageHandler.handleMessage(createMessageTypeFilter(MessageType.GetSelectedChain), getSelectedChainHandler);
-const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id';
-const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given';
-
-async function web3IdAddSignatureHandler(input: {
- credentialId: string;
- proof: CredentialProof;
- randomness: Record;
-}): Promise {
- const { credentialId, proof, randomness } = input;
-
- const network = await storedCurrentNetwork.get();
-
- if (!network) {
- throw new Error('No network chosen');
- }
-
- const { genesisHash } = network;
- const tempCredentials = await sessionVerifiableCredentials.get(genesisHash);
-
- if (!tempCredentials) {
- throw new Error(NO_CREDENTIALS_FIT);
- }
-
- const saved = tempCredentials.find((cred) => cred.credentialSubject.id === credentialId);
-
- if (!saved) {
- throw new Error(NO_CREDENTIALS_FIT);
- }
-
- const client = createConcordiumClient(network.grpcUrl, network.grpcPort, { timeout: GRPCTIMEOUT });
- const issuerContract = getCredentialRegistryContractAddress(saved.issuer);
-
- if (
- !proof?.proofValue ||
- !verifyWeb3IdCredentialSignature({
- globalContext: await client.getCryptographicParameters(),
- signature: proof.proofValue,
- randomness,
- values: saved.credentialSubject.attributes,
- issuerContract,
- issuerPublicKey: await getCredentialRegistryIssuerKey(client, issuerContract),
- holder: getPublicKeyfromPublicKeyIdentifierDID(saved.credentialSubject.id),
- })
- ) {
- throw new Error(INVALID_CREDENTIAL_PROOF);
- }
-
- const credential: VerifiableCredential = {
- ...saved,
- signature: proof.proofValue,
- randomness,
- };
-
- addToList(
- web3IdCredentialLock,
- credential,
- useIndexedStorage(storedVerifiableCredentials, () => Promise.resolve(genesisHash))
- );
- // TODO remove temp in session
-}
-
bgMessageHandler.handleMessage(
- createMessageTypeFilter(MessageType.AddWeb3IdCredentialGiveSignature),
+ createMessageTypeFilter(MessageType.AddWeb3IdCredentialFinish),
(input, sender, respond) => {
if (!sender.url || !isAllowlisted(sender.url)) {
respond({ success: false, message: 'not allowlisted' });
}
- web3IdAddSignatureHandler(input.payload)
+ web3IdAddCredentialFinishHandler(input.payload)
.then(() => respond({ success: true }))
.catch((error) => respond({ success: false, message: error.message }));
diff --git a/packages/browser-wallet/src/background/web3Id.ts b/packages/browser-wallet/src/background/web3Id.ts
new file mode 100644
index 00000000..40a10424
--- /dev/null
+++ b/packages/browser-wallet/src/background/web3Id.ts
@@ -0,0 +1,78 @@
+import { createConcordiumClient, verifyWeb3IdCredentialSignature } from '@concordium/web-sdk';
+import {
+ sessionVerifiableCredentials,
+ storedCurrentNetwork,
+ storedVerifiableCredentials,
+ useIndexedStorage,
+} from '@shared/storage/access';
+
+import { CredentialProof } from '@concordium/browser-wallet-api-helpers';
+import { GRPCTIMEOUT } from '@shared/constants/networkConfiguration';
+import { VerifiableCredential } from '@shared/storage/types';
+import { addToList, web3IdCredentialLock } from '@shared/storage/update';
+import {
+ getCredentialRegistryContractAddress,
+ getCredentialRegistryIssuerKey,
+ getPublicKeyfromPublicKeyIdentifierDID,
+} from '@shared/utils/verifiable-credential-helpers';
+
+const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id';
+const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given';
+
+export async function web3IdAddCredentialFinishHandler(input: {
+ credentialId: string;
+ proof: CredentialProof;
+ randomness: Record;
+}): Promise {
+ const { credentialId, proof, randomness } = input;
+
+ const network = await storedCurrentNetwork.get();
+
+ if (!network) {
+ throw new Error('No network chosen');
+ }
+
+ const { genesisHash } = network;
+ const tempCredentials = await sessionVerifiableCredentials.get(genesisHash);
+
+ if (!tempCredentials) {
+ throw new Error(NO_CREDENTIALS_FIT);
+ }
+
+ const saved = tempCredentials.find((cred) => cred.credentialSubject.id === credentialId);
+
+ if (!saved) {
+ throw new Error(NO_CREDENTIALS_FIT);
+ }
+
+ const client = createConcordiumClient(network.grpcUrl, network.grpcPort, { timeout: GRPCTIMEOUT });
+ const issuerContract = getCredentialRegistryContractAddress(saved.issuer);
+
+ if (
+ !proof?.proofValue ||
+ !verifyWeb3IdCredentialSignature({
+ globalContext: await client.getCryptographicParameters(),
+ signature: proof.proofValue,
+ randomness,
+ values: saved.credentialSubject.attributes,
+ issuerContract,
+ issuerPublicKey: await getCredentialRegistryIssuerKey(client, issuerContract),
+ holder: getPublicKeyfromPublicKeyIdentifierDID(saved.credentialSubject.id),
+ })
+ ) {
+ throw new Error(INVALID_CREDENTIAL_PROOF);
+ }
+
+ const credential: VerifiableCredential = {
+ ...saved,
+ signature: proof.proofValue,
+ randomness,
+ };
+
+ addToList(
+ web3IdCredentialLock,
+ credential,
+ useIndexedStorage(storedVerifiableCredentials, () => Promise.resolve(genesisHash))
+ );
+ // TODO remove temp in session
+}
diff --git a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx
index d2fb87a0..e6320258 100644
--- a/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx
+++ b/packages/browser-wallet/src/popup/pages/AddWeb3IdCredential/AddWeb3IdCredential.tsx
@@ -12,18 +12,19 @@ import {
storedVerifiableCredentialsAtom,
storedVerifiableCredentialSchemasAtom,
} from '@popup/store/verifiable-credential';
-import { NetworkConfiguration, VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types';
+import { VerifiableCredentialStatus, VerifiableCredentialSchema } from '@shared/storage/types';
import { useAsyncMemo } from 'wallet-common-helpers';
import { useHdWallet } from '@popup/shared/utils/account-helpers';
-import { ContractAddress } from '@concordium/web-sdk';
import { displayUrl } from '@popup/shared/utils/string-helpers';
import {
+ createCredentialId,
+ createPublicKeyIdentifier,
fetchCredentialMetadata,
+ fetchCredentialSchema,
getCredentialRegistryContractAddress,
} from '@shared/utils/verifiable-credential-helpers';
import { APIVerifiableCredential } from '@concordium/browser-wallet-api-helpers';
import { networkConfigurationAtom } from '@popup/store/settings';
-import { getNet } from '@shared/utils/network-helpers';
import { MetadataUrl } from '@concordium/browser-wallet-api-helpers/lib/wallet-api-types';
import { VerifiableCredentialCard } from '../VerifiableCredential/VerifiableCredentialCard';
@@ -42,16 +43,6 @@ interface Location {
};
}
-function createCredentialSubjectId(credentialHolderId: string, network: NetworkConfiguration) {
- return `did:ccd:${getNet(network).toLowerCase()}:pkc:${credentialHolderId}`;
-}
-
-function createCredentialId(credentialHolderId: string, issuer: ContractAddress, network: NetworkConfiguration) {
- return `did:ccd:${getNet(network).toLowerCase()}:sci:${issuer.index}:${
- issuer.subindex
- }/credentialEntry/${credentialHolderId}`;
-}
-
export default function AddWeb3IdCredential({ onAllow, onReject }: Props) {
const { state } = useLocation() as Location;
const { t } = useTranslation('addWeb3IdCredential');
@@ -74,15 +65,19 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) {
const controller = new AbortController();
const metadata = useAsyncMemo(
- () => {
+ async () => {
+ if (verifiableCredentialMetadata.loading) {
+ return undefined;
+ }
+ if (metadataUrl.url in verifiableCredentialMetadata.value) {
+ // TODO check hash?
+ return verifiableCredentialMetadata.value[metadataUrl.url];
+ }
return fetchCredentialMetadata(metadataUrl, controller);
},
undefined,
- [metadataUrl]
+ [verifiableCredentialMetadata.loading]
);
- useEffect(() => () => controller.abort(), [metadataUrl]);
-
- // TODO use Jakobs?
const schema = useAsyncMemo(
async () => {
if (schemas.loading) {
@@ -90,19 +85,19 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) {
}
const schemaUrl = credential.credentialSchema.id;
if (schemaUrl in schemas.value) {
+ // TODO check hash?
return schemas.value[schemaUrl];
}
- // TODO check checksum
- const response = await fetch(schemaUrl);
- return JSON.parse(await response.text());
+ return fetchCredentialSchema(metadataUrl, controller);
},
undefined,
[schemas.loading]
);
+ useEffect(() => () => controller.abort(), [metadataUrl]);
async function addCredential(credentialSchema: VerifiableCredentialSchema) {
if (!wallet) {
- throw new Error('unreachable');
+ throw new Error('Wallet is unexpectedly missing');
}
const schemaUrl = credential.credentialSchema.id;
@@ -111,7 +106,8 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) {
updatedSchemas[schemaUrl] = credentialSchema;
setSchemas(updatedSchemas);
}
- // Find the next unused index (// TODO verify on chain)
+ // Find the next unused index
+ // TODO verify index is unused on chain?
const index = [...(web3IdCredentials || []), ...(storedWeb3IdCredentials || [])].reduce(
(best, cred) => (cred.issuer === credential.issuer ? Math.max(cred.index + 1, best) : best),
0
@@ -120,7 +116,7 @@ export default function AddWeb3IdCredential({ onAllow, onReject }: Props) {
const issuer = getCredentialRegistryContractAddress(credential.issuer);
const credentialHolderId = wallet.getVerifiableCredentialPublicKey(issuer, index).toString('hex');
- const credentialSubjectId = createCredentialSubjectId(credentialHolderId, network);
+ const credentialSubjectId = createPublicKeyIdentifier(credentialHolderId, network);
const credentialSubject = { ...credential.credentialSubject, id: credentialSubjectId };
const fullCredential = {
diff --git a/packages/browser-wallet/src/shared/storage/types.ts b/packages/browser-wallet/src/shared/storage/types.ts
index 2ab063df..54b22ee7 100644
--- a/packages/browser-wallet/src/shared/storage/types.ts
+++ b/packages/browser-wallet/src/shared/storage/types.ts
@@ -267,22 +267,19 @@ export enum VerifiableCredentialStatus {
NotActivated,
}
-interface CredentialSchema {
- id: string;
- type: string;
-}
-
export type CredentialSubject = {
id: string;
attributes: Record;
};
export interface VerifiableCredential extends APIVerifiableCredential {
+ // With ID
+ credentialSubject: CredentialSubject;
id: string;
+ // Secrets
signature: string;
randomness: Record;
- credentialSchema: CredentialSchema;
- credentialSubject: CredentialSubject;
+ /** index used to derive keys for credential */
index: number;
}
diff --git a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts
index ddd2fd40..6e482691 100644
--- a/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts
+++ b/packages/browser-wallet/src/shared/utils/verifiable-credential-helpers.ts
@@ -1,6 +1,7 @@
import { ConcordiumGRPCClient, ContractAddress, sha256 } from '@concordium/web-sdk';
import {
MetadataUrl,
+ NetworkConfiguration,
VerifiableCredential,
VerifiableCredentialSchema,
VerifiableCredentialStatus,
@@ -8,6 +9,7 @@ import {
import { Buffer } from 'buffer/';
import jsonschema from 'jsonschema';
import { getContractName } from './contract-helpers';
+import { getNet } from './network-helpers';
/**
* Extracts the credential holder id from a verifiable credential id (did).
@@ -679,3 +681,23 @@ export async function getCredentialRegistryIssuerKey(
return returnValue;
}
+
+/**
+ * Create a publicKey DID identitifer for the given key.
+ */
+export function createPublicKeyIdentifier(publicKey: string, network: NetworkConfiguration): string {
+ return `did:ccd:${getNet(network).toLowerCase()}:pkc:${publicKey}`;
+}
+
+/**
+ * Create a DID identitifer for the given web3Id credential.
+ */
+export function createCredentialId(
+ credentialHolderId: string,
+ issuer: ContractAddress,
+ network: NetworkConfiguration
+): string {
+ return `did:ccd:${getNet(network).toLowerCase()}:sci:${issuer.index}:${
+ issuer.subindex
+ }/credentialEntry/${credentialHolderId}`;
+}