diff --git a/README.md b/README.md index 57cb357..0191ffe 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ If a call requires a DID assertion method key, either `ATT_MNEMONIC` or `DID_MNE To run this script, execute `yarn call-authorize` and then copy the HEX-encoded operation to be submitted via [PolkadotJS Apps][polkadot-apps] in `Developer > Extrinsics > Decode`, using the account specified in `SUBMITTER_ADDRESS`. -## Generate a DIP signature with a DID key +## Generate a DIP signature for a sibling parachain with a DID key This script signs any valid HEX-encoded call of any other parachain with the right key re-generated from the provided seedling information, i.e., either with the provided mnemonic, or with the provided combination of base mnemonic and derivation path. @@ -68,6 +68,8 @@ Once the right call (i.e., the right pallet and right method) with the right par The following env variables are required: +- `RELAY_WS_ADDRESS`: The endpoint address of the relaychain. +- `PROVIDER_WS_ADDRESS`: The endpoint address of the DIP provider chain. - `CONSUMER_WS_ADDRESS`: The endpoint address of the consumer chain on which DIP is to be used. - `SUBMITTER_ADDRESS`: The address (encoded with the target chain network prefix `38`) that is authorized to submit the transaction on the target chain. - `ENCODED_CALL`: The HEX-encoded call to DID-sign. @@ -76,8 +78,9 @@ The following env variables are required: The following optional env variables can be passed: -- `IDENTITY_DETAILS`: The runtime type definition of the identity details stored on the consumer chain, according to the DIP protocol. It defaults to `u128`, which represents a simple nonce value. +- `IDENTITY_DETAILS`: The runtime type definition of the identity details stored on the consumer chain, according to the DIP protocol. It defaults to `Option`, which represents a simple (optional) nonce value. - `ACCOUNT_ID`: The runtime type definition of account address on the consumer chain. It defaults to `AccountId32`, which is the default of most Substrate-based chains. Some chains might use `AccountId20`. +- `INCLUDE_WEB3NAME`: Wether the web3name of the DID should be added to the DIP proof of not. Values can be anything that is truthy in JS terms. It defaults to `false`. **The proof generation will fail if this value is `true` but the DID does not have a web3name.** As with DID creation, there is no strong requirement on what other variables must be set. Depending on the expected key to be used to sign the call, the right mnemonic or the right base mnemonic + derivation path must be provided. @@ -85,7 +88,37 @@ Depending on the expected key to be used to sign the call, the right mnemonic or For instance, if a call requires a DID authentication key, either `AUTH_MNEMONIC` or `DID_MNEMONIC` and `AUTH_DERIVATION_PATH` must be specified. If a call requires a DID assertion method key, either `ATT_MNEMONIC` or `DID_MNEMONIC` and `ATT_DERIVATION_PATH` must be specified. -To run this script, execute `yarn dip-sign` and then copy the generated signature and block number to be submitted via [PolkadotJS Apps][polkadot-apps] as part of the DIP tx submission process, using the account specified in `SUBMITTER_ADDRESS`. +To run this script, execute `yarn dip-sign:sibling` and then copy the generated signature and block number to be submitted via [PolkadotJS Apps][polkadot-apps] as part of the DIP tx submission process, using the account specified in `SUBMITTER_ADDRESS`. + +## Generate a DIP signature for the parent relaychain with a DID key + +This script signs any valid HEX-encoded call of the parent relaychain with the right key re-generated from the provided seedling information, i.e., either with the provided mnemonic, or with the provided combination of base mnemonic and derivation path. + +Valid HEX-encoded calls can be generated by interacting with [PolkadotJS Apps][polkadot-apps] under the `Developer > Extrinsics` menu. +Once the right call (i.e., the right pallet and right method) with the right parameters has been specified, the HEX-encoded value under `encoded call data` can be copied and passed as parameter to this script. + +The following env variables are required: + +- `RELAY_WS_ADDRESS`: The endpoint address of the relaychain. +- `PROVIDER_WS_ADDRESS`: The endpoint address of the DIP provider chain. +- `SUBMITTER_ADDRESS`: The address (encoded with the target chain network prefix `38`) that is authorized to submit the transaction on the target chain. +- `ENCODED_CALL`: The HEX-encoded call to DID-sign. +- `DID_URI`: The URI of the DID authorizing the operation +- `VERIFICATION_METHOD`: The verification method of the DID key to use. Because this script is not able to automatically derive the DID key required to sign the call on the target chain, it has to be explicitely set with this variable. Example values are `authentication`, `assertionMethod`, and `capabilityDelegation`. + +The following optional env variables can be passed: + +- `IDENTITY_DETAILS`: The runtime type definition of the identity details stored on the consumer chain, according to the DIP protocol. It defaults to `Option`, which represents a simple (optional) nonce value. +- `ACCOUNT_ID`: The runtime type definition of account address on the consumer chain. It defaults to `AccountId32`, which is the default of most Substrate-based chains. Some chains might use `AccountId20`. +- `INCLUDE_WEB3NAME`: Wether the web3name of the DID should be added to the DIP proof of not. Values can be anything that is truthy in JS terms. It defaults to `false`. **The proof generation will fail if this value is `true` but the DID does not have a web3name.** + +As with DID creation, there is no strong requirement on what other variables must be set. +Depending on the expected key to be used to sign the call, the right mnemonic or the right base mnemonic + derivation path must be provided. + +For instance, if a call requires a DID authentication key, either `AUTH_MNEMONIC` or `DID_MNEMONIC` and `AUTH_DERIVATION_PATH` must be specified. +If a call requires a DID assertion method key, either `ATT_MNEMONIC` or `DID_MNEMONIC` and `ATT_DERIVATION_PATH` must be specified. + +To run this script, execute `yarn dip-sign:parent` and then copy the generated signature and block number to be submitted via [PolkadotJS Apps][polkadot-apps] as part of the DIP tx submission process, using the account specified in `SUBMITTER_ADDRESS`. ## Change a DID key diff --git a/package.json b/package.json index 74c002c..93665de 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "att-key-set": "ts-node src/att-key-set.ts", "del-key-set": "ts-node src/del-key-set.ts", "did-create": "ts-node src/did-create.ts", - "dip-sign": "ts-node src/dip-sign.ts", + "dip-sign:parent": "ts-node src/dip-parent-sign.ts", + "dip-sign:sibling": "ts-node src/dip-sibling-sign.ts", "call-authorize": "ts-node src/call-sign", "check-ts": "tsc src/** --skipLibCheck --noEmit", "lint": "eslint . --ext .ts --format=codeframe", @@ -30,4 +31,4 @@ "typescript": "^4.7.4" }, "packageManager": "yarn@3.6.0" -} +} \ No newline at end of file diff --git a/src/dip-parent-sign.ts b/src/dip-parent-sign.ts new file mode 100644 index 0000000..946bfff --- /dev/null +++ b/src/dip-parent-sign.ts @@ -0,0 +1,112 @@ +import 'dotenv/config' + +import * as Kilt from '@kiltprotocol/sdk-js' +import { ApiPromise, WsProvider } from '@polkadot/api' +import { dipProviderCalls, types } from '@kiltprotocol/type-definitions' +import { cryptoWaitReady } from '@polkadot/util-crypto' + +import * as utils from './utils' + +async function main() { + const relayWsAddress = process.env[utils.envNames.relayWsAddress] + const providerWsAddress = process.env[utils.envNames.providerWsAddress] + if (relayWsAddress === undefined) { + throw new Error( + `No ${utils.envNames.relayWsAddress} env variable specified.` + ) + } + if (providerWsAddress === undefined) { + throw new Error( + `No ${utils.envNames.providerWsAddress} env variable specified.` + ) + } + const submitterAddress = process.env[ + utils.envNames.submitterAddress + ] as Kilt.KiltAddress + if (submitterAddress === undefined) { + throw new Error( + `No "${utils.envNames.submitterAddress}" env variable specified.` + ) + } + + await cryptoWaitReady() + // eslint-disable-next-line max-len + const authKey = + utils.generateAuthenticationKey() ?? + Kilt.Utils.Crypto.makeKeypairFromUri('//Alice') + const assertionKey = utils.generateAttestationKey() + const delegationKey = utils.generateDelegationKey() + + const didUri = process.env[utils.envNames.didUri] as Kilt.DidUri + if (didUri === undefined) { + throw new Error(`"${utils.envNames.didUri}" not specified.`) + } + + const consumerApi = await ApiPromise.create({ + provider: new WsProvider(relayWsAddress), + }) + + const encodedCall = process.env[utils.envNames.encodedCall] + const decodedCall = consumerApi.createType('Call', encodedCall) + + const [requiredKey, verificationMethod] = (() => { + const providedMethod = utils.parseVerificationMethod() + switch (providedMethod) { + case 'authentication': + return [authKey, providedMethod] + case 'assertionMethod': + return [assertionKey, providedMethod] + case 'capabilityDelegation': + return [delegationKey, providedMethod] + } + })() + if (requiredKey === undefined) { + throw new Error( + 'The DID key to authorize the operation is not part of the DID Document. Please add such a key before re-trying.' + ) + } + + const providerApi = await ApiPromise.create({ + provider: new WsProvider(providerWsAddress), + runtime: dipProviderCalls, + types, + }) + const didKeyId = utils.computeDidKeyId( + providerApi, + requiredKey.publicKey, + requiredKey.type + ) + + const includeWeb3Name = + process.env[utils.envNames.includeWeb3Name]?.toLowerCase() === 'true' || + utils.defaults.includeWeb3Name + const signedExtrinsic = await utils.generateParentDipTx( + consumerApi, + providerApi, + didUri, + decodedCall, + submitterAddress, + didKeyId, + verificationMethod, + includeWeb3Name, + utils.getKeypairTxSigningCallback(requiredKey) + ) + + const encodedOperation = signedExtrinsic.toHex() + console.log( + ` + DIP tx: ${encodedOperation}. + Please add these details to the "dipConsumer.dispatchAs" function in PolkadotJS. + ` + ) + console.log( + `Direct link: ${utils.generatePolkadotJSLink( + relayWsAddress, + encodedOperation + )}` + ) +} + +main() + .catch((e) => console.error(e)) + .then(() => process.exit(0)) diff --git a/src/dip-sign.ts b/src/dip-sibling-sign.ts similarity index 58% rename from src/dip-sign.ts rename to src/dip-sibling-sign.ts index ae7b0c8..4ce8a5c 100644 --- a/src/dip-sign.ts +++ b/src/dip-sibling-sign.ts @@ -2,19 +2,30 @@ import 'dotenv/config' import * as Kilt from '@kiltprotocol/sdk-js' import { ApiPromise, WsProvider } from '@polkadot/api' +import { dipProviderCalls, types } from '@kiltprotocol/type-definitions' +import { cryptoWaitReady } from '@polkadot/util-crypto' import * as utils from './utils' async function main() { + const relayWsAddress = process.env[utils.envNames.relayWsAddress] + const providerWsAddress = process.env[utils.envNames.providerWsAddress] const consumerWsAddress = process.env[utils.envNames.consumerWsAddress] + if (relayWsAddress === undefined) { + throw new Error( + `No ${utils.envNames.relayWsAddress} env variable specified.` + ) + } + if (providerWsAddress === undefined) { + throw new Error( + `No ${utils.envNames.providerWsAddress} env variable specified.` + ) + } if (consumerWsAddress === undefined) { throw new Error( `No ${utils.envNames.consumerWsAddress} env variable specified.` ) } - const api = await ApiPromise.create({ - provider: new WsProvider(consumerWsAddress), - }) const submitterAddress = process.env[ utils.envNames.submitterAddress @@ -25,6 +36,7 @@ async function main() { ) } + await cryptoWaitReady() // eslint-disable-next-line max-len const authKey = utils.generateAuthenticationKey() ?? @@ -37,8 +49,12 @@ async function main() { throw new Error(`"${utils.envNames.didUri}" not specified.`) } + const consumerApi = await ApiPromise.create({ + provider: new WsProvider(consumerWsAddress), + }) + const encodedCall = process.env[utils.envNames.encodedCall] - const decodedCall = api.createType('Call', encodedCall) + const decodedCall = consumerApi.createType('Call', encodedCall) const [requiredKey, verificationMethod] = (() => { const providedMethod = utils.parseVerificationMethod() @@ -56,26 +72,47 @@ async function main() { 'The DID key to authorize the operation is not part of the DID Document. Please add such a key before re-trying.' ) } - const [dipSignature, blockNumber] = await utils.generateDipTxSignature( - api, + + const providerApi = await ApiPromise.create({ + provider: new WsProvider(providerWsAddress), + runtime: dipProviderCalls, + types, + }) + const didKeyId = utils.computeDidKeyId( + providerApi, + requiredKey.publicKey, + requiredKey.type + ) + + const includeWeb3Name = + process.env[utils.envNames.includeWeb3Name]?.toLowerCase() === 'true' || + utils.defaults.includeWeb3Name + const signedExtrinsic = await utils.generateSiblingDipTx( + await ApiPromise.create({ provider: new WsProvider(relayWsAddress) }), + providerApi, + consumerApi, didUri, decodedCall, submitterAddress, + didKeyId, verificationMethod, + includeWeb3Name, utils.getKeypairTxSigningCallback(requiredKey) ) + const encodedOperation = signedExtrinsic.toHex() console.log( ` - DID signature for submission via DIP: ${JSON.stringify( - utils.hexifyDipSignature(dipSignature), - null, - 2 - )}. - Block number used for signature generation: ${blockNumber.toString()}. + DIP tx: ${encodedOperation}. Please add these details to the "dipConsumer.dispatchAs" function in PolkadotJS. ` ) + console.log( + `Direct link: ${utils.generatePolkadotJSLink( + consumerWsAddress, + encodedOperation + )}` + ) } main() diff --git a/src/utils.ts b/src/utils.ts index 300b643..97de1cb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import type { BN } from '@polkadot/util' import type { Call } from '@polkadot/types/interfaces' +import type { Codec } from '@polkadot/types/types' +import type { Result } from '@polkadot/types' import { ApiPromise, Keyring } from '@polkadot/api' import { KeyringPair } from '@polkadot/keyring/types' +import { blake2AsHex } from '@polkadot/util-crypto' import { u8aToHex } from '@polkadot/util' import * as Kilt from '@kiltprotocol/sdk-js' @@ -26,10 +31,13 @@ export const envNames = { delKeyType: 'DEL_KEY_TYPE', encodedCall: 'ENCODED_CALL', consumerWsAddress: 'CONSUMER_WS_ADDRESS', + providerWsAddress: 'PROVIDER_WS_ADDRESS', + relayWsAddress: 'RELAY_WS_ADDRESS', verificationMethod: 'VERIFICATION_METHOD', identityDetailsType: 'IDENTITY_DETAILS', accountIdType: 'ACCOUNT_ID', blockNumberType: 'BLOCK_NUMBER', + includeWeb3Name: 'INCLUDE_WEB3NAME', } type Defaults = { @@ -40,6 +48,7 @@ type Defaults = { identityDetailsType: string accountIdType: string blockNumberType: string + includeWeb3Name: boolean } export const defaults: Defaults = { @@ -47,9 +56,10 @@ export const defaults: Defaults = { authKeyType: 'sr25519', attKeyType: 'sr25519', delKeyType: 'sr25519', - identityDetailsType: 'u128', + identityDetailsType: 'Option', accountIdType: 'AccountId32', blockNumberType: 'u64', + includeWeb3Name: false, } export function getKeypairSigningCallback( @@ -226,9 +236,9 @@ export function generateNewAuthenticationKey(): } const validValues: Set = new Set([ - 'authentication', - 'assertionMethod', - 'capabilityDelegation', + 'authentication' as Kilt.VerificationKeyRelationship, + 'assertionMethod' as Kilt.VerificationKeyRelationship, + 'capabilityDelegation' as Kilt.VerificationKeyRelationship, ]) export function parseVerificationMethod(): Kilt.VerificationKeyRelationship { const verificationMethod = process.env[envNames.verificationMethod] @@ -246,7 +256,238 @@ export function parseVerificationMethod(): Kilt.VerificationKeyRelationship { } } -export async function generateDipTxSignature( +export async function generateSiblingDipTx( + relayApi: ApiPromise, + providerApi: ApiPromise, + consumerApi: ApiPromise, + did: Kilt.DidUri, + call: Call, + submitterAccount: KeyringPair['address'], + keyId: Kilt.DidVerificationKey['id'], + didKeyRelationship: Kilt.VerificationKeyRelationship, + includeWeb3Name: boolean, + sign: Kilt.SignExtrinsicCallback +): Promise { + const signature = await generateDipTxSignature( + consumerApi, + did, + call, + submitterAccount, + didKeyRelationship, + sign + ) + + const providerChainId = await providerApi.query.parachainInfo.parachainId() + console.log(`Provider chain has para ID = ${providerChainId.toHuman()}.`) + const providerFinalizedBlockHash = + await providerApi.rpc.chain.getFinalizedHead() + const providerFinalizedBlockNumber = await providerApi.rpc.chain + .getHeader(providerFinalizedBlockHash) + .then((h) => h.number) + console.log( + `DIP action targeting the last finalized identity provider block with hash: + ${providerFinalizedBlockHash} + and number + ${providerFinalizedBlockNumber}.` + ) + const relayParentBlockHeight = await providerApi + .at(providerFinalizedBlockHash) + .then((api) => api.query.parachainSystem.lastRelayChainBlockNumber()) + const relayParentBlockHash = await relayApi.rpc.chain.getBlockHash( + relayParentBlockHeight + ) + console.log( + `Relay chain block the identity provider block was anchored to: + ${relayParentBlockHeight.toHuman()} + with hash + ${relayParentBlockHash.toHuman()}.` + ) + + const { proof: relayProof } = await relayApi.rpc.state.getReadProof( + [relayApi.query.paras.heads.key(providerChainId)], + relayParentBlockHash + ) + + // Proof of commitment must be generated with the state root at the block before the last one finalized. + const previousBlockHash = await providerApi.rpc.chain.getBlockHash( + providerFinalizedBlockNumber.toNumber() - 1 + ) + console.log( + `Using previous provider block hash for the state proof generation: ${previousBlockHash.toHex()}.` + ) + const { proof: paraStateProof } = await providerApi.rpc.state.getReadProof( + [ + providerApi.query.dipProvider.identityCommitments.key( + Kilt.Did.toChain(did) + ), + ], + previousBlockHash + ) + console.log( + `DIP proof generated for the DID key ${keyId.substring( + 1 + )} (${didKeyRelationship}).` + ) + const dipProof = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ( + (await providerApi.call.dipProvider.generateProof({ + identifier: Kilt.Did.toChain(did), + keys: [keyId.substring(1)], + accounts: [], + shouldIncludeWeb3Name: includeWeb3Name, + // TODO: Improve this line below + })) as Result + ).asOk as any + providerApi.disconnect() + + const extrinsic = consumerApi.tx.dipConsumer.dispatchAs( + Kilt.Did.toChain(did), + { + paraStateRoot: { + relayBlockHeight: relayParentBlockHeight, + proof: relayProof, + }, + dipIdentityCommitment: paraStateProof, + did: { + leaves: { + blinded: dipProof.proof.blinded, + revealed: dipProof.proof.revealed, + }, + signature: { + signature: signature[0], + blockNumber: signature[1], + }, + }, + }, + call + ) + + return extrinsic +} + +export async function generateParentDipTx( + relayApi: ApiPromise, + providerApi: ApiPromise, + did: Kilt.DidUri, + call: Call, + submitterAccount: KeyringPair['address'], + keyId: Kilt.DidVerificationKey['id'], + didKeyRelationship: Kilt.VerificationKeyRelationship, + includeWeb3Name: boolean, + sign: Kilt.SignExtrinsicCallback +): Promise { + const signature = await generateDipTxSignature( + relayApi, + did, + call, + submitterAccount, + didKeyRelationship, + sign + ) + + const providerChainId = await providerApi.query.parachainInfo.parachainId() + console.log(`Provider chain has para ID = ${providerChainId.toHuman()}.`) + const providerFinalizedBlockHash = + await providerApi.rpc.chain.getFinalizedHead() + const providerFinalizedBlockNumber = await providerApi.rpc.chain + .getHeader(providerFinalizedBlockHash) + .then((h) => h.number) + console.log( + `DIP action targeting the last finalized identity provider block with hash: + ${providerFinalizedBlockHash} + and number + ${providerFinalizedBlockNumber}.` + ) + const relayParentBlockHeight = await providerApi + .at(providerFinalizedBlockHash) + .then((api) => api.query.parachainSystem.lastRelayChainBlockNumber()) + const relayParentBlockHash = await relayApi.rpc.chain.getBlockHash( + relayParentBlockHeight + ) + console.log( + `Relay chain block the identity provider block was anchored to: + ${relayParentBlockHeight.toHuman()} + with hash + ${relayParentBlockHash.toHuman()}.` + ) + + const { proof: relayProof } = await relayApi.rpc.state.getReadProof( + [relayApi.query.paras.heads.key(providerChainId)], + relayParentBlockHash + ) + + const header = await relayApi.rpc.chain.getHeader(relayParentBlockHash) + console.log( + `Header for the relay at block ${relayParentBlockHeight} (${relayParentBlockHash}): ${JSON.stringify( + header, + null, + 2 + )}` + ) + + // Proof of commitment must be generated with the state root at the block before the last one finalized. + const previousBlockHash = await providerApi.rpc.chain.getBlockHash( + providerFinalizedBlockNumber.toNumber() - 1 + ) + console.log( + `Using previous provider block hash for the state proof generation: ${previousBlockHash.toHex()}.` + ) + const { proof: paraStateProof } = await providerApi.rpc.state.getReadProof( + [ + providerApi.query.dipProvider.identityCommitments.key( + Kilt.Did.toChain(did) + ), + ], + previousBlockHash + ) + console.log( + `DIP proof generated for the DID key ${keyId.substring( + 1 + )} (${didKeyRelationship}).` + ) + const dipProof = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ( + (await providerApi.call.dipProvider.generateProof({ + identifier: Kilt.Did.toChain(did), + keys: [keyId.substring(1)], + accounts: [], + shouldIncludeWeb3Name: includeWeb3Name, + // TODO: Improve this line below + })) as Result + ).asOk as any + providerApi.disconnect() + + const extrinsic = relayApi.tx.dipConsumer.dispatchAs( + Kilt.Did.toChain(did), + { + paraStateRoot: { + relayBlockHeight: relayParentBlockHeight, + proof: relayProof, + }, + header: { + ...header.toJSON(), + }, + dipIdentityCommitment: paraStateProof, + did: { + leaves: { + blinded: dipProof.proof.blinded, + revealed: dipProof.proof.revealed, + }, + signature: { + signature: signature[0], + blockNumber: signature[1], + }, + }, + }, + call + ) + + return extrinsic +} + +async function generateDipTxSignature( api: ApiPromise, did: Kilt.DidUri, call: Call, @@ -262,11 +503,13 @@ export async function generateDipTxSignature( console.log(`DIP signature targeting block number: ${blockNumber.toHuman()}`) const genesisHash = await api.query.system.blockHash(0) console.log(`DIP consumer genesis hash: ${genesisHash.toHuman()}`) - const identityDetails = await api.query.dipConsumer.identityEntries( - Kilt.Did.toChain(did) - ) const identityDetailsType = process.env[envNames.identityDetailsType] ?? defaults.identityDetailsType + const identityDetails = + (await api.query.dipConsumer.identityEntries( + Kilt.Did.toChain(did) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + )) || api.createType(identityDetailsType, null) console.log( `DIP subject identity details on consumer chain: ${JSON.stringify( identityDetails, @@ -283,14 +526,7 @@ export async function generateDipTxSignature( const signaturePayload = api .createType( `(Call, ${identityDetailsType}, ${accountIdType}, ${blockNumberType}, Hash)`, - [ - call, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (identityDetails.toJSON() as any).details, - submitterAccount, - blockNumber, - genesisHash, - ] + [call, identityDetails, submitterAccount, blockNumber, genesisHash] ) .toU8a() console.log(`Encoded payload for signing: ${u8aToHex(signaturePayload)}`) @@ -315,6 +551,19 @@ export function hexifyDipSignature(signature: Kilt.Did.EncodedSignature) { return hexifiedSignature } +export function computeDidKeyId( + api: ApiPromise, + publicKey: Kilt.KeyringPair['publicKey'], + keyType: Kilt.DidKey['type'] +): Kilt.DidKey['id'] { + const didEncodedKey = api.createType('DidDidDetailsDidPublicKey', { + publicVerificationKey: { + [keyType]: publicKey, + }, + }) + return `#${blake2AsHex(didEncodedKey.toU8a(), 256)}` +} + export function generatePolkadotJSLink( wsAddress: string, encodedExtrinsic: `0x${string}`