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.

1,348 lines • 68.2 kB
import { Client, PrivateKey, AccountCreateTransaction, Hbar, CustomFixedFee, AccountId, TokenId, KeyList, TopicCreateTransaction, PublicKey, TopicMessageSubmitTransaction, TopicId, Transaction, ScheduleCreateTransaction, Timestamp } from "@hashgraph/sdk"; import { AccountCreationError, TopicCreationError, ConnectionConfirmationError, PayloadSizeError } from "./standards-sdk.es6.js"; import { I as InscriptionSDK } from "./standards-sdk.es33.js"; import { Logger } from "./standards-sdk.es22.js"; import { accountIdsToExemptKeys } from "./standards-sdk.es23.js"; import "axios"; import "@hashgraph/proto"; import "buffer"; import "ethers"; import { detectKeyTypeFromString } from "./standards-sdk.es27.js"; import { HCS10BaseClient, Hcs10MemoType } from "./standards-sdk.es5.js"; import * as mime from "mime-types"; import { InboundTopicType } from "./standards-sdk.es15.js"; import { HCS11Client } from "./standards-sdk.es14.js"; import { inscribe } from "./standards-sdk.es28.js"; import { addSeconds } from "date-fns"; class HCS10Client extends HCS10BaseClient { constructor(config) { super({ network: config.network, logLevel: config.logLevel, prettyPrint: config.prettyPrint, feeAmount: config.feeAmount, mirrorNode: config.mirrorNode, silent: config.silent, keyType: config.keyType }); this.client = config.network === "mainnet" ? Client.forMainnet() : Client.forTestnet(); this.operatorPrivateKey = config.operatorPrivateKey; this.operatorAccountId = config.operatorId; if (config.keyType) { this.keyType = config.keyType; const PK = this.keyType === "ecdsa" ? PrivateKey.fromStringECDSA(this.operatorPrivateKey) : PrivateKey.fromStringED25519(this.operatorPrivateKey); this.client.setOperator(config.operatorId, PK); } else { try { const keyDetection = detectKeyTypeFromString(this.operatorPrivateKey); this.client.setOperator(config.operatorId, keyDetection.privateKey); this.keyType = keyDetection.detectedType; } catch (error) { this.logger.warn( "Failed to detect key type from private key format, will query mirror node" ); this.keyType = "ed25519"; } this.initializeOperator(); } this.network = config.network; this.logger = Logger.getInstance({ level: config.logLevel || "info", module: "HCS-SDK", silent: config.silent }); this.guardedRegistryBaseUrl = config.guardedRegistryBaseUrl || "https://moonscape.tech"; this.hcs11Client = new HCS11Client({ network: config.network, auth: { operatorId: config.operatorId, privateKey: config.operatorPrivateKey }, logLevel: config.logLevel, silent: config.silent, keyType: config.keyType }); } async initializeOperator() { const account = await this.requestAccount(this.operatorAccountId); const keyType = account?.key?._type; if (keyType.includes("ECDSA")) { this.keyType = "ecdsa"; } else if (keyType.includes("ED25519")) { this.keyType = "ed25519"; } else { this.keyType = "ed25519"; } const PK = this.keyType === "ecdsa" ? PrivateKey.fromStringECDSA(this.operatorPrivateKey) : PrivateKey.fromStringED25519(this.operatorPrivateKey); this.logger.debug( `Setting operator: ${this.operatorAccountId} with key type: ${this.keyType}` ); this.client.setOperator(this.operatorAccountId, PK); return { accountId: this.operatorAccountId, privateKey: this.operatorPrivateKey, keyType: this.keyType, client: this.client }; } getClient() { return this.client; } /** * Creates a new Hedera account * @param initialBalance Optional initial balance in HBAR (default: 50) * @returns Object with account ID and private key */ async createAccount(initialBalance = 50) { if (!this.keyType) { await this.initializeOperator(); } this.logger.info( `Creating new account with ${initialBalance} HBAR initial balance` ); const newKey = PrivateKey.generateED25519(); const accountTransaction = new AccountCreateTransaction().setKeyWithoutAlias(newKey.publicKey).setInitialBalance(new Hbar(initialBalance)); this.logger.debug("Executing account creation transaction"); const accountResponse = await accountTransaction.execute(this.client); const accountReceipt = await accountResponse.getReceipt(this.client); const newAccountId = accountReceipt.accountId; if (!newAccountId) { this.logger.error("Account creation failed: accountId is null"); throw new AccountCreationError( "Failed to create account: accountId is null" ); } this.logger.info( `Account created successfully: ${newAccountId.toString()}` ); return { accountId: newAccountId.toString(), privateKey: newKey.toString() }; } /** * Creates an inbound topic for an agent * @param accountId The account ID associated with the inbound topic * @param topicType Type of inbound topic (public, controlled, or fee-based) * @param ttl Optional Time-To-Live for the topic memo, defaults to 60 * @param feeConfigBuilder Optional fee configuration builder for fee-based topics * @returns The topic ID of the created inbound topic */ async createInboundTopic(accountId, topicType, ttl = 60, feeConfigBuilder) { if (!this.keyType) { await this.initializeOperator(); } const memo = this._generateHcs10Memo(Hcs10MemoType.INBOUND, { accountId, ttl }); let submitKey; let finalFeeConfig; switch (topicType) { case InboundTopicType.PUBLIC: submitKey = false; break; case InboundTopicType.CONTROLLED: submitKey = true; break; case InboundTopicType.FEE_BASED: submitKey = false; if (!feeConfigBuilder) { throw new Error( "Fee configuration builder is required for fee-based topics" ); } const internalFees = feeConfigBuilder.customFees; internalFees.forEach((fee) => { if (!fee.feeCollectorAccountId) { this.logger.debug( `Defaulting fee collector for token ${fee.feeTokenId || "HBAR"} to agent ${accountId}` ); fee.feeCollectorAccountId = accountId; } }); finalFeeConfig = feeConfigBuilder.build(); break; default: throw new Error(`Unsupported inbound topic type: ${topicType}`); } return this.createTopic(memo, true, submitKey, finalFeeConfig); } /** * Creates a new agent with inbound and outbound topics * @param builder The agent builder object * @param ttl Optional Time-To-Live for the topic memos, defaults to 60 * @param existingState Optional existing state to resume from * @returns Object with topic IDs */ async createAgent(builder, ttl = 60, existingState, progressCallback) { if (!this.keyType) { await this.initializeOperator(); } const config = builder.build(); const accountId = this.client.operatorAccountId?.toString(); if (!accountId) { throw new Error("Failed to retrieve operator account ID"); } const result = await this._createEntityTopics( ttl, { outboundTopicId: existingState?.outboundTopicId || "", inboundTopicId: existingState?.inboundTopicId || "", pfpTopicId: existingState?.pfpTopicId || config.existingPfpTopicId || "", profileTopicId: existingState?.profileTopicId || "" }, accountId, config.inboundTopicType, config.feeConfig, config.pfpBuffer, config.pfpFileName, progressCallback ); if (!result.profileTopicId) { if (progressCallback) { progressCallback({ stage: "preparing", message: "Creating agent profile", progressPercent: 60, details: { outboundTopicId: result.outboundTopicId, inboundTopicId: result.inboundTopicId, pfpTopicId: result.pfpTopicId, state: { currentStage: "profile", completedPercentage: 60 } } }); } const profileResult = await this.storeHCS11Profile( config.name, config.bio, result.inboundTopicId, result.outboundTopicId, config.capabilities, config.metadata, config.pfpBuffer && config.pfpBuffer.length > 0 && !result.pfpTopicId ? config.pfpBuffer : void 0, config.pfpFileName, result.pfpTopicId ); result.profileTopicId = profileResult.profileTopicId; this.logger.info( `Profile stored with topic ID: ${result.profileTopicId}` ); if (progressCallback) { progressCallback({ stage: "preparing", message: "Agent profile created", progressPercent: 70, details: { outboundTopicId: result.outboundTopicId, inboundTopicId: result.inboundTopicId, pfpTopicId: result.pfpTopicId, profileTopicId: result.profileTopicId, state: { currentStage: "profile", completedPercentage: 70 } } }); } } else { this.logger.info( `Using existing profile topic ID: ${result.profileTopicId}` ); } return result; } /** * Inscribes a profile picture to Hedera * @param buffer Profile picture buffer * @param fileName Filename * @returns Response with topic ID and transaction ID */ async inscribePfp(buffer, fileName) { try { this.logger.info("Inscribing profile picture using HCS-11 client"); const imageResult = await this.hcs11Client.inscribeImage( buffer, fileName ); if (!imageResult.success) { this.logger.error( `Failed to inscribe profile picture: ${imageResult.error}` ); throw new Error( imageResult?.error || "Failed to inscribe profile picture" ); } this.logger.info( `Successfully inscribed profile picture with topic ID: ${imageResult.imageTopicId}` ); return { pfpTopicId: imageResult.imageTopicId, transactionId: imageResult.transactionId, success: true }; } catch (e) { const error = e; const logMessage = `Error inscribing profile picture: ${error.message}`; this.logger.error(logMessage); return { pfpTopicId: "", transactionId: "", success: false, error: error.message }; } } /** * Stores an HCS-11 profile for an agent * @param agentName Agent name * @param agentBio Agent description * @param inboundTopicId Inbound topic ID * @param outboundTopicId Outbound topic ID * @param capabilities Agent capability tags * @param metadata Additional metadata * @param pfpBuffer Optional profile picture buffer * @param pfpFileName Optional profile picture filename * @returns Response with topic IDs and transaction ID */ async storeHCS11Profile(agentName, agentBio, inboundTopicId, outboundTopicId, capabilities = [], metadata, pfpBuffer, pfpFileName, existingPfpTopicId) { try { let pfpTopicId = existingPfpTopicId || ""; if (!pfpTopicId && pfpBuffer && pfpFileName) { this.logger.info("Inscribing profile picture for HCS-11 profile"); const pfpResult = await this.inscribePfp(pfpBuffer, pfpFileName); if (!pfpResult.success) { this.logger.warn( `Failed to inscribe profile picture: ${pfpResult.error}, proceeding without pfp` ); } else { pfpTopicId = pfpResult.pfpTopicId; } } else if (existingPfpTopicId) { this.logger.info( `Using existing profile picture with topic ID: ${existingPfpTopicId} for HCS-11 profile` ); pfpTopicId = existingPfpTopicId; } const agentType = this.hcs11Client.getAgentTypeFromMetadata({ type: metadata.type || "autonomous" }); const formattedSocials = metadata.socials ? Object.entries(metadata.socials).filter(([_, handle]) => handle).map(([platform, handle]) => ({ platform, handle })) : void 0; const profile = this.hcs11Client.createAIAgentProfile( agentName, agentType, capabilities, metadata.model || "unknown", { alias: agentName.toLowerCase().replace(/\s+/g, "_"), bio: agentBio, profileImage: pfpTopicId ? `hcs://1/${pfpTopicId}` : void 0, socials: formattedSocials, properties: metadata.properties, inboundTopicId, outboundTopicId, creator: metadata.creator } ); const profileResult = await this.hcs11Client.createAndInscribeProfile( profile, true ); if (!profileResult.success) { this.logger.error(`Failed to inscribe profile: ${profileResult.error}`); throw new Error(profileResult.error || "Failed to inscribe profile"); } this.logger.info( `Profile inscribed with topic ID: ${profileResult.profileTopicId}, transaction ID: ${profileResult.transactionId}` ); return { profileTopicId: profileResult.profileTopicId, pfpTopicId, transactionId: profileResult.transactionId, success: true }; } catch (e) { const error = e; const logMessage = `Error storing HCS-11 profile: ${error.message}`; this.logger.error(logMessage); return { profileTopicId: "", pfpTopicId: "", transactionId: "", success: false, error: error.message }; } } async setupFees(transaction, feeConfig, additionalExemptAccounts = []) { let modifiedTransaction = transaction; if (!this.client.operatorPublicKey) { return modifiedTransaction; } if (!feeConfig.customFees || feeConfig.customFees.length === 0) { this.logger.warn("No custom fees provided in fee config for setupFees"); return modifiedTransaction; } if (feeConfig.customFees.length > 10) { this.logger.warn( "More than 10 custom fees provided, only the first 10 will be used" ); feeConfig.customFees = feeConfig.customFees.slice(0, 10); } const customFees = feeConfig.customFees.map((fee) => { if (!fee.feeCollectorAccountId) { this.logger.error( "Internal Error: Fee collector ID missing in setupFees" ); return null; } if (fee.type === "FIXED_FEE") { const customFee = new CustomFixedFee().setAmount(Number(fee.feeAmount.amount)).setFeeCollectorAccountId( AccountId.fromString(fee.feeCollectorAccountId) ); if (fee.feeTokenId) { customFee.setDenominatingTokenId( TokenId.fromString(fee.feeTokenId) ); } return customFee; } return null; }).filter(Boolean); if (customFees.length === 0) { this.logger.warn("No valid custom fees to apply in setupFees"); return modifiedTransaction; } const exemptAccountIds = [ ...feeConfig.exemptAccounts || [], ...additionalExemptAccounts ]; if (exemptAccountIds.length > 0) { modifiedTransaction = await this.setupExemptKeys( transaction, exemptAccountIds ); } return modifiedTransaction.setFeeScheduleKey(this.client.operatorPublicKey).setCustomFees(customFees); } async setupExemptKeys(transaction, exemptAccountIds) { let modifiedTransaction = transaction; const uniqueExemptAccountIds = Array.from(new Set(exemptAccountIds)); const filteredExemptAccounts = uniqueExemptAccountIds.filter( (account) => account !== this.client.operatorAccountId?.toString() ); let exemptKeys = []; if (filteredExemptAccounts.length > 0) { try { exemptKeys = await accountIdsToExemptKeys( filteredExemptAccounts, this.network, this.logger ); } catch (e) { const error = e; const logMessage = `Error getting exempt keys: ${error.message}, continuing without exempt keys`; this.logger.warn(logMessage); } } if (exemptKeys.length > 0) { modifiedTransaction = modifiedTransaction.setFeeExemptKeys(exemptKeys); } return modifiedTransaction; } /** * Handles a connection request from another account * @param inboundTopicId Inbound topic ID of your agent * @param requestingAccountId Requesting account ID * @param connectionRequestId Connection request ID * @param connectionFeeConfig Optional fee configuration for the connection topic * @param ttl Optional ttl parameter with default * @returns Response with connection details */ async handleConnectionRequest(inboundTopicId, requestingAccountId, connectionRequestId, connectionFeeConfig, ttl = 60) { const memo = this._generateHcs10Memo(Hcs10MemoType.CONNECTION, { ttl, inboundTopicId, connectionId: connectionRequestId }); this.logger.info( `Handling connection request ${connectionRequestId} from ${requestingAccountId}` ); const accountId = this.getClient().operatorAccountId?.toString(); if (!accountId) { throw new Error("Failed to retrieve operator account ID"); } let requesterKey = await this.mirrorNode.getPublicKey(requestingAccountId); const accountKey = await this.mirrorNode.getPublicKey(accountId); if (!accountKey) { throw new Error("Failed to retrieve public key"); } const thresholdKey = new KeyList([accountKey, requesterKey], 1); let connectionTopicId; try { if (connectionFeeConfig) { const feeConfig = connectionFeeConfig.build(); const modifiedFeeConfig = { ...feeConfig, exemptAccounts: [...feeConfig.exemptAccounts || []] }; connectionTopicId = await this.createTopic( memo, thresholdKey, thresholdKey, modifiedFeeConfig ); } else { connectionTopicId = await this.createTopic( memo, thresholdKey, thresholdKey ); } this.logger.info(`Created new connection topic ID: ${connectionTopicId}`); } catch (error) { const logMessage = `Failed to create connection topic: ${error}`; this.logger.error(logMessage); throw new TopicCreationError(logMessage); } const operatorId = `${inboundTopicId}@${accountId}`; const confirmedConnectionSequenceNumber = await this.confirmConnection( inboundTopicId, connectionTopicId, requestingAccountId, connectionRequestId, "Connection accepted. Looking forward to collaborating!" ); const accountTopics = await this.retrieveCommunicationTopics(accountId); const requestingAccountTopics = await this.retrieveCommunicationTopics(requestingAccountId); const requestingAccountOperatorId = `${requestingAccountTopics.inboundTopic}@${requestingAccountId}`; await this.recordOutboundConnectionConfirmation({ outboundTopicId: accountTopics.outboundTopic, requestorOutboundTopicId: requestingAccountTopics.outboundTopic, connectionRequestId, confirmedRequestId: confirmedConnectionSequenceNumber, connectionTopicId, operatorId: requestingAccountOperatorId, memo: `Connection established with ${requestingAccountId}` }); return { connectionTopicId, confirmedConnectionSequenceNumber, operatorId }; } /** * Confirms a connection request from another account * @param inboundTopicId Inbound topic ID * @param connectionTopicId Connection topic ID * @param connectedAccountId Connected account ID * @param connectionId Connection ID * @param memo Memo for the connection request * @param submitKey Optional submit key * @returns Sequence number of the confirmed connection */ async confirmConnection(inboundTopicId, connectionTopicId, connectedAccountId, connectionId, memo, submitKey) { const operatorId = await this.getOperatorId(); this.logger.info(`Confirming connection with ID ${connectionId}`); const payload = { p: "hcs-10", op: "connection_created", connection_topic_id: connectionTopicId, connected_account_id: connectedAccountId, operator_id: operatorId, connection_id: connectionId, m: memo }; const submissionCheck = await this.canSubmitToTopic( inboundTopicId, this.client.operatorAccountId?.toString() || "" ); const result = await this.submitPayload( inboundTopicId, payload, submitKey, submissionCheck.requiresFee ); const sequenceNumber = result.topicSequenceNumber?.toNumber(); if (!sequenceNumber) { throw new ConnectionConfirmationError( "Failed to confirm connection: sequence number is null" ); } return sequenceNumber; } async sendMessage(connectionTopicId, data, memo, submitKey, options) { const submissionCheck = await this.canSubmitToTopic( connectionTopicId, this.client.operatorAccountId?.toString() || "" ); const operatorId = await this.getOperatorId(); const payload = { p: "hcs-10", op: "message", operator_id: operatorId, data, m: memo }; const payloadString = JSON.stringify(payload); const isLargePayload = Buffer.from(payloadString).length > 1e3; if (isLargePayload) { this.logger.info( "Message payload exceeds 1000 bytes, storing via inscription" ); try { const contentBuffer = Buffer.from(data); const fileName = `message-${Date.now()}.json`; const inscriptionResult = await this.inscribeFile( contentBuffer, fileName, { progressCallback: options?.progressCallback, waitMaxAttempts: options?.waitMaxAttempts, waitIntervalMs: options?.waitIntervalMs } ); if (inscriptionResult?.topic_id) { payload.data = `hcs://1/${inscriptionResult.topic_id}`; this.logger.info( `Large message inscribed with topic ID: ${inscriptionResult.topic_id}` ); } else { throw new Error("Failed to inscribe large message content"); } } catch (error) { const logMessage = `Error inscribing large message: ${error.message}`; this.logger.error(logMessage); throw new Error(logMessage); } } this.logger.info("Submitting message to connection topic", payload); return await this.submitPayload( connectionTopicId, payload, submitKey, submissionCheck.requiresFee ); } async createTopic(memo, adminKey, submitKey, feeConfig) { this.logger.info("Creating topic"); const transaction = new TopicCreateTransaction().setTopicMemo(memo); if (adminKey) { if (typeof adminKey === "boolean" && adminKey && this.client.operatorPublicKey) { transaction.setAdminKey(this.client.operatorPublicKey); transaction.setAutoRenewAccountId(this.client.operatorAccountId); } else if (adminKey instanceof PublicKey || adminKey instanceof KeyList) { transaction.setAdminKey(adminKey); if (this.client.operatorAccountId) { transaction.setAutoRenewAccountId(this.client.operatorAccountId); } } } if (submitKey) { if (typeof submitKey === "boolean" && submitKey && this.client.operatorPublicKey) { transaction.setSubmitKey(this.client.operatorPublicKey); } else if (submitKey instanceof PublicKey || submitKey instanceof KeyList) { transaction.setSubmitKey(submitKey); } } if (feeConfig) { await this.setupFees(transaction, feeConfig); } this.logger.debug("Executing topic creation transaction"); const txResponse = await transaction.execute(this.client); const receipt = await txResponse.getReceipt(this.client); if (!receipt.topicId) { this.logger.error("Failed to create topic: topicId is null"); throw new Error("Failed to create topic: topicId is null"); } const topicId = receipt.topicId.toString(); return topicId; } async submitPayload(topicId, payload, submitKey, requiresFee = false) { const message = typeof payload === "string" ? payload : JSON.stringify(payload); const payloadSizeInBytes = Buffer.byteLength(message, "utf8"); if (payloadSizeInBytes > 1e3) { throw new PayloadSizeError( "Payload size exceeds 1000 bytes limit", payloadSizeInBytes ); } const transaction = new TopicMessageSubmitTransaction().setTopicId(TopicId.fromString(topicId)).setMessage(message); const transactionMemo = this.getHcs10TransactionMemo(payload); if (transactionMemo) { transaction.setTransactionMemo(transactionMemo); } if (requiresFee) { this.logger.info( "Topic requires fee payment, setting max transaction fee" ); transaction.setMaxTransactionFee(new Hbar(this.feeAmount)); } let transactionResponse; if (submitKey) { const frozenTransaction = transaction.freezeWith(this.client); const signedTransaction = await frozenTransaction.sign(submitKey); transactionResponse = await signedTransaction.execute(this.client); } else { transactionResponse = await transaction.execute(this.client); } const receipt = await transactionResponse.getReceipt(this.client); if (!receipt) { this.logger.error("Failed to submit message: receipt is null"); throw new Error("Failed to submit message: receipt is null"); } this.logger.info("Message submitted successfully"); return receipt; } async inscribeFile(buffer, fileName, options) { this.logger.info("Inscribing file"); if (!this.client.operatorAccountId) { this.logger.error("Operator account ID is not set"); throw new Error("Operator account ID is not set"); } if (!this.operatorPrivateKey) { this.logger.error("Operator private key is not set"); throw new Error("Operator private key is not set"); } const mimeType = mime.lookup(fileName) || "application/octet-stream"; const sdk = await InscriptionSDK.createWithAuth({ type: "server", accountId: this.client.operatorAccountId.toString(), privateKey: this.operatorPrivateKey, network: this.network }); const inscriptionOptions = { mode: "file", waitForConfirmation: true, waitMaxAttempts: options?.waitMaxAttempts || 30, waitIntervalMs: options?.waitIntervalMs || 4e3, progressCallback: options?.progressCallback, logging: { level: this.logger.getLevel ? this.logger.getLevel() : "info" } }; const response = await inscribe( { type: "buffer", buffer, fileName, mimeType }, { accountId: this.client.operatorAccountId.toString(), privateKey: this.operatorPrivateKey.toString(), network: this.network }, inscriptionOptions, sdk ); if (!response.confirmed || !response.inscription) { throw new Error("Inscription was not confirmed"); } return response.inscription; } /** * Waits for confirmation of a connection request * @param inboundTopicId Inbound topic ID * @param connectionRequestId Connection request ID * @param maxAttempts Maximum number of attempts * @param delayMs Delay between attempts in milliseconds * @returns Connection confirmation details */ async waitForConnectionConfirmation(inboundTopicId, connectionRequestId, maxAttempts = 60, delayMs = 2e3, recordConfirmation = true) { this.logger.info( `Waiting for connection confirmation on inbound topic ${inboundTopicId} for request ID ${connectionRequestId}` ); for (let attempt = 0; attempt < maxAttempts; attempt++) { this.logger.info( `Attempt ${attempt + 1}/${maxAttempts} to find connection confirmation` ); const messages = await this.mirrorNode.getTopicMessages(inboundTopicId); const connectionCreatedMessages = messages.filter( (m) => m.op === "connection_created" ); this.logger.info( `Found ${connectionCreatedMessages.length} connection_created messages` ); if (connectionCreatedMessages.length > 0) { for (const message of connectionCreatedMessages) { if (Number(message.connection_id) === Number(connectionRequestId)) { const confirmationResult = { connectionTopicId: message.connection_topic_id, sequence_number: Number(message.sequence_number), confirmedBy: message.operator_id, memo: message.m }; const confirmedByAccountId = this.extractAccountFromOperatorId( confirmationResult.confirmedBy ); const account = this.getAccountAndSigner(); const confirmedByConnectionTopics = await this.retrieveCommunicationTopics(confirmedByAccountId); const agentConnectionTopics = await this.retrieveCommunicationTopics(account.accountId); this.logger.info( "Connection confirmation found", confirmationResult ); if (recordConfirmation) { await this.recordOutboundConnectionConfirmation({ requestorOutboundTopicId: confirmedByConnectionTopics.outboundTopic, outboundTopicId: agentConnectionTopics.outboundTopic, connectionRequestId, confirmedRequestId: confirmationResult.sequence_number, connectionTopicId: confirmationResult.connectionTopicId, operatorId: confirmationResult.confirmedBy, memo: confirmationResult.memo || "Connection confirmed" }); } return confirmationResult; } } } if (attempt < maxAttempts - 1) { this.logger.info( `No matching confirmation found, waiting ${delayMs}ms before retrying...` ); await new Promise((resolve) => setTimeout(resolve, delayMs)); } } throw new Error( `Connection confirmation not found after ${maxAttempts} attempts for request ID ${connectionRequestId}` ); } getAccountAndSigner() { const PK = this.keyType === "ecdsa" ? PrivateKey.fromStringECDSA(this.operatorPrivateKey) : PrivateKey.fromStringED25519(this.operatorPrivateKey); return { accountId: this.client.operatorAccountId.toString(), signer: PK }; } /** * Creates and registers an agent with a Guarded registry. * * This function performs the following steps: * 1. Creates a new account if no existing account is provided. * 2. Initializes an HCS10 client with the new account. * 3. Creates an agent on the client. * 4. Registers the agent with the Hashgraph Online Guarded Registry. * * @param builder The agent builder object * @param options Optional configuration including progress callback and state management * @returns Agent registration result */ async createAndRegisterAgent(builder, options) { try { const config = builder.build(); const progressCallback = options?.progressCallback; const baseUrl = options?.baseUrl || this.guardedRegistryBaseUrl; let state = options?.existingState || { currentStage: "init", completedPercentage: 0, createdResources: [] }; state.agentMetadata = config.metadata; if (progressCallback) { progressCallback({ stage: "preparing", message: "Starting agent creation process", progressPercent: 0, details: { state } }); } let account = config.existingAccount; let agentClient; if (!state.inboundTopicId || !state.outboundTopicId || !state.profileTopicId) { if (!account) { if (state.createdResources && state.createdResources.some((r) => r.startsWith("account:"))) { const accountResource = state.createdResources.find( (r) => r.startsWith("account:") ); const existingAccountId = accountResource?.split(":")[1]; if (existingAccountId && config.existingAccount) { account = config.existingAccount; this.logger.info( `Resuming with existing account: ${existingAccountId}` ); } else { account = await this.createAccount(options?.initialBalance); state.createdResources = state.createdResources || []; state.createdResources.push(`account:${account.accountId}`); } } else { account = await this.createAccount(options?.initialBalance); state.createdResources = state.createdResources || []; state.createdResources.push(`account:${account.accountId}`); } } if (progressCallback) { progressCallback({ stage: "preparing", message: "Created account or using existing account", progressPercent: 20, details: { state, account } }); } const keyType = detectKeyTypeFromString(account.privateKey); const privateKey = keyType.detectedType === "ed25519" ? PrivateKey.fromStringED25519(account.privateKey) : PrivateKey.fromStringECDSA(account.privateKey); const publicKey = privateKey.publicKey.toString(); agentClient = new HCS10Client({ network: config.network, operatorId: account.accountId, operatorPrivateKey: account.privateKey, operatorPublicKey: publicKey, keyType: keyType.detectedType, logLevel: "info", guardedRegistryBaseUrl: baseUrl }); if (progressCallback) { progressCallback({ stage: "preparing", message: "Initialized agent client", progressPercent: 25, details: { state } }); } let outboundTopicId = state.outboundTopicId; let inboundTopicId = state.inboundTopicId; let pfpTopicId = state.pfpTopicId; let profileTopicId = state.profileTopicId; if (!outboundTopicId || !inboundTopicId || !profileTopicId) { if (pfpTopicId) { builder.setExistingProfilePicture(pfpTopicId); } const createResult = await agentClient.createAgent( builder, 60, state, (data) => { if (progressCallback) { progressCallback({ stage: data.stage, message: data.message, progressPercent: data.progressPercent || 0, details: { ...data.details, state: { ...state, ...data.details?.state } } }); } } ); outboundTopicId = createResult.outboundTopicId; inboundTopicId = createResult.inboundTopicId; pfpTopicId = createResult.pfpTopicId; profileTopicId = createResult.profileTopicId; state.outboundTopicId = outboundTopicId; state.inboundTopicId = inboundTopicId; state.pfpTopicId = pfpTopicId; state.profileTopicId = profileTopicId; if (!state.createdResources) { state.createdResources = []; } if (pfpTopicId && !state.createdResources.includes(`pfp:${pfpTopicId}`)) { state.createdResources.push(`pfp:${pfpTopicId}`); } if (!state.createdResources.includes(`inbound:${inboundTopicId}`)) { state.createdResources.push(`inbound:${inboundTopicId}`); } if (!state.createdResources.includes(`outbound:${outboundTopicId}`)) { state.createdResources.push(`outbound:${outboundTopicId}`); } if (!state.createdResources.includes(`profile:${profileTopicId}`)) { state.createdResources.push(`profile:${profileTopicId}`); } } state.currentStage = "profile"; state.completedPercentage = 60; if (progressCallback) { progressCallback({ stage: "submitting", message: "Created agent with topics and profile", progressPercent: 60, details: { state, outboundTopicId, inboundTopicId, pfpTopicId, profileTopicId } }); } } else { account = account || config.existingAccount; if (!account) { throw new Error( "Cannot resume registration without account information" ); } agentClient = new HCS10Client({ network: config.network, operatorId: account.accountId, operatorPrivateKey: account.privateKey, operatorPublicKey: PrivateKey.fromString( account.privateKey ).publicKey.toString(), logLevel: "info", guardedRegistryBaseUrl: baseUrl }); this.logger.info("Resuming registration with existing state", { inboundTopicId: state.inboundTopicId, outboundTopicId: state.outboundTopicId, profileTopicId: state.profileTopicId, pfpTopicId: state.pfpTopicId }); } const operatorId = `${state.inboundTopicId}@${account.accountId}`; if (state.currentStage !== "complete" || !state.createdResources?.includes( `registration:${state.inboundTopicId}` )) { const registrationResult = await agentClient.registerAgentWithGuardedRegistry( account.accountId, config.network, { progressCallback: (data) => { const adjustedPercent = 60 + (data.progressPercent || 0) * 0.4; if (progressCallback) { progressCallback({ stage: data.stage, message: data.message, progressPercent: adjustedPercent, details: { ...data.details, outboundTopicId: state.outboundTopicId, inboundTopicId: state.inboundTopicId, pfpTopicId: state.pfpTopicId, profileTopicId: state.profileTopicId, operatorId, state: data.details?.state || state } }); } }, existingState: state } ); if (!registrationResult.success) { return { ...registrationResult, state }; } state = registrationResult.state || state; } if (progressCallback) { progressCallback({ stage: "completed", message: "Agent creation and registration complete", progressPercent: 100, details: { outboundTopicId: state.outboundTopicId, inboundTopicId: state.inboundTopicId, pfpTopicId: state.pfpTopicId, profileTopicId: state.profileTopicId, operatorId, state } }); } return { success: true, state, metadata: { accountId: account.accountId, privateKey: account.privateKey, operatorId, inboundTopicId: state.inboundTopicId, outboundTopicId: state.outboundTopicId, profileTopicId: state.profileTopicId, pfpTopicId: state.pfpTopicId } }; } catch (e) { const error = e; const logMessage = `Failed to create and register agent: ${error.message}`; this.logger.error(logMessage); return { error: error.message, success: false, state: options?.existingState || { currentStage: "init", completedPercentage: 0, error: error.message } }; } } /** * Registers an agent with the guarded registry * @param accountId Account ID to register * @param inboundTopicId Inbound topic ID for the agent * @param network Network type ('mainnet' or 'testnet') * @param options Optional configuration including progress callback and confirmation settings * @returns Registration result */ async registerAgentWithGuardedRegistry(accountId, network = this.network, options) { try { this.logger.info("Registering agent with guarded registry"); const maxAttempts = options?.maxAttempts ?? 60; const delayMs = options?.delayMs ?? 2e3; const progressCallback = options?.progressCallback; let state = options?.existingState || { currentStage: "registration", completedPercentage: 0, createdResources: [] }; if (progressCallback) { progressCallback({ stage: "preparing", message: "Preparing agent registration", progressPercent: 10, details: { state } }); } const registrationResult = await this.executeRegistration( accountId, network, this.guardedRegistryBaseUrl, this.logger ); if (!registrationResult.success) { return { ...registrationResult, state }; } if (progressCallback) { progressCallback({ stage: "submitting", message: "Submitting registration to registry", progressPercent: 30, details: { transactionId: registrationResult.transactionId, state } }); } if (registrationResult.transaction) { const transaction = Transaction.fromBytes( Buffer.from(registrationResult.transaction, "base64") ); this.logger.info(`Processing registration transaction`); await transaction.execute(this.client); this.logger.info(`Successfully processed registration transaction`); } if (progressCallback) { progressCallback({ stage: "confirming", message: "Confirming registration transaction", progressPercent: 60, details: { accountId, transactionId: registrationResult.transactionId, state } }); } const confirmed = await this.waitForRegistrationConfirmation( registrationResult.transactionId, network, this.guardedRegistryBaseUrl, maxAttempts, delayMs, this.logger ); state.currentStage = "complete"; state.completedPercentage = 100; if (!state.createdResources) { state.createdResources = []; } if (registrationResult.transactionId) { state.createdResources.push( `registration:${registrationResult.transactionId}` ); } if (progressCallback) { progressCallback({ stage: "completed", message: "Agent registration complete", progressPercent: 100, details: { confirmed, transactionId: registrationResult.transactionId, state } }); } return { ...registrationResult, confirmed, state }; } catch (e) { const error = e; const logMessage = `Failed to register agent: ${error.message}`; this.logger.error(logMessage); return { error: error.message, success: false }; } } /** * Registers an agent with the guarded registry. Should be called by a registry. * @param registryTopicId - The topic ID of the guarded registry. * @param accountId - The account ID of the agent * @param inboundTopicId - The topic ID of the inbound topic * @param memo - The memo of the agent * @param submitKey - The submit key of the agent */ async registerAgent(registryTopicId, accountId, inboundTopicId, memo, submitKey) { this.logger.info("Registering agent"); const payload = { p: "hcs-10", op: "register", account_id: accountId, inbound_topic_id: inboundTopicId, m: memo }; await this.submitPayload(registryTopicId, payload, submitKey); } async getInboundTopicType(topicId) { try { const topicInfo = await this.mirrorNode.getTopicInfo(topicId); if (!topicInfo) { throw new Error("Topic does not exist"); } const hasSubmitKey = topicInfo.submit_key && topicInfo.submit_key.key; if (!hasSubmitKey) { return InboundTopicType.PUBLIC; } const hasFeeScheduleKey = topicInfo.fee_schedule_key && topicInfo.fee_schedule_key.key; if (hasFeeScheduleKey && topicInfo.custom_fees) { const customFees = topicInfo.custom_fees; if (customFees && customFees.fixed_fees && customFees.fixed_fees.length > 0) { this.logger.info( `Topic ${topicId} is fee-based with ${customFees.fixed_fees.length} custom fees` ); return InboundTopicType.FEE_BASED; } } return InboundTopicType.CONTROLLED; } catch (e) { const error = e; const logMessage = `Error determining topic type: ${error.message}`; this.logger.error(logMessage); throw new Error(logMessage); } } getNetwork() { return this.network; } getLogger() { return this.logger; } /** * Public method to get the operator account ID configured for this client instance. * @returns The operator account ID string, or null if not set. */ getOperatorAccountId() { return this.client.operatorAccountId?.toString() ?? null; } /** * Creates a scheduled transaction from a transaction object * @param transaction The transaction to schedule * @param memo Optional memo to include with the scheduled transaction * @param expirationTime Optional expiration time in seconds from now * @returns Object with schedule ID and transaction ID */ async createScheduledTransaction(transaction, memo, expirationTime, schedulePayerAccountId) { this.logger.info("Creating scheduled transaction"); const scheduleTransaction = new ScheduleCreateTransaction().setScheduledTransaction(transaction).setPayerAccountId( schedulePayerAccountId ? AccountId.fromString(schedulePayerAccountId) : this.client.operatorAccountId ); if (memo) { scheduleTransaction.setScheduleMemo(memo); } if (expirationTime) { const expirationDate = addSeconds(/* @__PURE__ */ new Date(), expirationTime); const timestamp = Timestamp.fromDate(expirationDate); scheduleTransaction.setExpirationTime(timestamp); } this.logger.debug("Executing schedule create transaction"); const scheduleResponse = await scheduleTransaction.execute(this.client); const scheduleReceipt = await scheduleResponse.getReceipt(this.client); if (!scheduleReceipt.scheduleId) { this.logger.error( "Failed to create scheduled transaction: scheduleId is null" ); throw new Error( "Failed to create scheduled transaction: scheduleId is null" ); } const scheduleId = scheduleReceipt.scheduleId.toString(); const transactionId = scheduleResponse.transactionId.toString(); this.logger.info( `Scheduled transaction created successfully: ${scheduleId}` ); return { scheduleId, transactionId }; } /** * Sends a transaction operation on a connection topic * @param connectionTopicId Connection topic ID * @param scheduleId Schedule ID of the scheduled transaction * @param data Human-readable description of the transaction, can also be a JSON string or HRL * @param submitKey Optional submit key * @param options Optional parameters including memo (timestamp is no longer used here) * @returns Transaction receipt */ async sendTransactionOperation(connectionTopicId, scheduleId, data, submitKey, options) { const submissionCheck = await this.canSubmitToTopic( connectionTopicId, this.client.operatorAccountId?.toString() || "" ); const operatorId = await this.getOperatorId(); const payload = { p: "hcs-10", op: "transaction", operator_id: operatorId, schedule_id: scheduleId, data, m: options?.memo }; this.logger.info( "Submitting transaction operation to connection topic", payload ); return await this.submitPayload( connectionTopicId, payload, submitKey, submissionCheck.requiresFee ); } /** * Creates and sends a transaction operation in one call * @param connectionTopicId Connection topic ID for sending the transaction operation * @param transaction The transaction to schedule * @param data Human-readable description of the transaction, can also be a JSON string or HRL * @param options Optional parameters for schedule creation and operation memo * @returns Object with schedule details (including scheduleId and its transactionId) and HCS-10 operation receipt */ async sendTransaction(connectionTopicId, transaction, data, options) { this.logger.info( "Creating scheduled transaction and sending transaction operation" ); const { scheduleId, transactionId } = await this.createScheduledTr