UNPKG

@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
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