@hashgraphonline/standards-sdk
Version:
The Hashgraph Online Standards SDK provides a complete implementation of the Hashgraph Consensus Standards (HCS), giving developers all the tools needed to build applications on Hedera.
782 lines (781 loc) • 26.6 kB
JavaScript
import { Client, PrivateKey, Status, AccountUpdateTransaction } from "@hashgraph/sdk";
import { inscribeWithSigner, inscribe } from "./standards-sdk.es28.js";
import { Logger } from "./standards-sdk.es22.js";
import { HederaMirrorNode } from "./standards-sdk.es29.js";
import { ProgressReporter } from "./standards-sdk.es24.js";
import "axios";
import "@hashgraph/proto";
import "buffer";
import "ethers";
import { detectKeyTypeFromString } from "./standards-sdk.es27.js";
import * as mime from "mime-types";
import { z } from "zod";
import { AIAgentCapability, AIAgentType, VerificationType, MCPServerCapability, ProfileType, capabilityNameToCapabilityMap } from "./standards-sdk.es15.js";
const SocialLinkSchema = z.object({
platform: z.string().min(1),
handle: z.string().min(1)
});
const AIAgentDetailsSchema = z.object({
type: z.nativeEnum(AIAgentType),
capabilities: z.array(z.nativeEnum(AIAgentCapability)).min(1),
model: z.string().min(1),
creator: z.string().optional()
});
const MCPServerConnectionInfoSchema = z.object({
url: z.string().min(1),
transport: z.enum(["stdio", "sse"])
});
const MCPServerVerificationSchema = z.object({
type: z.nativeEnum(VerificationType),
value: z.string(),
dns_field: z.string().optional(),
challenge_path: z.string().optional()
});
const MCPServerHostSchema = z.object({
minVersion: z.string().optional()
});
const MCPServerResourceSchema = z.object({
name: z.string().min(1),
description: z.string().min(1)
});
const MCPServerToolSchema = z.object({
name: z.string().min(1),
description: z.string().min(1)
});
const MCPServerDetailsSchema = z.object({
version: z.string().min(1),
connectionInfo: MCPServerConnectionInfoSchema,
services: z.array(z.nativeEnum(MCPServerCapability)).min(1),
description: z.string().min(1),
verification: MCPServerVerificationSchema.optional(),
host: MCPServerHostSchema.optional(),
capabilities: z.array(z.string()).optional(),
resources: z.array(MCPServerResourceSchema).optional(),
tools: z.array(MCPServerToolSchema).optional(),
maintainer: z.string().optional(),
repository: z.string().optional(),
docs: z.string().optional()
});
const BaseProfileSchema = z.object({
version: z.string().min(1),
type: z.nativeEnum(ProfileType),
display_name: z.string().min(1),
alias: z.string().optional(),
bio: z.string().optional(),
socials: z.array(SocialLinkSchema).optional(),
profileImage: z.string().optional(),
properties: z.record(z.any()).optional(),
inboundTopicId: z.string().optional(),
outboundTopicId: z.string().optional()
});
const PersonalProfileSchema = BaseProfileSchema.extend({
type: z.literal(ProfileType.PERSONAL),
language: z.string().optional(),
timezone: z.string().optional()
});
const AIAgentProfileSchema = BaseProfileSchema.extend({
type: z.literal(ProfileType.AI_AGENT),
aiAgent: AIAgentDetailsSchema
});
const MCPServerProfileSchema = BaseProfileSchema.extend({
type: z.literal(ProfileType.MCP_SERVER),
mcpServer: MCPServerDetailsSchema
});
const HCS11ProfileSchema = z.union([
PersonalProfileSchema,
AIAgentProfileSchema,
MCPServerProfileSchema
]);
class HCS11Client {
constructor(config) {
this.client = config.network === "mainnet" ? Client.forMainnet() : Client.forTestnet();
this.auth = config.auth;
this.network = config.network;
this.operatorId = config.auth.operatorId;
this.logger = Logger.getInstance({
level: config.logLevel || "info",
module: "HCS-11",
silent: config.silent
});
this.mirrorNode = new HederaMirrorNode(
this.network,
this.logger
);
if (this.auth.privateKey) {
if (config.keyType) {
this.keyType = config.keyType;
this.initializeOperatorWithKeyType();
} else {
try {
const keyDetection = detectKeyTypeFromString(this.auth.privateKey);
this.keyType = keyDetection.detectedType;
this.client.setOperator(this.operatorId, keyDetection.privateKey);
} catch (error) {
this.logger.warn(
"Failed to detect key type from private key format, will query mirror node"
);
this.keyType = "ed25519";
}
this.initializeOperator();
}
}
}
getClient() {
return this.client;
}
getOperatorId() {
return this.auth.operatorId;
}
async initializeOperator() {
const account = await this.mirrorNode.requestAccount(this.operatorId);
const keyType = account?.key?._type;
if (keyType && keyType.includes("ECDSA")) {
this.keyType = "ecdsa";
} else if (keyType && keyType.includes("ED25519")) {
this.keyType = "ed25519";
} else {
this.keyType = "ed25519";
}
this.initializeOperatorWithKeyType();
}
initializeOperatorWithKeyType() {
if (!this.auth.privateKey) {
return;
}
const PK = this.keyType === "ecdsa" ? PrivateKey.fromStringECDSA(this.auth.privateKey) : PrivateKey.fromStringED25519(this.auth.privateKey);
this.client.setOperator(this.operatorId, PK);
}
createPersonalProfile(displayName, options) {
return {
version: "1.0",
type: ProfileType.PERSONAL,
display_name: displayName,
alias: options?.alias,
bio: options?.bio,
socials: options?.socials,
profileImage: options?.profileImage,
properties: options?.properties,
inboundTopicId: options?.inboundTopicId,
outboundTopicId: options?.outboundTopicId
};
}
createAIAgentProfile(displayName, agentType, capabilities, model, options) {
const validation = this.validateProfile({
version: "1.0",
type: ProfileType.AI_AGENT,
display_name: displayName,
alias: options?.alias,
bio: options?.bio,
socials: options?.socials,
profileImage: options?.profileImage,
properties: options?.properties,
inboundTopicId: options?.inboundTopicId,
outboundTopicId: options?.outboundTopicId,
aiAgent: {
type: agentType,
capabilities,
model,
creator: options?.creator
}
});
if (!validation.valid) {
throw new Error(
`Invalid AI Agent Profile: ${validation.errors.join(", ")}`
);
}
return {
version: "1.0",
type: ProfileType.AI_AGENT,
display_name: displayName,
alias: options?.alias,
bio: options?.bio,
socials: options?.socials,
profileImage: options?.profileImage,
properties: options?.properties,
inboundTopicId: options?.inboundTopicId,
outboundTopicId: options?.outboundTopicId,
aiAgent: {
type: agentType,
capabilities,
model,
creator: options?.creator
}
};
}
/**
* Creates an MCP server profile.
*
* @param displayName - The display name for the MCP server
* @param serverDetails - The MCP server details
* @param options - Additional profile options
* @returns An MCPServerProfile object
*/
createMCPServerProfile(displayName, serverDetails, options) {
const validation = this.validateProfile({
version: "1.0",
type: ProfileType.MCP_SERVER,
display_name: displayName,
alias: options?.alias,
bio: options?.bio,
socials: options?.socials,
profileImage: options?.profileImage,
properties: options?.properties,
inboundTopicId: options?.inboundTopicId,
outboundTopicId: options?.outboundTopicId,
mcpServer: serverDetails
});
if (!validation.valid) {
throw new Error(
`Invalid MCP Server Profile: ${validation.errors.join(", ")}`
);
}
return {
version: "1.0",
type: ProfileType.MCP_SERVER,
display_name: displayName,
alias: options?.alias,
bio: options?.bio,
socials: options?.socials,
profileImage: options?.profileImage,
properties: options?.properties,
inboundTopicId: options?.inboundTopicId,
outboundTopicId: options?.outboundTopicId,
mcpServer: serverDetails
};
}
validateProfile(profile) {
const result = HCS11ProfileSchema.safeParse(profile);
if (result.success) {
return { valid: true, errors: [] };
}
const formattedErrors = result.error.errors.map((err) => {
const path = err.path.join(".");
let message = err.message;
if (err.code === "invalid_type") {
message = `Expected ${err.expected}, got ${err.received}`;
} else if (err.code === "invalid_enum_value") {
const validOptions = err.options?.join(", ");
message = `Invalid value. Valid options are: ${validOptions}`;
} else if (err.code === "too_small" && err.type === "string") {
message = "Cannot be empty";
}
return `${path}: ${message}`;
});
return { valid: false, errors: formattedErrors };
}
profileToJSONString(profile) {
return JSON.stringify(profile);
}
parseProfileFromString(profileStr) {
try {
const parsedProfile = JSON.parse(profileStr);
const validation = this.validateProfile(parsedProfile);
if (!validation.valid) {
this.logger.error("Invalid profile format:", validation.errors);
return null;
}
return parsedProfile;
} catch (error) {
this.logger.error("Error parsing profile:");
return null;
}
}
setProfileForAccountMemo(topicId, topicStandard = 1) {
return `hcs-11:hcs://${topicStandard}/${topicId}`;
}
async executeTransaction(transaction) {
try {
if (this.auth.privateKey) {
const signedTx = await transaction.signWithOperator(this.client);
const response2 = await signedTx.execute(this.client);
const receipt2 = await response2.getReceipt(this.client);
if (receipt2.status.toString() !== Status.Success.toString()) {
return {
success: false,
error: `Transaction failed: ${receipt2.status.toString()}`
};
}
return {
success: true,
result: receipt2
};
}
if (!this.auth.signer) {
throw new Error("No valid authentication method provided");
}
const signer = this.auth.signer;
const frozenTransaction = await transaction.freezeWithSigner(signer);
const response = await frozenTransaction.executeWithSigner(signer);
const receipt = await response.getReceiptWithSigner(signer);
if (receipt.status.toString() !== Status.Success.toString()) {
return {
success: false,
error: `Transaction failed: ${receipt.status.toString()}: ${Status.Success.toString()}`
};
}
return {
success: true,
result: receipt
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error during transaction execution"
};
}
}
async inscribeImage(buffer, fileName, options) {
try {
const progressCallback = options?.progressCallback;
const progressReporter = new ProgressReporter({
module: "HCS11-Image",
logger: this.logger,
callback: progressCallback
});
progressReporter.preparing("Preparing to inscribe image", 0);
const mimeType = mime.lookup(fileName) || "application/octet-stream";
const waitForConfirmation = options?.waitForConfirmation ?? true;
let inscriptionResponse;
if (this.auth.signer) {
if ("accountId" in this.auth.signer) {
progressReporter.preparing("Using signer for inscription", 10);
inscriptionResponse = await inscribeWithSigner(
{
type: "buffer",
buffer,
fileName,
mimeType
},
this.auth.signer,
{
network: this.network,
waitForConfirmation,
waitMaxAttempts: 150,
waitIntervalMs: 4e3,
logging: {
level: "debug"
},
progressCallback: (data) => {
const adjustedPercent = 10 + (data.progressPercent || 0) * 0.8;
progressReporter.report({
stage: data.stage,
message: data.message,
progressPercent: adjustedPercent,
details: data.details
});
}
}
);
} else {
progressReporter.failed(
"Signer must be a DAppSigner for inscription"
);
throw new Error("Signer must be a DAppSigner for inscription");
}
} else {
if (!this.auth.privateKey) {
progressReporter.failed("Private key is required for inscription");
this.logger.error("Private key is required for inscription");
throw new Error("Private key is required for inscription");
}
progressReporter.preparing("Using private key for inscription", 10);
inscriptionResponse = await inscribe(
{
type: "buffer",
buffer,
fileName,
mimeType
},
{
accountId: this.auth.operatorId,
privateKey: this.auth.privateKey,
network: this.network
},
{
waitForConfirmation,
waitMaxAttempts: 150,
waitIntervalMs: 2e3,
logging: {
level: "debug"
},
progressCallback: (data) => {
const adjustedPercent = 10 + (data.progressPercent || 0) * 0.8;
progressReporter.report({
stage: data.stage,
message: data.message,
progressPercent: adjustedPercent,
details: data.details
});
}
}
);
}
if (inscriptionResponse.confirmed) {
progressReporter.completed("Image inscription completed", {
topic_id: inscriptionResponse.inscription.topic_id
});
return {
imageTopicId: inscriptionResponse.inscription.topic_id || "",
transactionId: inscriptionResponse.result.jobId,
success: true
};
} else {
progressReporter.verifying("Waiting for inscription confirmation", 50, {
jobId: inscriptionResponse.result.jobId
});
return {
imageTopicId: "",
transactionId: inscriptionResponse.result.jobId,
success: false,
error: "Inscription not confirmed"
};
}
} catch (error) {
this.logger.error("Error inscribing image:", error);
return {
imageTopicId: "",
transactionId: "",
success: false,
error: error.message || "Error inscribing image"
};
}
}
async inscribeProfile(profile, options) {
this.logger.info("Inscribing HCS-11 profile");
const progressCallback = options?.progressCallback;
const progressReporter = new ProgressReporter({
module: "HCS11-Profile",
logger: this.logger,
callback: progressCallback
});
progressReporter.preparing("Validating profile data", 5);
const validation = this.validateProfile(profile);
if (!validation.valid) {
progressReporter.failed(
`Invalid profile: ${validation.errors.join(", ")}`
);
return {
profileTopicId: "",
transactionId: "",
success: false,
error: `Invalid profile: ${validation.errors.join(", ")}`
};
}
progressReporter.preparing("Formatting profile for inscription", 15);
const profileJson = this.profileToJSONString(profile);
const fileName = `profile-${profile.display_name.toLowerCase().replace(/\s+/g, "-")}.json`;
try {
const contentBuffer = Buffer.from(profileJson, "utf-8");
const contentType = "application/json";
progressReporter.preparing("Preparing profile for inscription", 20);
const input = {
type: "buffer",
buffer: contentBuffer,
fileName,
mimeType: contentType
};
const inscriptionOptions = {
waitForConfirmation: true,
mode: "file",
network: this.network,
waitMaxAttempts: 100,
waitIntervalMs: 2e3,
progressCallback: (data) => {
const adjustedPercent = 20 + Number(data?.progressPercent || 0) * 0.75;
progressReporter?.report({
stage: data.stage,
message: data.message,
progressPercent: adjustedPercent,
details: data.details
});
}
};
progressReporter.submitting("Submitting profile to Hedera network", 30);
let inscriptionResponse;
if (this.auth.privateKey) {
inscriptionResponse = await inscribe(
input,
{
accountId: this.auth.operatorId,
privateKey: this.auth.privateKey,
//this breaks when using privateKey Object
network: this.network
},
inscriptionOptions
);
} else if (this.auth.signer) {
inscriptionResponse = await inscribeWithSigner(
input,
this.auth.signer,
inscriptionOptions
);
} else {
throw new Error("No authentication method available - neither private key nor signer");
}
if (!inscriptionResponse.confirmed || !inscriptionResponse.inscription.topic_id) {
progressReporter.failed("Failed to inscribe profile content");
return {
profileTopicId: "",
transactionId: "",
success: false,
error: "Failed to inscribe profile content"
};
}
const topicId = inscriptionResponse.inscription.topic_id;
progressReporter.completed("Profile inscription completed", {
topicId,
transactionId: inscriptionResponse.result.transactionId
});
return {
profileTopicId: topicId,
transactionId: inscriptionResponse.result.transactionId,
success: true
};
} catch (error) {
progressReporter.failed(
`Error inscribing profile: ${error.message || "Unknown error"}`
);
return {
profileTopicId: "",
transactionId: "",
success: false,
error: error.message || "Unknown error during inscription"
};
}
}
async updateAccountMemoWithProfile(accountId, profileTopicId) {
try {
this.logger.info(
`Updating account memo for ${accountId} with profile ${profileTopicId}`
);
const memo = this.setProfileForAccountMemo(profileTopicId);
const transaction = new AccountUpdateTransaction().setAccountMemo(memo).setAccountId(accountId);
return this.executeTransaction(transaction);
} catch (error) {
this.logger.error(
`Error updating account memo: ${error instanceof Error ? error.message : "Unknown error"}`
);
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error updating account memo"
};
}
}
/**
* Creates and inscribes a profile.
*
* @param profile - The profile to create and inscribe.
* @param updateAccountMemo - Whether to update the account memo with the profile.
* @param options - Optional configuration options.
* @returns A promise that resolves to the inscription result.
*/
async createAndInscribeProfile(profile, updateAccountMemo = true, options) {
const progressCallback = options?.progressCallback;
const progressReporter = new ProgressReporter({
module: "HCS11-ProfileCreation",
logger: this.logger,
callback: progressCallback
});
progressReporter.preparing("Starting profile creation process", 0);
const inscriptionProgress = progressReporter.createSubProgress({
minPercent: 0,
maxPercent: 80,
logPrefix: "Inscription"
});
const inscriptionResult = await this.inscribeProfile(profile, {
...options,
progressCallback: (data) => {
inscriptionProgress.report({
stage: data.stage,
message: data.message,
progressPercent: data.progressPercent,
details: data.details
});
}
});
if (!inscriptionResult?.success) {
progressReporter.failed("Profile inscription failed", {
error: inscriptionResult?.error
});
return inscriptionResult;
}
progressReporter.confirming("Profile inscribed, updating account memo", 85);
if (updateAccountMemo) {
const memoResult = await this.updateAccountMemoWithProfile(
this.auth.operatorId,
inscriptionResult.profileTopicId
);
if (!memoResult.success) {
progressReporter.failed("Failed to update account memo", {
error: memoResult?.error
});
return {
...inscriptionResult,
success: false,
error: memoResult?.error
};
}
}
progressReporter.completed("Profile creation completed successfully", {
profileTopicId: inscriptionResult.profileTopicId,
transactionId: inscriptionResult.transactionId
});
return inscriptionResult;
}
/**
* Gets the capabilities from the capability names.
*
* @param capabilityNames - The capability names to get the capabilities for.
* @returns The capabilities.
*/
async getCapabilitiesFromTags(capabilityNames) {
const capabilities = [];
if (capabilityNames.length === 0) {
return [AIAgentCapability.TEXT_GENERATION];
}
for (const capabilityName of capabilityNames) {
const capability = capabilityNameToCapabilityMap[capabilityName.toLowerCase()];
if (capability !== void 0 && !capabilities.includes(capability)) {
capabilities.push(capability);
}
}
if (capabilities.length === 0) {
capabilities.push(AIAgentCapability.TEXT_GENERATION);
}
return capabilities;
}
/**
* Gets the agent type from the metadata.
*
* @param metadata - The metadata of the agent.
* @returns The agent type.
*/
getAgentTypeFromMetadata(metadata) {
if (metadata.type === "autonomous") {
return AIAgentType.AUTONOMOUS;
} else {
return AIAgentType.MANUAL;
}
}
/**
* Fetches a profile from the account memo.
*
* @param accountId - The account ID of the agent to fetch the profile for.
* @param network - The network to use for the fetch.
* @returns A promise that resolves to the profile.
*/
async fetchProfileByAccountId(accountId, network) {
try {
this.logger.info(
`Fetching profile for account ${accountId.toString()} on ${this.network}`
);
const memo = await this.mirrorNode.getAccountMemo(accountId.toString());
this.logger.info(`Got account memo: ${memo}`);
if (!memo?.startsWith("hcs-11:")) {
return {
success: false,
error: `Account ${accountId.toString()} does not have a valid HCS-11 memo`
};
}
this.logger.info(`Found HCS-11 memo: ${memo}`);
const protocolReference = memo.substring(7);
if (protocolReference?.startsWith("hcs://")) {
const hcsFormat = protocolReference.match(/hcs:\/\/(\d+)\/(.+)/);
if (!hcsFormat) {
return {
success: false,
error: `Invalid HCS protocol reference format: ${protocolReference}`
};
}
const [_, protocolId, profileTopicId] = hcsFormat;
const networkParam = network || this.network || "mainnet";
this.logger.info(
`Retrieving profile from Kiloscribe CDN: ${profileTopicId}`
);
const cdnUrl = `https://kiloscribe.com/api/inscription-cdn/${profileTopicId}?network=${networkParam}`;
try {
const response = await fetch(cdnUrl);
if (!response.ok) {
return {
success: false,
error: `Failed to fetch profile from Kiloscribe CDN: ${response.statusText}`
};
}
const profileData = await response.json();
if (!profileData) {
return {
success: false,
error: `No profile data found for topic ${profileTopicId}`
};
}
return {
success: true,
profile: profileData,
topicInfo: {
inboundTopic: profileData.inboundTopicId,
outboundTopic: profileData.outboundTopicId,
profileTopicId
}
};
} catch (cdnError) {
this.logger.error(
`Error retrieving from Kiloscribe CDN: ${cdnError.message}`
);
return {
success: false,
error: `Error retrieving from Kiloscribe CDN: ${cdnError.message}`
};
}
} else if (protocolReference.startsWith("ipfs://")) {
this.logger.warn("IPFS protocol references are not fully supported");
const response = await fetch(
`https://ipfs.io/ipfs/${protocolReference.replace("ipfs://", "")}`
);
const profileData = await response.json();
return {
success: true,
profile: profileData,
topicInfo: {
inboundTopic: profileData.inboundTopicId,
outboundTopic: profileData.outboundTopicId,
profileTopicId: profileData.profileTopicId
}
};
} else if (protocolReference.startsWith("ar://")) {
const arTxId = protocolReference.replace("ar://", "");
const response = await fetch(`https://arweave.net/${arTxId}`);
if (!response.ok) {
return {
success: false,
error: `Failed to fetch profile from Arweave ${arTxId}: ${response.statusText}`
};
}
const profileData = await response.json();
return {
success: true,
profile: profileData,
topicInfo: {
inboundTopic: profileData.inboundTopicId,
outboundTopic: profileData.outboundTopicId,
profileTopicId: profileData.profileTopicId
}
};
} else {
return {
success: false,
error: `Invalid protocol reference format: ${protocolReference}`
};
}
} catch (error) {
this.logger.error(`Error fetching profile: ${error.message}`);
return {
success: false,
error: `Error fetching profile: ${error.message}`
};
}
}
}
export {
HCS11Client
};
//# sourceMappingURL=standards-sdk.es14.js.map