UNPKG

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