@cheqd/sdk
Version:
A TypeScript SDK built with CosmJS to interact with the cheqd network ledger
1,008 lines • 68.5 kB
JavaScript
import { createPagination, createProtobufRpcClient } from '@cosmjs/stargate';
import { AbstractCheqdSDKModule } from './_.js';
import { ISignInputs, VerificationMethods, } from '../types.js';
import { MsgCreateDidDoc, MsgCreateDidDocPayload, MsgCreateDidDocResponse, MsgDeactivateDidDoc, MsgDeactivateDidDocPayload, MsgDeactivateDidDocResponse, MsgUpdateDidDoc, MsgUpdateDidDocPayload, MsgUpdateDidDocResponse, protobufPackage, QueryClientImpl, VerificationMethod, } from '@cheqd/ts-proto/cheqd/did/v2/index.js';
import { parseCoins } from '@cosmjs/proto-signing';
import { v4 } from 'uuid';
import { assert } from '@cosmjs/utils';
import { denormalizeService, normalizeAuthentication, normalizeController, normalizeService } from '../utils.js';
import { defaultOracleExtensionKey, MovingAverages, WMAStrategies } from './oracle.js';
import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin.js';
/** Default extension key for DID-related query operations */
export const defaultDidExtensionKey = 'did';
/**
* Standard W3C and DID-related context URIs used in DID documents.
* These contexts define the semantic meaning of properties in DID documents.
*/
export const contexts = {
/** W3C DID Core v1 context */
W3CDIDv1: 'https://www.w3.org/ns/did/v1',
/** Ed25519 Signature Suite 2020 context */
W3CSuiteEd255192020: 'https://w3id.org/security/suites/ed25519-2020/v1',
/** Ed25519 Signature Suite 2018 context */
W3CSuiteEd255192018: 'https://w3id.org/security/suites/ed25519-2018/v1',
/** JSON Web Signature Suite 2020 context */
W3CSuiteJws2020: 'https://w3id.org/security/suites/jws-2020/v1',
/** Linked Domains context for domain verification */
LinkedDomainsContext: 'https://identity.foundation/.well-known/did-configuration/v1',
};
/**
* Protobuf message type literals for DID operations.
* Used for consistent message type identification across the module.
*/
export const protobufLiterals = {
/** Create DID document message type */
MsgCreateDidDoc: 'MsgCreateDidDoc',
/** Create DID document response message type */
MsgCreateDidDocResponse: 'MsgCreateDidDocResponse',
/** Update DID document message type */
MsgUpdateDidDoc: 'MsgUpdateDidDoc',
/** Update DID document response message type */
MsgUpdateDidDocResponse: 'MsgUpdateDidDocResponse',
/** Deactivate DID document message type */
MsgDeactivateDidDoc: 'MsgDeactivateDidDoc',
/** Deactivate DID document response message type */
MsgDeactivateDidDocResponse: 'MsgDeactivateDidDocResponse',
};
/** Type URL for MsgCreateDidDoc messages */
export const typeUrlMsgCreateDidDoc = `/${protobufPackage}.${protobufLiterals.MsgCreateDidDoc}`;
/** Type URL for MsgCreateDidDocResponse messages */
export const typeUrlMsgCreateDidDocResponse = `/${protobufPackage}.${protobufLiterals.MsgCreateDidDocResponse}`;
/** Type URL for MsgUpdateDidDoc messages */
export const typeUrlMsgUpdateDidDoc = `/${protobufPackage}.${protobufLiterals.MsgUpdateDidDoc}`;
/** Type URL for MsgUpdateDidDocResponse messages */
export const typeUrlMsgUpdateDidDocResponse = `/${protobufPackage}.${protobufLiterals.MsgUpdateDidDocResponse}`;
/** Type URL for MsgDeactivateDidDoc messages */
export const typeUrlMsgDeactivateDidDoc = `/${protobufPackage}.${protobufLiterals.MsgDeactivateDidDoc}`;
/** Type URL for MsgDeactivateDidDocResponse messages */
export const typeUrlMsgDeactivateDidDocResponse = `/${protobufPackage}.${protobufLiterals.MsgDeactivateDidDocResponse}`;
/**
* Type guard function to check if an object is a MsgCreateDidDocEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgCreateDidDocEncodeObject
*/
export function isMsgCreateDidDocEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgCreateDidDoc;
}
/**
* Type guard function to check if an object is a MsgUpdateDidDocEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgUpdateDidDocEncodeObject
*/
export function isMsgUpdateDidDocEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgUpdateDidDoc;
}
/**
* Type guard function to check if an object is a MsgDeactivateDidDocEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgDeactivateDidDocEncodeObject
*/
export function isMsgDeactivateDidDocEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgDeactivateDidDoc;
}
/**
* Type guard function to check if an object is a MsgCreateDidDocResponseEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgCreateDidDocResponseEncodeObject
*/
export function MsgCreateDidDocResponseEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgCreateDidDocResponse;
}
/**
* Type guard function to check if an object is a MsgUpdateDidDocEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgUpdateDidDocEncodeObject
*/
export function MsgUpdateDidDocEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgUpdateDidDoc;
}
/**
* Type guard function to check if an object is a MsgUpdateDidDocResponseEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgUpdateDidDocResponseEncodeObject
*/
export function MsgUpdateDidDocResponseEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgUpdateDidDocResponse;
}
/**
* Type guard function to check if an object is a MsgDeactivateDidDocEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgDeactivateDidDocEncodeObject
*/
export function MsgDeactivateDidDocEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgDeactivateDidDoc;
}
/**
* Type guard function to check if an object is a MsgDeactivateDidDocResponseEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgDeactivateDidDocResponseEncodeObject
*/
export function MsgDeactiveDidDocResponseEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgUpdateDidDocResponse;
}
/**
* Sets up the DID extension for the querier client.
* Creates and configures the DID-specific query methods.
*
* @param base - Base QueryClient to extend
* @returns Configured DID extension with query methods
*/
export const setupDidExtension = (base) => {
const rpc = createProtobufRpcClient(base);
const queryService = new QueryClientImpl(rpc);
return {
[defaultDidExtensionKey]: {
didDoc: async (id) => {
const { value } = await queryService.DidDoc({ id });
assert(value);
return value;
},
didDocVersion: async (id, versionId) => {
const { value } = await queryService.DidDocVersion({ id, version: versionId });
assert(value);
return value;
},
allDidDocVersionsMetadata: async (id, paginationKey) => {
const response = await queryService.AllDidDocVersionsMetadata({
id,
pagination: createPagination(paginationKey),
});
return response;
},
params: async () => {
const response = await queryService.Params({});
assert(response.params);
return response;
},
},
};
};
/**
* DID Module class providing comprehensive DID document management functionality.
* Handles creation, updates, deactivation, and querying of DID documents on the Cheqd blockchain.
*/
export class DIDModule extends AbstractCheqdSDKModule {
// @ts-expect-error underlying type `GeneratedType` is intentionally wider
static registryTypes = [
[typeUrlMsgCreateDidDoc, MsgCreateDidDoc],
[typeUrlMsgCreateDidDocResponse, MsgCreateDidDocResponse],
[typeUrlMsgUpdateDidDoc, MsgUpdateDidDoc],
[typeUrlMsgUpdateDidDocResponse, MsgUpdateDidDocResponse],
[typeUrlMsgDeactivateDidDoc, MsgDeactivateDidDoc],
[typeUrlMsgDeactivateDidDocResponse, MsgDeactivateDidDocResponse],
];
/** Base denomination for Cheqd network transactions */
static baseMinimalDenom = 'ncheq';
/** Base denomination in USD for Cheqd network transactions */
static baseUsdDenom = 'usd';
/** Default slippage tolerance in base points (BPS) */
static defaultSlippageBps = 500n;
/**
* Standard fee amounts for DID operations.
* These represent the default costs for different DID document operations.
*/
static fees = {
/** Default fee for creating a new DID document */
DefaultCreateDidDocFee: { amount: '50000000000', denom: DIDModule.baseMinimalDenom },
/** Default fee for updating an existing DID document */
DefaultUpdateDidDocFee: { amount: '25000000000', denom: DIDModule.baseMinimalDenom },
/** Default fee for deactivating a DID document */
DefaultDeactivateDidDocFee: { amount: '10000000000', denom: DIDModule.baseMinimalDenom },
/** Default fee for creating a new DID document in USD */
DefaultCreateDidDocFeeUSD: { amount: '2000000000000000000', denom: DIDModule.baseUsdDenom },
/** Default fee for updating an existing DID document in USD */
DefaultUpdateDidDocFeeUSD: { amount: '1000000000000000000', denom: DIDModule.baseUsdDenom },
/** Default fee for deactivating a DID document in USD */
DefaultDeactivateDidDocFeeUSD: { amount: '400000000000000000', denom: DIDModule.baseUsdDenom },
};
/**
* Standard gas limits for DID operations.
* These represent the default gas limits for different DID document operations.
*/
static gasLimits = {
/** Gas limit for creating a new DID document */
CreateDidDocGasLimit: '360000',
/** Gas limit for updating an existing DID document */
UpdateDidDocGasLimit: '360000',
/** Gas limit for deactivating a DID document */
DeactivateDidDocGasLimit: '360000',
};
/** Querier extension setup function for DID operations */
static querierExtensionSetup = setupDidExtension;
/** Querier instance with DID extension capabilities */
querier;
oracleFeesAvailability;
/**
* Constructs a new DID module instance.
*
* @param signer - Signing client for blockchain transactions
* @param querier - Querier client with DID extension for data retrieval
*/
constructor(signer, querier) {
super(signer, querier);
this.querier = querier;
this.methods = {
createDidDocTx: this.createDidDocTx.bind(this),
updateDidDocTx: this.updateDidDocTx.bind(this),
deactivateDidDocTx: this.deactivateDidDocTx.bind(this),
queryDidDoc: this.queryDidDoc.bind(this),
queryDidDocVersion: this.queryDidDocVersion.bind(this),
queryAllDidDocVersionsMetadata: this.queryAllDidDocVersionsMetadata.bind(this),
queryDidParams: this.queryDidParams.bind(this),
generateCreateDidDocFees: this.generateCreateDidDocFees.bind(this),
generateUpdateDidDocFees: this.generateUpdateDidDocFees.bind(this),
generateDeactivateDidDocFees: this.generateDeactivateDidDocFees.bind(this),
getPriceRangeFromDidParams: this.getPriceRangeFromDidParams.bind(this),
};
}
/**
* Gets the registry types for DID message encoding/decoding.
*
* @returns Iterable of [typeUrl, GeneratedType] pairs for the registry
*/
getRegistryTypes() {
return DIDModule.registryTypes;
}
/**
* Gets the querier extension setup for DID operations.
*
* @returns Query extension setup function for DID functionality
*/
getQuerierExtensionSetup() {
return DIDModule.querierExtensionSetup;
}
/**
* Creates a new DID document transaction on the blockchain.
* Validates the DID payload and authentication before submission.
*
* @param signInputs - Signing inputs or pre-computed signatures for the transaction
* @param didPayload - DID document payload to create
* @param address - Address of the account submitting the transaction
* @param fee - Transaction fee configuration or 'auto' for automatic calculation
* @param memo - Optional transaction memo
* @param versionId - Optional version identifier for the DID document
* @param feeOptions - Optional fee options for the transaction
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the transaction response
* @throws Error if DID payload is not spec compliant or authentication is invalid
*/
async createDidDocTx(signInputs, didPayload, address, fee, memo, versionId, feeOptions, context) {
if (!this._signer) {
this._signer = context.sdk.signer;
}
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (!versionId || versionId === '') {
versionId = v4();
}
const { valid, error, protobufVerificationMethod, protobufService } = await DIDModule.validateSpecCompliantPayload(didPayload);
if (!valid) {
throw new Error(`DID payload is not spec compliant: ${error}`);
}
const { valid: authenticationValid, error: authenticationError } = await DIDModule.validateAuthenticationAgainstSignatures(didPayload, signInputs, this.querier);
if (!authenticationValid) {
throw new Error(`DID authentication is not valid: ${authenticationError}`);
}
const payload = MsgCreateDidDocPayload.fromPartial({
context: didPayload?.['@context'],
id: didPayload.id,
controller: didPayload.controller,
verificationMethod: protobufVerificationMethod,
authentication: didPayload.authentication,
assertionMethod: didPayload.assertionMethod,
capabilityInvocation: didPayload.capabilityInvocation,
capabilityDelegation: didPayload.capabilityDelegation,
keyAgreement: didPayload.keyAgreement,
service: protobufService,
alsoKnownAs: didPayload.alsoKnownAs,
versionId: versionId,
});
let signatures;
if (ISignInputs.isSignInput(signInputs)) {
signatures = await this._signer.signCreateDidDocTx(signInputs, payload);
}
else {
signatures = signInputs;
}
const value = {
payload,
signatures,
};
const createDidMsg = {
typeUrl: typeUrlMsgCreateDidDoc,
value,
};
if (address === '') {
address = (await context.sdk.options.wallet.getAccounts())[0].address;
}
if (!fee) {
fee = await this.generateCreateDidDocFees(address, undefined, feeOptions, context);
}
return this._signer.signAndBroadcast(address, [createDidMsg], fee, memo);
}
/**
* Updates an existing DID document transaction on the blockchain.
* Validates the updated DID payload and handles key rotation scenarios.
*
* @param signInputs - Signing inputs or pre-computed signatures for the transaction
* @param didPayload - Updated DID document payload
* @param address - Address of the account submitting the transaction
* @param fee - Transaction fee configuration or 'auto' for automatic calculation
* @param memo - Optional transaction memo
* @param versionId - Optional version identifier for the updated DID document
* @param feeOptions - Optional fee options for the transaction
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the transaction response
* @throws Error if DID payload is not spec compliant or authentication is invalid
*/
async updateDidDocTx(signInputs, didPayload, address, fee, memo, versionId, feeOptions, context) {
if (!this._signer) {
this._signer = context.sdk.signer;
}
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (!versionId || versionId === '') {
versionId = v4();
}
const { valid, error, protobufVerificationMethod, protobufService } = await DIDModule.validateSpecCompliantPayload(didPayload);
if (!valid) {
throw new Error(`DID payload is not spec compliant: ${error}`);
}
const { valid: authenticationValid, error: authenticationError, externalControllersDocuments, previousDidDocument, } = await DIDModule.validateAuthenticationAgainstSignaturesKeyRotation(didPayload, signInputs, this.querier);
if (!authenticationValid) {
throw new Error(`DID authentication is not valid: ${authenticationError}`);
}
const payload = MsgUpdateDidDocPayload.fromPartial({
context: didPayload?.['@context'],
id: didPayload.id,
controller: didPayload.controller,
verificationMethod: protobufVerificationMethod,
authentication: didPayload.authentication,
assertionMethod: didPayload.assertionMethod,
capabilityInvocation: didPayload.capabilityInvocation,
capabilityDelegation: didPayload.capabilityDelegation,
keyAgreement: didPayload.keyAgreement,
service: protobufService,
alsoKnownAs: didPayload.alsoKnownAs,
versionId: versionId,
});
let signatures;
if (ISignInputs.isSignInput(signInputs)) {
signatures = await this._signer.signUpdateDidDocTx(signInputs, payload, externalControllersDocuments, previousDidDocument);
}
else {
signatures = signInputs;
}
const value = {
payload,
signatures,
};
const updateDidMsg = {
typeUrl: typeUrlMsgUpdateDidDoc,
value,
};
if (address === '') {
address = (await context.sdk.options.wallet.getAccounts())[0].address;
}
if (!fee) {
fee = await this.generateUpdateDidDocFees(address, undefined, feeOptions, context);
}
return this._signer.signAndBroadcast(address, [updateDidMsg], fee, memo);
}
/**
* Deactivates an existing DID document transaction on the blockchain.
* Validates authentication and creates a deactivation transaction.
*
* @param signInputs - Signing inputs or pre-computed signatures for the transaction
* @param didPayload - DID document payload containing the ID to deactivate
* @param address - Address of the account submitting the transaction
* @param fee - Transaction fee configuration or 'auto' for automatic calculation
* @param memo - Optional transaction memo
* @param versionId - Optional version identifier for the deactivation
* @param feeOptions - Optional fee options for the transaction
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the transaction response
* @throws Error if DID payload is not spec compliant or authentication is invalid
*/
async deactivateDidDocTx(signInputs, didPayload, address, fee, memo, versionId, feeOptions, context) {
if (!this._signer) {
this._signer = context.sdk.signer;
}
if (!versionId || versionId === '') {
versionId = v4();
}
const { valid, error, protobufVerificationMethod } = await DIDModule.validateSpecCompliantPayload(didPayload);
if (!valid) {
throw new Error(`DID payload is not spec compliant: ${error}`);
}
const { valid: authenticationValid, error: authenticationError } = await DIDModule.validateAuthenticationAgainstSignatures(didPayload, signInputs, this.querier);
if (!authenticationValid) {
throw new Error(`DID authentication is not valid: ${authenticationError}`);
}
const payload = MsgDeactivateDidDocPayload.fromPartial({
id: didPayload.id,
versionId: versionId,
});
let signatures;
if (ISignInputs.isSignInput(signInputs)) {
signatures = await this._signer.signDeactivateDidDocTx(signInputs, payload, protobufVerificationMethod);
}
else {
signatures = signInputs;
}
const value = {
payload,
signatures,
};
const deactivateDidMsg = {
typeUrl: typeUrlMsgDeactivateDidDoc,
value,
};
if (address === '') {
address = (await context.sdk.options.wallet.getAccounts())[0].address;
}
if (!fee) {
fee = await this.generateDeactivateDidDocFees(address, undefined, feeOptions, context);
}
return this._signer.signAndBroadcast(address, [deactivateDidMsg], fee, memo);
}
/**
* Queries a DID document by its identifier.
* Retrieves the latest version of the DID document with metadata.
*
* @param id - DID identifier to query
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the DID document with metadata
*/
async queryDidDoc(id, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
const { didDoc, metadata } = await this.querier[defaultDidExtensionKey].didDoc(id);
return {
didDocument: await DIDModule.toSpecCompliantPayload(didDoc),
didDocumentMetadata: await DIDModule.toSpecCompliantMetadata(metadata),
};
}
/**
* Queries a specific version of a DID document by its identifier and version ID.
*
* @param id - DID identifier to query
* @param versionId - Specific version identifier to retrieve
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the DID document version with metadata
*/
async queryDidDocVersion(id, versionId, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
const { didDoc, metadata } = await this.querier[defaultDidExtensionKey].didDocVersion(id, versionId);
return {
didDocument: await DIDModule.toSpecCompliantPayload(didDoc),
didDocumentMetadata: await DIDModule.toSpecCompliantMetadata(metadata),
};
}
/**
* Queries metadata for all versions of a DID document.
* Retrieves version history information for a specific DID.
*
* @param id - DID identifier to query version metadata for
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to array of version metadata and pagination info
*/
async queryAllDidDocVersionsMetadata(id, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
const { versions, pagination } = await this.querier[defaultDidExtensionKey].allDidDocVersionsMetadata(id);
return {
didDocumentVersionsMetadata: await Promise.all(versions.map(async (m) => await DIDModule.toSpecCompliantMetadata(m))),
pagination,
};
}
async shouldUseOracleFees(context) {
if (!this.oracleFeesAvailability) {
this.oracleFeesAvailability = (async () => {
if (!this.querier && context?.sdk?.querier) {
this.querier = context.sdk.querier;
}
const oracle = this.querier?.[defaultOracleExtensionKey];
if (!oracle?.queryParams) {
return false;
}
try {
await oracle.queryParams();
return true;
}
catch {
return false;
}
})();
}
return this.oracleFeesAvailability;
}
/**
* Queries the DID module parameters from the blockchain.
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the DID module parameters
*/
async queryDidParams(context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
return this.querier[defaultDidExtensionKey].params();
}
/**
* Generates oracle-powered fees for creating a DID document.
*
* @param feePayer - Address of the account that will pay the transaction fees
* @param granter - Optional address of the account granting fee payment permissions
* @param feeOptions - Options for fetching oracle fees
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the fee configuration for DID document creation with oracle fees
*/
async generateCreateDidDocFees(feePayer, granter, feeOptions, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (!(await this.shouldUseOracleFees(context))) {
return DIDModule.generateCreateDidDocFees(feePayer, granter);
}
// fetch fee parameters from the DID module
const feeParams = await this.queryDidParams(context);
// get the price range for the create operation
const priceRange = await this.getPriceRangeFromDidParams(feeParams, 'create', feeOptions);
// calculate the oracle fee amount based on the price range and options
return {
amount: [await this.calculateOracleFeeAmount(priceRange, feeOptions, context)],
gas: feeOptions?.gasLimit || DIDModule.gasLimits.CreateDidDocGasLimit,
payer: feePayer,
granter,
};
}
/**
* Generates oracle-powered fees for updating a DID document.
*
* @param feePayer - Address of the account that will pay the transaction fees
* @param granter - Optional address of the account granting fee payment permissions
* @param feeOptions - Options for fetching oracle fees
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the fee configuration for DID document update with oracle fees
*/
async generateUpdateDidDocFees(feePayer, granter, fetchOptions, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (!(await this.shouldUseOracleFees(context))) {
return DIDModule.generateUpdateDidDocFees(feePayer, granter);
}
// fetch fee parameters from the DID module
const feeParams = await this.queryDidParams(context);
// get the price range for the update operation
const priceRange = await this.getPriceRangeFromDidParams(feeParams, 'update', fetchOptions);
// calculate the oracle fee amount based on the price range and options
return {
amount: [await this.calculateOracleFeeAmount(priceRange, fetchOptions, context)],
gas: fetchOptions?.gasLimit || DIDModule.gasLimits.UpdateDidDocGasLimit,
payer: feePayer,
granter,
};
}
/** Generates oracle-powered fees for deactivating a DID document.
*
* @param feePayer - Address of the account that will pay the transaction fees
* @param granter - Optional address of the account granting fee payment permissions
* @param feeOptions - Options for fetching oracle fees
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the fee configuration for DID document deactivation with oracle fees
*/
async generateDeactivateDidDocFees(feePayer, granter, feeOptions, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (!(await this.shouldUseOracleFees(context))) {
return DIDModule.generateDeactivateDidDocFees(feePayer, granter);
}
// fetch fee parameters from the DID module
const feeParams = await this.queryDidParams(context);
// get the price range for the deactivate operation
const priceRange = await this.getPriceRangeFromDidParams(feeParams, 'deactivate', feeOptions);
// calculate the oracle fee amount based on the price range and options
return {
amount: [await this.calculateOracleFeeAmount(priceRange, feeOptions, context)],
gas: feeOptions?.gasLimit || DIDModule.gasLimits.DeactivateDidDocGasLimit,
payer: feePayer,
granter,
};
}
/**
* Gets the fee range for a specific DID operation from the module parameters.
* @param feeParams - DID module fee parameters
* @param operation - DID operation type ('create', 'update', 'deactivate')
* @param feeOptions - Options for fee retrieval
* @returns Promise resolving to the fee range for the specified operation
*/
async getPriceRangeFromDidParams(feeParams, operation, feeOptions) {
const operationFees = (() => {
switch (operation) {
case 'create':
return feeParams.params?.createDid;
case 'update':
return feeParams.params?.updateDid;
case 'deactivate':
return feeParams.params?.deactivateDid;
default:
throw new Error('Unsupported operation for fee retrieval');
}
})();
const operationFee = feeOptions?.feeDenom
? operationFees?.find((fee) => fee.denom === feeOptions.feeDenom)
: (operationFees?.find((fee) => fee.denom === DIDModule.baseUsdDenom) ??
operationFees?.find((fee) => fee.denom === DIDModule.baseMinimalDenom));
if (!operationFee) {
throw new Error(`Fee parameters not found for operation: ${operation}`);
}
return operationFee;
}
/**
* Calculates the oracle fee amount based on the provided fee range and options.
* @param feeRange - Fee range for the DID operation
* @param feeOptions - Options for fee calculation
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the calculated fee amount as a Coin
*/
async calculateOracleFeeAmount(feeRange, feeOptions, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (feeRange.denom !== feeOptions?.feeDenom && feeOptions?.feeDenom !== undefined) {
throw new Error(`Fee denomination mismatch: expected ${feeRange.denom}, got ${feeOptions.feeDenom}`);
}
const wantedFeeAmount = feeRange.denom === DIDModule.baseUsdDenom
? (feeOptions?.wantedAmountUsd ?? DIDModule.isFixedRange(feeRange))
? feeRange.minAmount
: feeRange.minAmount
: feeRange.minAmount;
// override fee options, if unassigned - case: moving average type
feeOptions = {
...feeOptions,
movingAverageType: feeOptions?.movingAverageType || MovingAverages.WMA,
};
// override fee options, if unassigned - case: WMA strategy
feeOptions = {
...feeOptions,
wmaStrategy: feeOptions?.wmaStrategy || feeOptions?.movingAverageType === MovingAverages.WMA
? WMAStrategies.BALANCED
: undefined,
};
const convertedFeeAmount = feeRange.denom === DIDModule.baseUsdDenom
? parseCoins((await this.querier[defaultOracleExtensionKey].convertUSDtoCHEQ(wantedFeeAmount, feeOptions?.movingAverageType, feeOptions?.wmaStrategy, feeOptions?.wmaWeights?.map((w) => BigInt(w)))).amount)[0]
: Coin.fromPartial({ amount: wantedFeeAmount, denom: feeRange.denom });
return feeOptions?.slippageBps
? DIDModule.applySlippageToCoin(convertedFeeAmount, feeOptions.slippageBps)
: convertedFeeAmount;
}
/**
* Applies slippage to a given coin amount based on the specified basis points.
* @param coin - Coin amount to apply slippage to
* @param slippageBps - Slippage in basis points (bps)
* @returns Coin with adjusted amount after applying slippage
*/
static applySlippageToCoin(coin, slippageBps) {
const base = BigInt(coin.amount);
const delta = (base * BigInt(slippageBps)) / BigInt(10_000);
const adjustedAmount = base + delta;
return Coin.fromPartial({ amount: adjustedAmount.toString(), denom: coin.denom });
}
/**
* Checks if a fee range represents a fixed fee (minAmount equals maxAmount).
* @param feeRange - Fee range to check
* @returns True if the fee range is fixed, false otherwise
*/
static isFixedRange(feeRange) {
return feeRange.minAmount === feeRange.maxAmount;
}
/**
* Validates a DID document against the Cheqd specification requirements.
* Ensures all required fields are present and verification methods are supported.
*
* @param didDocument - DID document to validate
* @returns Promise resolving to validation result with protobuf conversion or error details
*/
static async validateSpecCompliantPayload(didDocument) {
// id is required, validated on both compile and runtime
if (!didDocument?.id)
return { valid: false, error: 'id is required' };
// verificationMethod is required
if (!didDocument?.verificationMethod)
return { valid: false, error: 'verificationMethod is required' };
// verificationMethod must be an array
if (!Array.isArray(didDocument?.verificationMethod))
return { valid: false, error: 'verificationMethod must be an array' };
// verificationMethod types must be supported
const protoVerificationMethod = didDocument.verificationMethod.map((vm) => {
switch (vm?.type) {
case VerificationMethods.Ed255192020:
if (!vm?.publicKeyMultibase)
throw new Error('publicKeyMultibase is required');
return VerificationMethod.fromPartial({
id: vm.id,
controller: vm.controller,
verificationMethodType: VerificationMethods.Ed255192020,
verificationMaterial: vm.publicKeyMultibase,
});
case VerificationMethods.JWK:
if (!vm?.publicKeyJwk)
throw new Error('publicKeyJwk is required');
return VerificationMethod.fromPartial({
id: vm.id,
controller: vm.controller,
verificationMethodType: VerificationMethods.JWK,
verificationMaterial: JSON.stringify(vm.publicKeyJwk),
});
case VerificationMethods.Ed255192018:
if (!vm?.publicKeyBase58)
throw new Error('publicKeyBase58 is required');
return VerificationMethod.fromPartial({
id: vm.id,
controller: vm.controller,
verificationMethodType: VerificationMethods.Ed255192018,
verificationMaterial: vm.publicKeyBase58,
});
default:
throw new Error('Unsupported verificationMethod type');
}
});
const protoService = normalizeService(didDocument);
return {
valid: true,
protobufVerificationMethod: protoVerificationMethod,
protobufService: protoService,
};
}
/**
* Converts a protobuf DID document to a specification-compliant DID document format.
* Handles context inclusion, verification method formatting, and service denormalization.
*
* @param protobufDidDocument - Protobuf DID document to convert
* @returns Promise resolving to a spec-compliant DID document
*/
static async toSpecCompliantPayload(protobufDidDocument) {
const verificationMethod = protobufDidDocument.verificationMethod.map((vm) => {
switch (vm.verificationMethodType) {
case VerificationMethods.Ed255192020:
if (!protobufDidDocument.context.includes(contexts.W3CSuiteEd255192020))
protobufDidDocument.context = [...protobufDidDocument.context, contexts.W3CSuiteEd255192020];
return {
id: vm.id,
type: vm.verificationMethodType,
controller: vm.controller,
publicKeyMultibase: vm.verificationMaterial,
};
case VerificationMethods.JWK:
if (!protobufDidDocument.context.includes(contexts.W3CSuiteJws2020))
protobufDidDocument.context = [...protobufDidDocument.context, contexts.W3CSuiteJws2020];
return {
id: vm.id,
type: vm.verificationMethodType,
controller: vm.controller,
publicKeyJwk: JSON.parse(vm.verificationMaterial),
};
case VerificationMethods.Ed255192018:
if (!protobufDidDocument.context.includes(contexts.W3CSuiteEd255192018))
protobufDidDocument.context = [...protobufDidDocument.context, contexts.W3CSuiteEd255192018];
return {
id: vm.id,
type: vm.verificationMethodType,
controller: vm.controller,
publicKeyBase58: vm.verificationMaterial,
};
default:
throw new Error('Unsupported verificationMethod type'); // should never happen
}
});
const service = denormalizeService(protobufDidDocument);
const context = (function () {
if (protobufDidDocument.context.includes(contexts.W3CDIDv1))
return protobufDidDocument.context;
return [contexts.W3CDIDv1, ...protobufDidDocument.context];
})();
const assertionMethod = protobufDidDocument.assertionMethod.map((am) => {
try {
// Check if the assertionMethod is a DID URL
if (!am.startsWith('did:cheqd:')) {
// Parse once if it's a stringified JSON
const parsedAm = JSON.parse(am);
if (typeof parsedAm === 'string') {
// Parse again only if necessary
return JSON.parse(parsedAm);
}
return parsedAm;
}
return am;
}
catch (error) {
throw new Error(`Unsupported assertionMethod type: ${am}`);
}
});
const specCompliant = {
'@context': context,
id: protobufDidDocument.id,
controller: protobufDidDocument.controller,
verificationMethod: verificationMethod,
authentication: protobufDidDocument.authentication,
assertionMethod: assertionMethod,
capabilityInvocation: protobufDidDocument.capabilityInvocation,
capabilityDelegation: protobufDidDocument.capabilityDelegation,
keyAgreement: protobufDidDocument.keyAgreement,
service: service,
alsoKnownAs: protobufDidDocument.alsoKnownAs,
};
if (!protobufDidDocument.authentication?.length)
delete specCompliant.authentication;
if (!protobufDidDocument.assertionMethod?.length)
delete specCompliant.assertionMethod;
if (!protobufDidDocument.capabilityInvocation?.length)
delete specCompliant.capabilityInvocation;
if (!protobufDidDocument.capabilityDelegation?.length)
delete specCompliant.capabilityDelegation;
if (!protobufDidDocument.keyAgreement?.length)
delete specCompliant.keyAgreement;
if (!protobufDidDocument.service?.length)
delete specCompliant.service;
if (!protobufDidDocument.alsoKnownAs?.length)
delete specCompliant.alsoKnownAs;
return specCompliant;
}
/**
* Converts protobuf metadata to specification-compliant DID document metadata format.
* Handles date formatting and optional field normalization.
*
* @param protobufDidDocument - Protobuf metadata to convert
* @returns Promise resolving to spec-compliant DID document metadata
*/
static async toSpecCompliantMetadata(protobufDidDocument) {
return {
created: protobufDidDocument.created?.toISOString(),
updated: protobufDidDocument.updated?.toISOString(),
deactivated: protobufDidDocument.deactivated,
versionId: protobufDidDocument.versionId,
nextVersionId: protobufDidDocument?.nextVersionId,
previousVersionId: protobufDidDocument?.previousVersionId,
};
}
/**
* Validates that provided signatures match the authentication requirements in a DID document.
* Checks signature count, authentication presence, and controller authorization.
*
* @param didDocument - DID document containing authentication requirements
* @param signatures - Array of signatures to validate against authentication
* @param querier - Optional querier for retrieving external controller documents
* @param externalControllersDidDocuments - Optional pre-loaded external controller documents
* @returns Promise resolving to validation result with error details if invalid
*/
static async validateAuthenticationAgainstSignatures(didDocument, signatures, querier, externalControllersDidDocuments) {
// validate signatures - case: no signatures
if (!signatures || !signatures.length)
return { valid: false, error: 'signatures are required' };
// validate authentication - case: no authentication when at least one verificationMethod
if ((!didDocument.authentication || !didDocument.authentication.length) &&
didDocument.verificationMethod?.length)
return { valid: false, error: 'authentication is required' };
const normalizedAuthentication = normalizeAuthentication(didDocument);
// define unique authentication
const uniqueAuthentication = new Set(normalizedAuthentication);
// validate authentication - case: authentication contains duplicates
if (uniqueAuthentication.size < normalizedAuthentication.length)
return {
valid: false,
error: `authentication contains duplicate key references: duplicate key reference ${Array.from(uniqueAuthentication).find((a) => normalizeAuthentication(didDocument).filter((aa) => aa === a).length > 1)}`,
};
// define unique signatures - shallow, only verificationMethodId, no signature
const uniqueSignatures = new Set(signatures.map((s) => s.verificationMethodId));
// validate signatures - case: signatures contain duplicates
if (uniqueSignatures.size < signatures.length)
return {
valid: false,
error: `signatures contain duplicates: duplicate signature for key reference ${Array.from(uniqueSignatures).find((s) => signatures.filter((ss) => ss.verificationMethodId === s).length > 1)}`,
};
// validate authentication - case: authentication contains invalid key references
if (!Array.from(uniqueAuthentication).every((a) => didDocument.verificationMethod?.some((vm) => vm.id === a)))
return {
valid: false,
error: `authentication contains invalid key references: invalid key reference ${Array.from(uniqueAuthentication).find((a) => !didDocument.verificationMethod?.some((vm) => vm.id === a))}`,
};
// define whether external controller or not
const externalController = normalizeController(didDocument).some((c) => c !== didDocument.id);
// validate authentication - case: authentication matches signatures, unique, if no external controller
if (!Array.from(uniqueAuthentication).every((a) => uniqueSignatures.has(a)) && !externalController)
return {
valid: false,
error: `authentication does not match signatures: signature from key ${Array.from(uniqueAuthentication).find((a) => !uniqueSignatures.has(a))} is missing`,
};
// validate signatures - case: authentication matches signatures, unique, excessive signatures, no external controller
if (!Array.from(uniqueSignatures).every((s) => uniqueAuthentication.has(s)) && !externalController)
return {
valid: false,
error: `authentication does not match signatures: signature from key ${Array.from(uniqueSignatures).find((s) => !uniqueAuthentication.has(s))} is not required`,
};
// return, if no external controller
if (!externalController)
return { valid: true };
// require querier
if (!querier)
throw new Error('querier is required for external controller validation');
// get external controllers
const externalControllers = normalizeController(didDocument).filter((c) => c !== didDocument.id);
// get external controllers' documents
const externalControllersDocuments = await Promise.all(externalControllers?.map(async (c) => {
// compute index of external controller's document, if provided
const externalControllerDocumentIndex = externalControllersDidDocuments?.findIndex((d) => d.id === c);
// get external controller's document, if provided
if (externalControllerDocumentIndex !== undefined && externalControllerDocumentIndex !== -1)
return externalControllersDidDocuments?.[externalControllerDocumentIndex];
// fetch external controller's document
const protobufDocument = await querier[defaultDidExtensionKey].didDoc(c);
// throw, if not found
if (!protobufDocument || !protobufDocument.didDoc)
throw new Error(`Document for controller ${c} not found`);
// throw, if deactivated
if (protobufDocument.metadata?.deactivated === true)
throw new Error(`Controller ${c} is deactivated`);
// convert to spec compliant payload
return await DIDModule.toSpecCompliantPayload(protobufDocument.didDoc);
}));
// define unique required signatures
const uniqueRequiredSignatures = new Set(externalControllersDocuments.concat(didDocument).flatMap((d) => (d ? normalizeAuthentication(d) : [])));
// validate authentication - case: authentication matches signatures, unique, if external controller
if (!Array.from(uniqueRequiredSignatures).every((a) => uniqueSignatures.has(a)))
return {
valid: false,
error: `authentication does not match signatures: signature from key ${Array.from(uniqueRequiredSignatures).find((a) => !uniqueSignatures.has(a))} is missing`,
};
// validate authentication - case: authentication matches signatures, unique, excessive signatures, if external controller
if (uniqueRequiredSignatures.size < uniqueSignatures.size)
return {
valid: false,
error: `authentication does not match signatures: signature from key ${Array.from(uniqueSignatures).find((s) => !uniqueRequiredSignatures.has(s))} is not required`,
};
// return valid
return { valid: true };
}
/**
* Validates authentication against signatures for key rotation scenarios.
* Handles validation during DID document updates where keys may have changed.
*
* @param didDocument - Updated DID document to validate
* @param signatures - Array of signatures to validate
* @param querier - Querier for retrieving previous DID document and controllers
* @param previousDidDocument - Optional previous version of the DID document
* @param externalControllersDidDocuments - Optional pre-loaded external controller documents
* @returns Promise resolving to validation result with controller documents and previous document
*/
static async validateAuthenticationAgainstSignaturesKeyRotation(didDocument, signatures, querier, previousDidDocument, externalControllersDidDocuments) {
// validate signatures - case: no signatures
if (!signatures || !signatures.length)
return { valid: false, error: 'signatures are required' };
// validate authentication - case: no authentication when at least one verificationMethod
if ((!didDocument.authentication || !didDocument.authentication.length) &&
didDocument.verificationMethod?.length)
return { valid: false, error: 'authentication is required' };
// define unique authentication
const authentication = normalizeAuthentication(didDocument);
const uniqueAuthentication = new Set(authentication);
// validate authentication - case: authentication contains duplicates
if (uniqueAuthentication.size < authentication.length)
return {
valid: false,
err