@cheqd/sdk
Version:
A TypeScript SDK built with CosmJS to interact with the cheqd network ledger
591 lines • 27.9 kB
JavaScript
import { AbstractCheqdSDKModule } from './_.js';
import { CheqdSigningStargateClient } from '../signer.js';
import { parseCoins } from '@cosmjs/proto-signing';
import { ISignInputs } from '../types.js';
import { MsgCreateResource, MsgCreateResourcePayload, MsgCreateResourceResponse, QueryClientImpl, protobufPackage, } from '@cheqd/ts-proto/cheqd/resource/v2/index.js';
import { createPagination, createProtobufRpcClient } from '@cosmjs/stargate';
import { fileTypeFromBuffer } from 'file-type';
import { toString } from 'uint8arrays/to-string';
import { assert } from '@cosmjs/utils';
import { isJSON } from '../utils.js';
import { defaultOracleExtensionKey, MovingAverages, WMAStrategies } from './oracle.js';
import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin.js';
/** Default extension key for resource-related query operations */
export const defaultResourceExtensionKey = 'resource';
/**
* Protobuf message type literals for resource operations.
* Used for consistent message type identification across the module.
*/
export const protobufLiterals = {
/** Create resource message type */
MsgCreateResource: 'MsgCreateResource',
/** Create resource response message type */
MsgCreateResourceResponse: 'MsgCreateResourceResponse',
};
/** Type URL for MsgCreateResource messages */
export const typeUrlMsgCreateResource = `/${protobufPackage}.${protobufLiterals.MsgCreateResource}`;
/** Type URL for MsgCreateResourceResponse messages */
export const typeUrlMsgCreateResourceResponse = `/${protobufPackage}.${protobufLiterals.MsgCreateResourceResponse}`;
/**
* Type guard function to check if an object is a MsgCreateResourceEncodeObject.
*
* @param obj - EncodeObject to check
* @returns True if the object is a MsgCreateResourceEncodeObject
*/
export function isMsgCreateResourceEncodeObject(obj) {
return obj.typeUrl === typeUrlMsgCreateResource;
}
/**
* Sets up the resource extension for the querier client.
* Creates and configures the resource-specific query methods.
*
* @param base - Base QueryClient to extend
* @returns Configured resource extension with query methods
*/
export const setupResourceExtension = (base) => {
const rpc = createProtobufRpcClient(base);
const queryService = new QueryClientImpl(rpc);
return {
[defaultResourceExtensionKey]: {
resource: async (collectionId, resourceId) => {
const { resource } = await queryService.Resource({ collectionId, id: resourceId });
assert(resource);
return resource;
},
resourceMetadata: async (collectionId, resourceId) => {
const { resource } = await queryService.ResourceMetadata({ collectionId, id: resourceId });
assert(resource);
return resource;
},
collectionResources: async (collectionId, paginationKey) => {
const response = await queryService.CollectionResources({
collectionId,
pagination: createPagination(paginationKey),
});
return response;
},
latestResourceVersion: async (collectionId, name, type) => {
const { resource } = await queryService.LatestResourceVersion({
collectionId,
name,
resourceType: type,
});
assert(resource);
return resource;
},
latestResourceVersionMetadata: async (collectionId, name, type) => {
const { resource } = await queryService.LatestResourceVersionMetadata({
collectionId,
name,
resourceType: type,
});
assert(resource);
return resource;
},
params: async () => {
const response = await queryService.Params({});
assert(response.params);
return response;
},
},
};
};
/**
* Resource Module class providing comprehensive linked resource functionality.
* Handles creation, querying, and metadata management of resources linked to DID documents.
*/
export class ResourceModule extends AbstractCheqdSDKModule {
// @ts-expect-error underlying type `GeneratedType` is intentionally wider
static registryTypes = [
[typeUrlMsgCreateResource, MsgCreateResource],
[typeUrlMsgCreateResourceResponse, MsgCreateResourceResponse],
];
/** 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 different resource types.
* Fees vary based on resource content type and processing requirements.
*/
static fees = {
/** Default fee for creating image resources */
DefaultCreateResourceImageFee: { amount: '10000000000', denom: ResourceModule.baseMinimalDenom },
/** Default fee for creating JSON resources */
DefaultCreateResourceJsonFee: { amount: '10000000000', denom: ResourceModule.baseMinimalDenom },
/** Default fee for creating other types of resources */
DefaultCreateResourceDefaultFee: { amount: '10000000000', denom: ResourceModule.baseMinimalDenom },
/** Default fee for creating image resources in USD */
DefaultCreateResourceImageFeeUsd: { amount: '100000000000000000', denom: ResourceModule.baseUsdDenom },
/** Default fee for creating JSON resources in USD */
DefaultCreateResourceJsonFeeUsd: { amount: '400000000000000000', denom: ResourceModule.baseUsdDenom },
/** Default fee for creating other types of resources in USD */
DefaultCreateResourceDefaultFeeUsd: {
amount: '200000000000000000',
denom: ResourceModule.baseUsdDenom,
},
};
/** Standard gas limits for DLR operations.
* These represent the default gas limits for various DLR-related transactions.
*/
static gasLimits = {
/** Gas limit for creating linked resources */
CreateLinkedResourceImageGasLimit: '1000000',
/** Gas limit for creating linked JSON resources */
CreateLinkedResourceJsonGasLimit: '1000000',
/** Gas limit for creating other types of linked resources */
CreateLinkedResourceDefaultGasLimit: '1000000',
};
/** Querier extension setup function for resource operations */
static querierExtensionSetup = setupResourceExtension;
/** Querier instance with resource extension capabilities */
querier;
oracleFeesAvailability;
/**
* Constructs a new resource module instance.
*
* @param signer - Signing client for blockchain transactions
* @param querier - Querier client with resource extension for data retrieval
*/
constructor(signer, querier) {
super(signer, querier);
this.querier = querier;
this.methods = {
createLinkedResourceTx: this.createLinkedResourceTx.bind(this),
queryLinkedResource: this.queryLinkedResource.bind(this),
queryLinkedResourceMetadata: this.queryLinkedResourceMetadata.bind(this),
queryLinkedResources: this.queryLinkedResources.bind(this),
queryLatestLinkedResourceVersion: this.queryLatestLinkedResourceVersion.bind(this),
queryLatestLinkedResourceVersionMetadata: this.queryLatestLinkedResourceVersionMetadata.bind(this),
queryResourceParams: this.queryResourceParams.bind(this),
generateCreateResourceImageFees: this.generateCreateResourceImageFees.bind(this),
generateCreateResourceJsonFees: this.generateCreateResourceJsonFees.bind(this),
generateCreateResourceDefaultFees: this.generateCreateResourceDefaultFees.bind(this),
getPriceRangeFromResourceParams: this.getPriceRangeFromResourceParams.bind(this),
};
}
/**
* Gets the registry types for resource message encoding/decoding.
*
* @returns Iterable of [typeUrl, GeneratedType] pairs for the registry
*/
getRegistryTypes() {
return ResourceModule.registryTypes;
}
/**
* Gets the querier extension setup for resource operations.
*
* @returns Query extension setup function for resource functionality
*/
getQuerierExtensionSetup() {
return ResourceModule.querierExtensionSetup;
}
/**
* Signs a resource payload with provided signature inputs.
* Creates a complete signed resource message ready for blockchain submission.
*
* @param payload - Resource payload to sign
* @param signInputs - Signing inputs or pre-computed signatures
* @returns Promise resolving to the signed resource message
*/
static async signPayload(payload, signInputs) {
const signBytes = MsgCreateResourcePayload.encode(payload).finish();
let signatures;
if (ISignInputs.isSignInput(signInputs)) {
signatures = await CheqdSigningStargateClient.signIdentityTx(signBytes, signInputs);
}
else {
signatures = signInputs;
}
return {
payload,
signatures,
};
}
/**
* Creates a linked resource transaction on the blockchain.
* Handles automatic fee calculation based on resource content type and size.
*
* @param signInputs - Signing inputs or pre-computed signatures for the transaction
* @param resourcePayload - Resource payload containing data and metadata
* @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 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 linked resource data is empty when automatic fee calculation is requested
*/
async createLinkedResourceTx(signInputs, resourcePayload, address, fee, memo, feeOptions, context) {
if (!this._signer) {
this._signer = context.sdk.signer;
}
const payload = MsgCreateResourcePayload.fromPartial(resourcePayload);
const msg = await ResourceModule.signPayload(payload, signInputs);
const encObj = {
typeUrl: typeUrlMsgCreateResource,
value: msg,
};
if (address === '') {
address = (await context.sdk.options.wallet.getAccounts())[0].address;
}
if (!fee) {
if (payload.data.length === 0) {
throw new Error('Linked resource data is empty');
}
fee = await (async function (that) {
const mimeType = await ResourceModule.readMimeType(payload.data);
if (mimeType.startsWith('image/')) {
return await that.generateCreateResourceImageFees(address, undefined, feeOptions, context);
}
if (mimeType.startsWith('application/json')) {
return await that.generateCreateResourceJsonFees(address, undefined, feeOptions, context);
}
return await that.generateCreateResourceDefaultFees(address, undefined, feeOptions, context);
})(this);
}
return this._signer.signAndBroadcast(address, [encObj], fee, memo);
}
/**
* Queries a specific linked resource by collection and resource ID.
* Retrieves the complete resource data along with its metadata.
*
* @param collectionId - ID of the collection containing the resource
* @param resourceId - ID of the resource to query
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the resource with metadata
*/
async queryLinkedResource(collectionId, resourceId, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
return await this.querier[defaultResourceExtensionKey].resource(collectionId, resourceId);
}
/**
* Queries metadata for a specific linked resource.
* Retrieves only the metadata without the resource content data.
*
* @param collectionId - ID of the collection containing the resource
* @param resourceId - ID of the resource to query metadata for
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the resource metadata
*/
async queryLinkedResourceMetadata(collectionId, resourceId, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
return await this.querier[defaultResourceExtensionKey].resourceMetadata(collectionId, resourceId);
}
/**
* Queries all resources in a collection with pagination support.
* Retrieves a list of all resources belonging to a specific collection.
*
* @param collectionId - ID of the collection to query resources for
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the collection resources response with pagination
*/
async queryLinkedResources(collectionId, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
return await this.querier[defaultResourceExtensionKey].collectionResources(collectionId);
}
/**
* Queries the latest version of a linked resource by collection ID, name, and type.
* Retrieves the most recent version of the specified resource.
*
* @param collectionId - ID of the collection containing the resource
* @param name - Name of the resource
* @param type - Type of the resource
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the latest resource version with metadata
*/
async queryLatestLinkedResourceVersion(collectionId, name, type, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
return await this.querier[defaultResourceExtensionKey].latestResourceVersion(collectionId, name, type);
}
/**
* Queries the latest metadata of a linked resource by collection ID, name, and type.
* Retrieves only the metadata of the most recent version of the specified resource.
*
* @param collectionId - ID of the collection containing the resource
* @param name - Name of the resource
* @param type - Type of the resource
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the latest resource metadata
*/
async queryLatestLinkedResourceVersionMetadata(collectionId, name, type, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
return await this.querier[defaultResourceExtensionKey].latestResourceVersionMetadata(collectionId, name, type);
}
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 Resource module parameters from the blockchain.
* @param context - Optional SDK context for accessing clients
* @returns Promise resolving to the Resource module parameters
*/
async queryResourceParams(context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
return await this.querier[defaultResourceExtensionKey].params();
}
/**
* Generates oracle-powered fees for creating image DID-Linked Resources.
*
* @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 DLR creation
*/
async generateCreateResourceImageFees(feePayer, granter, feeOptions, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (!(await this.shouldUseOracleFees(context))) {
return ResourceModule.generateCreateResourceImageFees(feePayer, granter);
}
// fetch fee parameters from the Resource module
const feeParams = await this.queryResourceParams(context);
// get the price range for the image resource creation
const priceRange = await this.getPriceRangeFromResourceParams(feeParams, 'image', feeOptions);
// calculate the oracle fee amount based on the price range and options
return {
amount: [await this.calculateOracleFeeAmount(priceRange, feeOptions, context)],
gas: ResourceModule.gasLimits.CreateLinkedResourceImageGasLimit,
payer: feePayer,
granter,
};
}
/**
* Generates oracle-powered fees for creating JSON DID-Linked Resources.
*
* @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 DLR creation
*/
async generateCreateResourceJsonFees(feePayer, granter, feeOptions, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (!(await this.shouldUseOracleFees(context))) {
return ResourceModule.generateCreateResourceJsonFees(feePayer, granter);
}
// fetch fee parameters from the Resource module
const feeParams = await this.queryResourceParams(context);
// get the price range for the JSON resource creation
const priceRange = await this.getPriceRangeFromResourceParams(feeParams, 'json', feeOptions);
// calculate the oracle fee amount based on the price range and options
return {
amount: [await this.calculateOracleFeeAmount(priceRange, feeOptions, context)],
gas: ResourceModule.gasLimits.CreateLinkedResourceJsonGasLimit,
payer: feePayer,
granter,
};
}
/**
* Generates oracle-powered fees for creating default DID-Linked Resources.
*
* @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 DLR creation
*/
async generateCreateResourceDefaultFees(feePayer, granter, feeOptions, context) {
if (!this.querier) {
this.querier = context.sdk.querier;
}
if (!(await this.shouldUseOracleFees(context))) {
return ResourceModule.generateCreateResourceDefaultFees(feePayer, granter);
}
// fetch fee parameters from the Resource module
const feeParams = await this.queryResourceParams(context);
// get the price range for the default resource creation
const priceRange = await this.getPriceRangeFromResourceParams(feeParams, 'default', feeOptions);
// calculate the oracle fee amount based on the price range and options
return {
amount: [await this.calculateOracleFeeAmount(priceRange, feeOptions, context)],
gas: ResourceModule.gasLimits.CreateLinkedResourceDefaultGasLimit,
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 getPriceRangeFromResourceParams(feeParams, operation, feeOptions) {
const operationFees = (() => {
switch (operation) {
case 'image':
return feeParams.params?.image;
case 'json':
return feeParams.params?.json;
case 'default':
return feeParams.params?.default;
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 === ResourceModule.baseUsdDenom) ??
operationFees?.find((fee) => fee.denom === ResourceModule.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 === ResourceModule.baseUsdDenom
? (feeOptions?.wantedAmountUsd ?? ResourceModule.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 === ResourceModule.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
? ResourceModule.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;
}
/**
* Reads and determines the MIME type of resource content.
* Analyzes the content to identify the appropriate MIME type for fee calculation and validation.
*
* @param content - Resource content as byte array
* @returns Promise resolving to the detected MIME type string
*/
static async readMimeType(content) {
if (isJSON(toString(content, 'utf-8')))
return 'application/json';
return (await fileTypeFromBuffer(content))?.mime ?? 'application/octet-stream';
}
/**
* Generates fee configuration for image resource creation transactions.
* Uses higher gas limits appropriate for image processing and storage.
*
* @param feePayer - Address of the account that will pay the transaction fees
* @param granter - Optional address of the account granting fee payment permissions
* @returns Promise resolving to the fee configuration for image resources
*/
static async generateCreateResourceImageFees(feePayer, granter) {
return {
amount: [ResourceModule.fees.DefaultCreateResourceImageFee],
gas: ResourceModule.gasLimits.CreateLinkedResourceImageGasLimit,
payer: feePayer,
granter: granter,
};
}
/**
* Generates fee configuration for JSON resource creation transactions.
* Uses gas limits optimized for JSON data processing and validation.
*
* @param feePayer - Address of the account that will pay the transaction fees
* @param granter - Optional address of the account granting fee payment permissions
* @returns Promise resolving to the fee configuration for JSON resources
*/
static async generateCreateResourceJsonFees(feePayer, granter) {
return {
amount: [ResourceModule.fees.DefaultCreateResourceJsonFee],
gas: ResourceModule.gasLimits.CreateLinkedResourceJsonGasLimit,
payer: feePayer,
granter: granter,
};
}
/**
* Generates fee configuration for default resource creation transactions.
* Uses standard gas limits for generic resource types and binary data.
*
* @param feePayer - Address of the account that will pay the transaction fees
* @param granter - Optional address of the account granting fee payment permissions
* @returns Promise resolving to the fee configuration for default resources
*/
static async generateCreateResourceDefaultFees(feePayer, granter) {
return {
amount: [ResourceModule.fees.DefaultCreateResourceDefaultFee],
gas: ResourceModule.gasLimits.CreateLinkedResourceDefaultGasLimit,
payer: feePayer,
granter: granter,
};
}
}
//# sourceMappingURL=resource.js.map