@hashgraphonline/standards-agent-kit
Version:
A modular SDK for building on-chain autonomous agents using Hashgraph Online Standards, including HCS-10 for agent discovery and communication. https://hol.org
537 lines (536 loc) • 21.4 kB
JavaScript
import { z } from "zod";
import { BaseInscriberQueryTool } from "./standards-agent-kit.es35.js";
import { Logger, ContentResolverRegistry } from "@hashgraphonline/standards-sdk";
import { validateHIP412Metadata } from "./standards-agent-kit.es54.js";
import { contentRefSchema } from "./standards-agent-kit.es53.js";
import { generateDefaultMetadata } from "./standards-agent-kit.es55.js";
import { extendZodSchema, renderConfigs } from "./standards-agent-kit.es44.js";
import { createInscriptionError, createInscriptionQuote, createInscriptionSuccess, createInscriptionPending } from "./standards-agent-kit.es56.js";
import { extractTopicIds, buildInscriptionLinks } from "./standards-agent-kit.es51.js";
const HASHLINK_BLOCK_CONFIG = {
testnet: {
blockId: "0.0.6617393",
hashLink: "hcs://12/0.0.6617393",
template: "0.0.6617393"
},
mainnet: {
blockId: "0.0.TBD",
hashLink: "hcs://12/0.0.TBD",
template: "0.0.TBD"
}
};
function getHashLinkBlockId(network) {
const config = network === "mainnet" ? HASHLINK_BLOCK_CONFIG.mainnet : HASHLINK_BLOCK_CONFIG.testnet;
if (!config || config.blockId === "0.0.TBD") {
return HASHLINK_BLOCK_CONFIG.testnet;
}
return config;
}
const inscribeHashinalSchema = extendZodSchema(
z.object({
url: z.string().optional().describe(
"The URL of the content to inscribe as Hashinal NFT (use this OR contentRef)"
),
contentRef: contentRefSchema.optional().describe(
'Content reference ID in format "content-ref:[id]" for already stored content (use this OR url)'
),
base64Data: z.string().optional().describe(
"Base64 encoded content data (use this if neither url nor contentRef provided)"
),
fileName: z.string().optional().describe(
"File name for the content (required when using base64Data or contentRef)"
),
mimeType: z.string().optional().describe('MIME type of the content (e.g., "image/png", "image/jpeg")'),
name: z.string().optional().describe(
'Display name for the NFT (e.g., "Sunset Landscape #42", "Digital Abstract Art")'
),
creator: z.string().optional().describe(
'Creator account ID, artist name, or brand (e.g., "0.0.123456", "ArtistName", "StudioBrand")'
),
description: z.string().optional().describe(
"Meaningful description of the artwork, story, or concept behind this NFT"
),
type: z.string().optional().describe(
'Category or genre of the NFT (e.g., "Digital Art", "Photography", "Collectible Card")'
),
attributes: extendZodSchema(
z.array(
z.object({
trait_type: z.string(),
value: z.union([z.string(), z.number()])
})
)
).withRender(renderConfigs.array("NFT Attributes", "Attribute")).optional().describe(
'Collectible traits and characteristics (e.g., "Rarity": "Epic", "Color": "Blue", "Style": "Abstract")'
),
properties: z.record(z.unknown()).optional().describe("Additional properties"),
jsonFileURL: z.string().url().optional().describe("URL to JSON metadata file"),
fileStandard: z.enum(["1", "6"]).optional().default("1").describe(
"HCS file standard: 1 for static Hashinals (HCS-5), 6 for dynamic Hashinals (HCS-6)"
),
tags: z.array(z.string()).optional().describe("Tags to categorize the NFT"),
chunkSize: z.number().int().positive().optional().describe("Chunk size for large files"),
waitForConfirmation: z.boolean().optional().describe("Whether to wait for inscription confirmation").default(true),
timeoutMs: z.number().int().positive().optional().describe(
"Timeout in milliseconds for inscription (default: no timeout - waits until completion)"
),
quoteOnly: z.boolean().optional().default(false).describe(
"If true, returns a cost quote instead of executing the inscription"
),
withHashLinkBlocks: z.boolean().optional().default(true).describe(
"If true, creates interactive HashLink blocks for the inscribed content and returns block data alongside the inscription response"
),
renderForm: z.boolean().optional().default(true).describe(
"Whether to show a form to collect metadata. Set to false only if user provided complete metadata including name, description, creator, and attributes."
)
})
).withRender({
fieldType: "object",
ui: {
label: "Inscribe Hashinal NFT",
description: "Create a Hashinal inscription for NFT minting"
}
});
class InscribeHashinalTool extends BaseInscriberQueryTool {
constructor() {
super(...arguments);
this.name = "inscribeHashinal";
this.description = "Tool for inscribing Hashinal NFTs. CRITICAL: When user provides content (url/contentRef/base64Data), call with ONLY the content parameters - DO NOT auto-generate name, description, creator, or attributes. A form will be automatically shown to collect metadata from the user. Only include metadata parameters if the user explicitly provided them in their message.";
}
getEntityResolutionPreferences() {
return {
name: "literal",
description: "literal",
creator: "literal",
attributes: "literal",
properties: "literal"
};
}
get specificInputSchema() {
const baseSchema = inscribeHashinalSchema._def?.schema || inscribeHashinalSchema;
return baseSchema;
}
get schema() {
if (!this._schemaWithRenderConfig) {
const baseSchema = this.specificInputSchema;
const schemaWithRender = baseSchema;
if (!schemaWithRender._renderConfig) {
schemaWithRender._renderConfig = {
fieldType: "object",
ui: {
label: "Inscribe Hashinal NFT",
description: "Create a Hashinal inscription for NFT minting"
}
};
}
this._schemaWithRenderConfig = baseSchema;
}
return this._schemaWithRenderConfig;
}
/**
* Implementation of FormValidatable interface
* Determines if a form should be generated for the given input
*/
shouldGenerateForm(input) {
const logger = new Logger({ module: "InscribeHashinalTool" });
const inputObj = input;
logger.info("InscribeHashinalTool: Checking if form should be generated", {
inputKeys: Object.keys(inputObj || {}),
hasContent: !!(inputObj.url || inputObj.contentRef || inputObj.base64Data),
renderFormProvided: "renderForm" in inputObj,
renderFormValue: inputObj.renderForm
});
const hasContentSource = !!(inputObj.url || inputObj.contentRef || inputObj.base64Data);
if (!hasContentSource) {
logger.info("InscribeHashinalTool: No content source provided");
return false;
}
if ("renderForm" in inputObj && inputObj.renderForm === false) {
logger.info(
"InscribeHashinalTool: renderForm=false, skipping form generation"
);
return false;
}
const isNonEmptyString = (v) => {
if (typeof v !== "string") {
return false;
}
if (v.trim().length === 0) {
return false;
}
return true;
};
const hasRequiredMetadata = isNonEmptyString(inputObj.name) && isNonEmptyString(inputObj.description) && isNonEmptyString(inputObj.creator);
if (hasRequiredMetadata) {
logger.info(
"InscribeHashinalTool: Required metadata present, skipping form generation"
);
return false;
}
logger.info(
"InscribeHashinalTool: Content provided, showing form for metadata collection"
);
return true;
}
/**
* Implementation of FormValidatable interface
* Returns the focused schema for form generation
*/
getFormSchema() {
const focusedSchema = extendZodSchema(
z.object({
name: z.string().min(1, "Name is required").describe(
'Display name for the NFT (e.g., "Sunset Landscape #42", "Digital Abstract Art")'
),
description: z.string().min(1, "Description is required").describe(
"Meaningful description of the artwork, story, or concept behind this NFT"
),
creator: z.string().min(1, "Creator is required").describe(
'Creator account ID, artist name, or brand (e.g., "0.0.123456", "ArtistName", "StudioBrand")'
),
attributes: extendZodSchema(
z.array(
z.object({
trait_type: z.string().describe('Trait name (e.g., "Rarity", "Color", "Style")'),
value: z.union([z.string(), z.number()]).describe('Trait value (e.g., "Epic", "Blue", 85)')
})
)
).withRender(renderConfigs.array("NFT Attributes", "Attribute")).optional().describe("Collectible traits and characteristics."),
type: z.string().optional().describe(
'Category or genre of the NFT (e.g., "Digital Art", "Photography", "Collectible Card)'
)
})
).withRender({
fieldType: "object",
ui: {
label: "Complete NFT Metadata",
description: "Provide meaningful metadata to create a valuable NFT"
}
});
return focusedSchema;
}
async executeQuery(params, _runManager) {
if (!params.url && !params.contentRef && !params.base64Data) {
return createInscriptionError({
code: "MISSING_CONTENT",
details: "No content source provided",
suggestions: [
"Provide a URL to content you want to inscribe",
"Upload a file and use the content reference",
"Provide base64-encoded content data"
]
});
}
const operatorAccount = this.inscriberBuilder["hederaKit"]?.client?.operatorAccountId?.toString() || "0.0.unknown";
const rawMetadata = {
...generateDefaultMetadata({
name: params.name,
creator: params.creator,
description: params.description,
type: params.type,
fileName: params.fileName,
mimeType: params.mimeType,
operatorAccount
}),
attributes: Array.isArray(params.attributes) ? params.attributes : [],
properties: params.properties || {}
};
let validatedMetadata;
try {
validatedMetadata = validateHIP412Metadata(rawMetadata);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return createInscriptionError({
code: "METADATA_VALIDATION_FAILED",
details: `Metadata validation error: ${errorMessage}`,
suggestions: [
"Ensure all required metadata fields are provided",
"Check that attribute values are valid",
"Verify metadata follows HIP-412 standard"
]
});
}
const options = {
mode: "hashinal",
metadata: validatedMetadata,
jsonFileURL: params.jsonFileURL,
fileStandard: params.fileStandard,
tags: params.tags,
chunkSize: params.chunkSize,
waitForConfirmation: params.quoteOnly ? false : params.waitForConfirmation ?? true,
waitMaxAttempts: 60,
waitIntervalMs: 5e3,
network: this.inscriberBuilder["hederaKit"].client.network.toString().includes("mainnet") ? "mainnet" : "testnet",
quoteOnly: params.quoteOnly
};
let inscriptionData;
if (params.url) {
inscriptionData = { type: "url", url: params.url };
} else if (params.contentRef || params.base64Data) {
const inputData = params.contentRef || params.base64Data || "";
const { buffer, mimeType, fileName } = await this.resolveContent(
inputData,
params.mimeType,
params.fileName
);
inscriptionData = {
type: "buffer",
buffer,
fileName: fileName || params.fileName || "hashinal-content",
mimeType: mimeType || params.mimeType
};
} else {
throw new Error("No valid input data provided for inscription");
}
if (params.quoteOnly) {
try {
const quote = await this.generateInscriptionQuote(
inscriptionData,
options
);
return createInscriptionQuote({
totalCostHbar: quote.totalCostHbar,
validUntil: quote.validUntil,
breakdown: quote.breakdown,
content: {
name: params.name,
creator: params.creator,
type: params.type
}
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to generate inscription quote";
return createInscriptionError({
code: "QUOTE_GENERATION_FAILED",
details: `Quote generation failed: ${errorMessage}`,
suggestions: [
"Check network connectivity",
"Verify content is accessible",
"Try again in a moment"
]
});
}
}
try {
let result;
if (params.timeoutMs) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(
() => reject(
new Error(`Inscription timed out after ${params.timeoutMs}ms`)
),
params.timeoutMs
);
});
result = await Promise.race([
this.inscriberBuilder.inscribeAuto ? this.inscriberBuilder.inscribeAuto(inscriptionData, options) : this.inscriberBuilder.inscribe(inscriptionData, options),
timeoutPromise
]);
} else {
result = this.inscriberBuilder.inscribeAuto ? await this.inscriberBuilder.inscribeAuto(inscriptionData, options) : await this.inscriberBuilder.inscribe(inscriptionData, options);
}
if (result.confirmed && !result.quote) {
const ids = extractTopicIds(result.inscription, result.result);
const network = options.network || "testnet";
const fileStandard = params.fileStandard || "1";
const { hrl, topicId, cdnUrl } = buildInscriptionLinks(
ids,
network,
fileStandard
);
const txId = result?.inscription?.tx_id || result?.result?.transactionId || "unknown";
const successResponse = createInscriptionSuccess({
hrl: hrl || "hcs://1/unknown",
topicId: topicId || "unknown",
standard: fileStandard === "6" ? "Dynamic" : "Static",
cdnUrl,
transactionId: txId,
metadata: {
name: params.name,
creator: params.creator,
description: params.description,
type: params.type,
attributes: Array.isArray(params.attributes) ? params.attributes : []
}
});
if (params.withHashLinkBlocks !== false) {
try {
const block = await this.createHashLinkBlock(successResponse);
successResponse.hashLinkBlock = block;
} catch (e) {
const logger = new Logger({ module: "InscribeHashinalTool" });
logger.warn("Failed to create HashLink block", e);
}
}
return successResponse;
} else if (!result.quote && !result.confirmed) {
const txId = result.result?.transactionId || result?.inscription?.transactionId || "unknown";
return createInscriptionPending({
transactionId: txId,
details: "Successfully submitted Hashinal inscription. Waiting for network confirmation..."
});
} else {
return createInscriptionError({
code: "UNEXPECTED_RESULT",
details: "Received an unexpected inscription result state",
suggestions: ["Try again or verify network status"]
});
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to inscribe Hashinal NFT";
throw new Error(`Inscription failed: ${errorMessage}`);
}
}
/**
* Creates HashLink block configuration for Hashinal inscriptions.
* Automatically detects network and selects appropriate block ID configuration.
* Uses testnet block as fallback for unknown networks or undeployed mainnet blocks.
*
* @param response The inscription response containing metadata and network information
* @param _mimeType Optional MIME type (currently unused, preserved for compatibility)
* @returns HCS12BlockResult with network-specific block configuration
*
* @example
* ```typescript
* // Testnet usage (automatic detection from client)
* const testnetClient = Client.forTestnet();
* const tool = new InscribeHashinalTool(testnetClient);
* const block = await tool.createHashLinkBlock(inscriptionResponse);
* const id = block.blockId; // '0.0.6617393'
* const link = block.hashLink; // 'hcs://12/0.0.6617393'
*
* // Mainnet usage (automatic detection from client)
* const mainnetClient = Client.forMainnet();
* const tool = new InscribeHashinalTool(mainnetClient);
* const block = await tool.createHashLinkBlock(inscriptionResponse);
* const mainnetId = block.blockId; // Network-specific mainnet block ID
*
* // HashLink Block Response Structure:
* {
* blockId: string; // Hedera account ID format (e.g., '0.0.6617393')
* hashLink: string; // HCS-12 URL format: 'hcs://12/{blockId}'
* template: string; // Block template reference matching blockId
* attributes: { // Metadata for client-side processing
* name: string; // Content display name
* creator: string; // Creator account ID
* topicId: string; // HCS topic containing the inscription
* hrl: string; // Hedera Resource Locator
* network: string; // Network type: 'testnet' | 'mainnet'
* }
* }
*
* // Render function usage in HashLink blocks:
* // The block's JavaScript render function receives this structure
* // and can access network-specific resources through attributes.network
* ```
*/
async createHashLinkBlock(response, _mimeType) {
const clientNetwork = this.inscriberBuilder["hederaKit"].client.network.toString().includes("mainnet") ? "mainnet" : "testnet";
const cdnNetwork = response.inscription.cdnUrl?.includes("mainnet") ? "mainnet" : "testnet";
if (clientNetwork !== cdnNetwork) {
const logger = new Logger({ module: "InscribeHashinalTool" });
logger.warn(
`Network mismatch detected: client=${clientNetwork}, cdn=${cdnNetwork}. Using client network.`
);
}
const network = clientNetwork;
const config = getHashLinkBlockId(network);
return {
blockId: config.blockId,
hashLink: config.hashLink,
template: config.template,
attributes: {
name: response.metadata.name || "Untitled Content",
creator: response.metadata.creator || "",
topicId: response.inscription.topicId,
hrl: response.inscription.hrl,
network
}
};
}
async resolveContent(input, providedMimeType, providedFileName) {
const trimmedInput = input.trim();
const resolver = this.getContentResolver() || ContentResolverRegistry.getResolver();
if (!resolver) {
return this.handleDirectContent(
trimmedInput,
providedMimeType,
providedFileName
);
}
const referenceId = resolver.extractReferenceId(trimmedInput);
if (referenceId) {
try {
const resolution = await resolver.resolveReference(referenceId);
return {
buffer: resolution.content,
mimeType: resolution.metadata?.mimeType || providedMimeType,
fileName: resolution.metadata?.fileName || providedFileName,
wasReference: true
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error resolving reference";
throw new Error(`Reference resolution failed: ${errorMsg}`);
}
}
return this.handleDirectContent(
trimmedInput,
providedMimeType,
providedFileName
);
}
handleDirectContent(input, providedMimeType, providedFileName) {
const isValidBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(input);
if (isValidBase64) {
try {
const buffer2 = Buffer.from(input, "base64");
return {
buffer: buffer2,
mimeType: providedMimeType,
fileName: providedFileName,
wasReference: false
};
} catch (error) {
throw new Error(
"Failed to decode base64 data. Please ensure the data is properly encoded."
);
}
}
const buffer = Buffer.from(input, "utf8");
return {
buffer,
mimeType: providedMimeType || "text/plain",
fileName: providedFileName,
wasReference: false
};
}
/**
* Implementation of FormValidatable interface
* Returns essential fields that should always be shown in forms
*/
getEssentialFields() {
return ["name", "description", "creator", "attributes"];
}
/**
* Implementation of FormValidatable interface
* Determines if a field value should be considered empty for this tool
*/
isFieldEmpty(fieldName, value) {
if (value === void 0 || value === null || value === "") {
return true;
}
if (Array.isArray(value) && value.length === 0) {
return true;
}
if (fieldName === "attributes" && Array.isArray(value)) {
return value.every(
(attr) => !attr || typeof attr === "object" && (!attr.trait_type || !attr.value)
);
}
return false;
}
}
export {
InscribeHashinalTool
};
//# sourceMappingURL=standards-agent-kit.es39.js.map