@hashgraphonline/standards-agent-kit 0.2.125 → 0.2.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cjs/standards-agent-kit.cjs +1 -1
  2. package/dist/cjs/standards-agent-kit.cjs.map +1 -1
  3. package/dist/cjs/tools/inscriber/InscribeFromBufferTool.d.ts +2 -10
  4. package/dist/cjs/tools/inscriber/InscribeHashinalTool.d.ts +62 -30
  5. package/dist/cjs/utils/content-resolver.d.ts +10 -0
  6. package/dist/cjs/utils/metadata-defaults.d.ts +18 -0
  7. package/dist/cjs/validation/content-ref-schemas.d.ts +9 -0
  8. package/dist/cjs/validation/hip412-schemas.d.ts +125 -0
  9. package/dist/es/standards-agent-kit.es34.js +64 -22
  10. package/dist/es/standards-agent-kit.es34.js.map +1 -1
  11. package/dist/es/standards-agent-kit.es36.js +12 -65
  12. package/dist/es/standards-agent-kit.es36.js.map +1 -1
  13. package/dist/es/standards-agent-kit.es37.js +159 -33
  14. package/dist/es/standards-agent-kit.es37.js.map +1 -1
  15. package/dist/es/standards-agent-kit.es44.js +57 -0
  16. package/dist/es/standards-agent-kit.es44.js.map +1 -0
  17. package/dist/es/standards-agent-kit.es45.js +6 -0
  18. package/dist/es/standards-agent-kit.es45.js.map +1 -0
  19. package/dist/es/standards-agent-kit.es46.js +43 -0
  20. package/dist/es/standards-agent-kit.es46.js.map +1 -0
  21. package/dist/es/standards-agent-kit.es47.js +15 -0
  22. package/dist/es/standards-agent-kit.es47.js.map +1 -0
  23. package/dist/es/tools/inscriber/InscribeFromBufferTool.d.ts +2 -10
  24. package/dist/es/tools/inscriber/InscribeHashinalTool.d.ts +62 -30
  25. package/dist/es/utils/content-resolver.d.ts +10 -0
  26. package/dist/es/utils/metadata-defaults.d.ts +18 -0
  27. package/dist/es/validation/content-ref-schemas.d.ts +9 -0
  28. package/dist/es/validation/hip412-schemas.d.ts +125 -0
  29. package/dist/umd/standards-agent-kit.umd.js +1 -1
  30. package/dist/umd/standards-agent-kit.umd.js.map +1 -1
  31. package/dist/umd/tools/inscriber/InscribeFromBufferTool.d.ts +2 -10
  32. package/dist/umd/tools/inscriber/InscribeHashinalTool.d.ts +62 -30
  33. package/dist/umd/utils/content-resolver.d.ts +10 -0
  34. package/dist/umd/utils/metadata-defaults.d.ts +18 -0
  35. package/dist/umd/validation/content-ref-schemas.d.ts +9 -0
  36. package/dist/umd/validation/hip412-schemas.d.ts +125 -0
  37. package/package.json +5 -5
  38. package/src/tools/inscriber/InscribeFromBufferTool.ts +31 -137
  39. package/src/tools/inscriber/InscribeFromUrlTool.ts +147 -65
  40. package/src/tools/inscriber/InscribeHashinalTool.ts +271 -46
  41. package/src/utils/content-resolver.ts +87 -0
  42. package/src/utils/metadata-defaults.ts +25 -0
  43. package/src/validation/content-ref-schemas.ts +20 -0
  44. package/src/validation/hip412-schemas.ts +52 -0
@@ -1,17 +1,66 @@
1
1
  import { z } from 'zod';
2
2
  import { BaseInscriberQueryTool } from './base-inscriber-tools';
3
- import { InscriptionOptions } from '@hashgraphonline/standards-sdk';
3
+ import {
4
+ InscriptionOptions,
5
+ ContentResolverRegistry,
6
+ } from '@hashgraphonline/standards-sdk';
4
7
  import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
8
+ import { validateHIP412Metadata } from '../../validation/hip412-schemas';
9
+ import { contentRefSchema } from '../../validation/content-ref-schemas';
10
+ import { generateDefaultMetadata } from '../../utils/metadata-defaults';
11
+
5
12
 
6
13
  /**
7
14
  * Schema for inscribing Hashinal NFT
8
15
  */
9
16
  const inscribeHashinalSchema = z.object({
10
- url: z.string().url().describe('The URL of the content to inscribe as Hashinal NFT'),
11
- name: z.string().describe('Name of the Hashinal NFT'),
12
- creator: z.string().describe('Creator account ID or name'),
13
- description: z.string().describe('Description of the Hashinal NFT'),
14
- type: z.string().describe('Type of NFT (e.g., "image", "video", "audio")'),
17
+ url: z
18
+ .string()
19
+ .optional()
20
+ .describe(
21
+ 'The URL of the content to inscribe as Hashinal NFT (use this OR contentRef)'
22
+ ),
23
+ contentRef: contentRefSchema
24
+ .optional()
25
+ .describe(
26
+ 'Content reference ID in format "content-ref:[id]" for already stored content (use this OR url)'
27
+ ),
28
+ base64Data: z
29
+ .string()
30
+ .optional()
31
+ .describe(
32
+ 'Base64 encoded content data (use this if neither url nor contentRef provided)'
33
+ ),
34
+ fileName: z
35
+ .string()
36
+ .optional()
37
+ .describe(
38
+ 'File name for the content (required when using base64Data or contentRef)'
39
+ ),
40
+ mimeType: z
41
+ .string()
42
+ .optional()
43
+ .describe('MIME type of the content (e.g., "image/png", "image/jpeg")'),
44
+ name: z
45
+ .string()
46
+ .optional()
47
+ .describe(
48
+ 'Name of the Hashinal NFT (defaults to filename if not provided)'
49
+ ),
50
+ creator: z
51
+ .string()
52
+ .optional()
53
+ .describe('Creator account ID or name (defaults to operator account)'),
54
+ description: z
55
+ .string()
56
+ .optional()
57
+ .describe(
58
+ 'Description of the Hashinal NFT (auto-generated if not provided)'
59
+ ),
60
+ type: z
61
+ .string()
62
+ .optional()
63
+ .describe('Type of NFT (auto-detected from MIME type if not provided)'),
15
64
  attributes: z
16
65
  .array(
17
66
  z.object({
@@ -30,10 +79,14 @@ const inscribeHashinalSchema = z.object({
30
79
  .url()
31
80
  .optional()
32
81
  .describe('URL to JSON metadata file'),
33
- tags: z
34
- .array(z.string())
82
+ fileStandard: z
83
+ .enum(['1', '6'])
35
84
  .optional()
36
- .describe('Tags to categorize the NFT'),
85
+ .default('1')
86
+ .describe(
87
+ 'HCS file standard: 1 for static Hashinals (HCS-5), 6 for dynamic Hashinals (HCS-6)'
88
+ ),
89
+ tags: z.array(z.string()).optional().describe('Tags to categorize the NFT'),
37
90
  chunkSize: z
38
91
  .number()
39
92
  .int()
@@ -49,25 +102,28 @@ const inscribeHashinalSchema = z.object({
49
102
  .int()
50
103
  .positive()
51
104
  .optional()
52
- .describe('Timeout in milliseconds for inscription (default: no timeout - waits until completion)'),
53
- apiKey: z
54
- .string()
55
- .optional()
56
- .describe('API key for inscription service'),
105
+ .describe(
106
+ 'Timeout in milliseconds for inscription (default: no timeout - waits until completion)'
107
+ ),
108
+ apiKey: z.string().optional().describe('API key for inscription service'),
57
109
  quoteOnly: z
58
110
  .boolean()
59
111
  .optional()
60
112
  .default(false)
61
- .describe('If true, returns a cost quote instead of executing the inscription'),
113
+ .describe(
114
+ 'If true, returns a cost quote instead of executing the inscription'
115
+ ),
62
116
  });
63
117
 
64
-
65
118
  /**
66
119
  * Tool for inscribing Hashinal NFTs
67
120
  */
68
- export class InscribeHashinalTool extends BaseInscriberQueryTool<typeof inscribeHashinalSchema> {
121
+ export class InscribeHashinalTool extends BaseInscriberQueryTool<
122
+ typeof inscribeHashinalSchema
123
+ > {
69
124
  name = 'inscribeHashinal';
70
- description = 'Inscribe content as a Hashinal NFT on the Hedera network. Set quoteOnly=true to get cost estimates without executing the inscription.';
125
+ description =
126
+ 'STEP 1: Inscribe content as Hashinal NFT. This tool creates the inscription and returns metadataForMinting (HRL format). CRITICAL: You MUST use the metadataForMinting field from this tool output as the metadata parameter when calling mint NFT tools. DO NOT use the original content reference for minting. This tool only inscribes - call minting tools separately after this completes. Use fileStandard=6 for dynamic Hashinals (HCS-6) or fileStandard=1 for static Hashinals (HCS-5).';
71
127
 
72
128
  get specificInputSchema() {
73
129
  return inscribeHashinalSchema;
@@ -77,36 +133,90 @@ export class InscribeHashinalTool extends BaseInscriberQueryTool<typeof inscribe
77
133
  params: z.infer<typeof inscribeHashinalSchema>,
78
134
  _runManager?: CallbackManagerForToolRun
79
135
  ): Promise<unknown> {
80
- const metadata = {
81
- name: params.name,
82
- creator: params.creator,
83
- description: params.description,
84
- type: params.type,
136
+ // Validate input - must have either url, contentRef, or base64Data
137
+ if (!params.url && !params.contentRef && !params.base64Data) {
138
+ throw new Error(
139
+ 'Must provide either url, contentRef, or base64Data for the Hashinal NFT content'
140
+ );
141
+ }
142
+
143
+ const operatorAccount =
144
+ this.inscriberBuilder['hederaKit']?.client?.operatorAccountId?.toString() || '0.0.unknown';
145
+
146
+ const rawMetadata = {
147
+ ...generateDefaultMetadata({
148
+ name: params.name,
149
+ creator: params.creator,
150
+ description: params.description,
151
+ type: params.type,
152
+ fileName: params.fileName,
153
+ mimeType: params.mimeType,
154
+ operatorAccount,
155
+ }),
85
156
  attributes: params.attributes,
86
157
  properties: params.properties,
87
158
  };
88
159
 
160
+ // Validate metadata against HIP-412 standard
161
+ let validatedMetadata;
162
+ try {
163
+ validatedMetadata = validateHIP412Metadata(rawMetadata);
164
+ } catch (error) {
165
+ const errorMessage =
166
+ error instanceof Error ? error.message : String(error);
167
+ throw new Error(`Metadata validation error: ${errorMessage}`);
168
+ }
169
+
89
170
  const options: InscriptionOptions = {
90
171
  mode: 'hashinal',
91
- metadata,
172
+ metadata: validatedMetadata,
92
173
  jsonFileURL: params.jsonFileURL,
174
+ fileStandard: params.fileStandard,
93
175
  tags: params.tags,
94
176
  chunkSize: params.chunkSize,
95
- waitForConfirmation: params.quoteOnly ? false : (params.waitForConfirmation ?? true),
177
+ waitForConfirmation: params.quoteOnly
178
+ ? false
179
+ : params.waitForConfirmation ?? true,
96
180
  waitMaxAttempts: 10,
97
181
  waitIntervalMs: 3000,
98
182
  apiKey: params.apiKey,
99
- network: this.inscriberBuilder['hederaKit'].client.network.toString().includes('mainnet') ? 'mainnet' : 'testnet',
183
+ network: this.inscriberBuilder['hederaKit'].client.network
184
+ .toString()
185
+ .includes('mainnet')
186
+ ? 'mainnet'
187
+ : 'testnet',
100
188
  quoteOnly: params.quoteOnly,
101
189
  };
102
190
 
191
+ // Determine inscription data based on input type
192
+ let inscriptionData: any;
193
+
194
+ if (params.url) {
195
+ inscriptionData = { type: 'url', url: params.url };
196
+ } else if (params.contentRef || params.base64Data) {
197
+ // Handle content reference or base64 data
198
+ const inputData = params.contentRef || params.base64Data || '';
199
+ const { buffer, mimeType, fileName } = await this.resolveContent(
200
+ inputData,
201
+ params.mimeType,
202
+ params.fileName
203
+ );
204
+
205
+ inscriptionData = {
206
+ type: 'buffer' as const,
207
+ buffer,
208
+ fileName: fileName || params.fileName || 'hashinal-content',
209
+ mimeType: mimeType || params.mimeType,
210
+ };
211
+ }
212
+
103
213
  if (params.quoteOnly) {
104
214
  try {
105
215
  const quote = await this.generateInscriptionQuote(
106
- { type: 'url', url: params.url },
216
+ inscriptionData,
107
217
  options
108
218
  );
109
-
219
+
110
220
  return {
111
221
  success: true,
112
222
  quote: {
@@ -124,49 +234,164 @@ export class InscribeHashinalTool extends BaseInscriberQueryTool<typeof inscribe
124
234
  };
125
235
  } catch (error) {
126
236
  const errorMessage =
127
- error instanceof Error ? error.message : 'Failed to generate inscription quote';
237
+ error instanceof Error
238
+ ? error.message
239
+ : 'Failed to generate inscription quote';
128
240
  throw new Error(`Quote generation failed: ${errorMessage}`);
129
241
  }
130
242
  }
131
243
 
132
244
  try {
133
245
  let result: Awaited<ReturnType<typeof this.inscriberBuilder.inscribe>>;
134
-
246
+
135
247
  if (params.timeoutMs) {
136
248
  const timeoutPromise = new Promise<never>((_, reject) => {
137
249
  setTimeout(
138
- () => reject(new Error(`Inscription timed out after ${params.timeoutMs}ms`)),
250
+ () =>
251
+ reject(
252
+ new Error(`Inscription timed out after ${params.timeoutMs}ms`)
253
+ ),
139
254
  params.timeoutMs
140
255
  );
141
256
  });
142
257
 
143
258
  result = await Promise.race([
144
- this.inscriberBuilder.inscribe(
145
- { type: 'url', url: params.url },
146
- options
147
- ),
148
- timeoutPromise
259
+ this.inscriberBuilder.inscribe(inscriptionData, options),
260
+ timeoutPromise,
149
261
  ]);
150
262
  } else {
151
- result = await this.inscriberBuilder.inscribe(
152
- { type: 'url', url: params.url },
153
- options
154
- );
263
+ result = await this.inscriberBuilder.inscribe(inscriptionData, options);
155
264
  }
156
265
 
157
266
  if (result.confirmed && !result.quote) {
158
- const topicId = result.inscription?.topic_id || (result.result as any).topicId;
267
+ const imageTopicId = result.inscription?.topic_id;
268
+ const jsonTopicId = result.inscription?.jsonTopicId;
159
269
  const network = options.network || 'testnet';
160
- const cdnUrl = topicId ? `https://kiloscribe.com/api/inscription-cdn/${topicId}?network=${network}` : null;
161
- return `Successfully inscribed and confirmed Hashinal NFT on the Hedera network!\n\nTransaction ID: ${(result.result as any).transactionId}\nTopic ID: ${topicId || 'N/A'}${cdnUrl ? `\nView inscription: ${cdnUrl}` : ''}\n\nThe Hashinal NFT is now available.`;
270
+
271
+ const cdnUrl = jsonTopicId
272
+ ? `https://kiloscribe.com/api/inscription-cdn/${jsonTopicId}?network=${network}`
273
+ : null;
274
+
275
+ const fileStandard = params.fileStandard || '1';
276
+ const hrl = jsonTopicId ? `hcs://${fileStandard}/${jsonTopicId}` : null;
277
+ const standardType = fileStandard === '6' ? 'Dynamic' : 'Static';
278
+
279
+ return {
280
+ success: true,
281
+ transactionId: (result.result as any).transactionId,
282
+ imageTopicId: imageTopicId || 'N/A',
283
+ jsonTopicId: jsonTopicId || 'N/A',
284
+ metadataForMinting: hrl,
285
+ hcsStandard: `hcs://${fileStandard}`,
286
+ fileStandard: fileStandard,
287
+ standardType: standardType,
288
+ message: `STEP 1 COMPLETE: ${standardType} Hashinal inscription finished! NOW FOR STEP 2: Use EXACTLY this metadata for minting: "${hrl}". DO NOT use the original content reference.`,
289
+ cdnUrl: cdnUrl,
290
+ NEXT_STEP_INSTRUCTIONS: `Call mint NFT tool with metadata: ["${hrl}"]`,
291
+ WARNING:
292
+ 'DO NOT use content-ref for minting - only use the metadataForMinting value above',
293
+ };
162
294
  } else if (!result.quote && !result.confirmed) {
163
- return `Successfully submitted Hashinal NFT inscription to the Hedera network!\n\nTransaction ID: ${(result.result as any).transactionId}\n\nThe inscription is processing and will be confirmed shortly.`;
295
+ return `Successfully submitted Hashinal NFT inscription to the Hedera network!\n\nTransaction ID: ${
296
+ (result.result as any).transactionId
297
+ }\n\nThe inscription is processing and will be confirmed shortly.`;
164
298
  } else {
165
299
  return 'Inscription operation completed.';
166
300
  }
167
301
  } catch (error) {
168
- const errorMessage = error instanceof Error ? error.message : 'Failed to inscribe Hashinal NFT';
302
+ const errorMessage =
303
+ error instanceof Error
304
+ ? error.message
305
+ : 'Failed to inscribe Hashinal NFT';
169
306
  throw new Error(`Inscription failed: ${errorMessage}`);
170
307
  }
171
308
  }
172
- }
309
+
310
+ private async resolveContent(
311
+ input: string,
312
+ providedMimeType?: string,
313
+ providedFileName?: string
314
+ ): Promise<{
315
+ buffer: Buffer;
316
+ mimeType?: string;
317
+ fileName?: string;
318
+ wasReference?: boolean;
319
+ }> {
320
+ const trimmedInput = input.trim();
321
+
322
+ const resolver =
323
+ this.getContentResolver() || ContentResolverRegistry.getResolver();
324
+
325
+ if (!resolver) {
326
+ return this.handleDirectContent(
327
+ trimmedInput,
328
+ providedMimeType,
329
+ providedFileName
330
+ );
331
+ }
332
+
333
+ const referenceId = resolver.extractReferenceId(trimmedInput);
334
+
335
+ if (referenceId) {
336
+ try {
337
+ const resolution = await resolver.resolveReference(referenceId);
338
+
339
+ return {
340
+ buffer: resolution.content,
341
+ mimeType: resolution.metadata?.mimeType || providedMimeType,
342
+ fileName: resolution.metadata?.fileName || providedFileName,
343
+ wasReference: true,
344
+ };
345
+ } catch (error) {
346
+ const errorMsg =
347
+ error instanceof Error
348
+ ? error.message
349
+ : 'Unknown error resolving reference';
350
+ throw new Error(`Reference resolution failed: ${errorMsg}`);
351
+ }
352
+ }
353
+
354
+ return this.handleDirectContent(
355
+ trimmedInput,
356
+ providedMimeType,
357
+ providedFileName
358
+ );
359
+ }
360
+
361
+ private handleDirectContent(
362
+ input: string,
363
+ providedMimeType?: string,
364
+ providedFileName?: string
365
+ ): {
366
+ buffer: Buffer;
367
+ mimeType?: string;
368
+ fileName?: string;
369
+ wasReference?: boolean;
370
+ } {
371
+ const isValidBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(input);
372
+
373
+ if (isValidBase64) {
374
+ try {
375
+ const buffer = Buffer.from(input, 'base64');
376
+ return {
377
+ buffer,
378
+ mimeType: providedMimeType,
379
+ fileName: providedFileName,
380
+ wasReference: false,
381
+ };
382
+ } catch (error) {
383
+ throw new Error(
384
+ 'Failed to decode base64 data. Please ensure the data is properly encoded.'
385
+ );
386
+ }
387
+ }
388
+
389
+ const buffer = Buffer.from(input, 'utf8');
390
+ return {
391
+ buffer,
392
+ mimeType: providedMimeType || 'text/plain',
393
+ fileName: providedFileName,
394
+ wasReference: false,
395
+ };
396
+ }
397
+ }
@@ -0,0 +1,87 @@
1
+ import { ContentResolverRegistry } from '@hashgraphonline/standards-sdk';
2
+
3
+ export interface ContentResolutionResult {
4
+ buffer: Buffer;
5
+ mimeType?: string;
6
+ fileName?: string;
7
+ wasReference?: boolean;
8
+ }
9
+
10
+ /**
11
+ * Resolves content from various input formats (content-ref, base64, plain text)
12
+ */
13
+ export async function resolveContent(
14
+ input: string,
15
+ providedMimeType?: string,
16
+ providedFileName?: string
17
+ ): Promise<ContentResolutionResult> {
18
+ const trimmedInput = input.trim();
19
+
20
+ const resolver = ContentResolverRegistry.getResolver();
21
+
22
+ if (!resolver) {
23
+ return handleDirectContent(
24
+ trimmedInput,
25
+ providedMimeType,
26
+ providedFileName
27
+ );
28
+ }
29
+
30
+ const referenceId = resolver.extractReferenceId(trimmedInput);
31
+
32
+ if (referenceId) {
33
+ try {
34
+ const resolution = await resolver.resolveReference(referenceId);
35
+
36
+ return {
37
+ buffer: resolution.content,
38
+ mimeType: resolution.metadata?.mimeType || providedMimeType,
39
+ fileName: resolution.metadata?.fileName || providedFileName,
40
+ wasReference: true,
41
+ };
42
+ } catch (error) {
43
+ const errorMsg =
44
+ error instanceof Error
45
+ ? error.message
46
+ : 'Unknown error resolving reference';
47
+ throw new Error(`Reference resolution failed: ${errorMsg}`);
48
+ }
49
+ }
50
+
51
+ return handleDirectContent(trimmedInput, providedMimeType, providedFileName);
52
+ }
53
+
54
+ /**
55
+ * Handles direct content (base64 or plain text)
56
+ */
57
+ function handleDirectContent(
58
+ input: string,
59
+ providedMimeType?: string,
60
+ providedFileName?: string
61
+ ): ContentResolutionResult {
62
+ const isValidBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(input);
63
+
64
+ if (isValidBase64) {
65
+ try {
66
+ const buffer = Buffer.from(input, 'base64');
67
+ return {
68
+ buffer,
69
+ mimeType: providedMimeType,
70
+ fileName: providedFileName,
71
+ wasReference: false,
72
+ };
73
+ } catch (error) {
74
+ throw new Error(
75
+ 'Failed to decode base64 data. Please ensure the data is properly encoded.'
76
+ );
77
+ }
78
+ }
79
+
80
+ const buffer = Buffer.from(input, 'utf8');
81
+ return {
82
+ buffer,
83
+ mimeType: providedMimeType || 'text/plain',
84
+ fileName: providedFileName,
85
+ wasReference: false,
86
+ };
87
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Generates default metadata for NFT inscription
3
+ */
4
+ export function generateDefaultMetadata(params: {
5
+ name?: string;
6
+ creator?: string;
7
+ description?: string;
8
+ type?: string;
9
+ fileName?: string;
10
+ mimeType?: string;
11
+ operatorAccount: string;
12
+ }) {
13
+ const defaultName = params.fileName?.replace(/\.[^/.]+$/, '') || 'Hashinal NFT';
14
+ const defaultType = params.mimeType?.startsWith('image/') ? 'image' :
15
+ params.mimeType?.startsWith('video/') ? 'video' :
16
+ params.mimeType?.startsWith('audio/') ? 'audio' : 'media';
17
+
18
+ return {
19
+ name: params.name || defaultName,
20
+ creator: params.creator || params.operatorAccount,
21
+ description: params.description || `${defaultType.charAt(0).toUpperCase() + defaultType.slice(1)} NFT inscribed as Hashinal`,
22
+ type: params.type || defaultType,
23
+ image: '',
24
+ };
25
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Validates content reference format
5
+ */
6
+ export const contentRefSchema = z
7
+ .string()
8
+ .regex(/^content-ref:[a-fA-F0-9]{64}$/, 'Content reference must be in format "content-ref:[64-char hex]"')
9
+ .describe('Content reference in format "content-ref:[id]"');
10
+
11
+ /**
12
+ * Validates content reference or returns error for dumber models
13
+ */
14
+ export function validateContentRef(input: string): string {
15
+ try {
16
+ return contentRefSchema.parse(input);
17
+ } catch (error) {
18
+ throw new Error(`Invalid content reference format. Expected "content-ref:[64-character-hash]" but got "${input}"`);
19
+ }
20
+ }
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * HIP-412 file schema for multi-file NFTs
5
+ */
6
+ export const hip412FileSchema = z.object({
7
+ uri: z.string().describe('URI of the file'),
8
+ checksum: z.string().optional().describe('SHA-256 checksum of the file'),
9
+ is_default_file: z.boolean().optional().describe('Whether this is the default file'),
10
+ type: z.string().describe('MIME type of the file'),
11
+ });
12
+
13
+ /**
14
+ * HIP-412 attribute schema for NFT traits
15
+ */
16
+ export const hip412AttributeSchema = z.object({
17
+ trait_type: z.string().describe('The trait type'),
18
+ value: z.union([z.string(), z.number()]).describe('The trait value'),
19
+ display_type: z.string().optional().describe('Display type for the attribute'),
20
+ });
21
+
22
+ /**
23
+ * HIP-412 compliant metadata schema for Hedera NFTs
24
+ */
25
+ export const hip412MetadataSchema = z.object({
26
+ name: z.string().describe('Token name (required by HIP-412)'),
27
+ description: z.string().describe('Human readable description (required by HIP-412)'),
28
+ image: z.string().describe('Preview image URI (required by HIP-412)'),
29
+ type: z.string().describe('MIME type (required by HIP-412)'),
30
+ creator: z.string().optional().describe('Creator name or comma-separated names'),
31
+ creatorDID: z.string().optional().describe('Decentralized identifier for creator'),
32
+ checksum: z.string().optional().describe('SHA-256 checksum of the image'),
33
+ format: z.string().optional().default('HIP412@2.0.0').describe('Metadata format version'),
34
+ files: z.array(hip412FileSchema).optional().describe('Array of files for multi-file NFTs'),
35
+ attributes: z.array(hip412AttributeSchema).optional().describe('NFT attributes/traits'),
36
+ properties: z.record(z.unknown()).optional().describe('Additional properties'),
37
+ });
38
+
39
+ /**
40
+ * Validates metadata against HIP-412 standard
41
+ */
42
+ export function validateHIP412Metadata(metadata: any): z.infer<typeof hip412MetadataSchema> {
43
+ try {
44
+ return hip412MetadataSchema.parse(metadata);
45
+ } catch (error) {
46
+ if (error instanceof z.ZodError) {
47
+ const issues = error.errors.map(err => `${err.path.join('.')}: ${err.message}`).join('; ');
48
+ throw new Error(`HIP-412 metadata validation failed: ${issues}`);
49
+ }
50
+ throw error;
51
+ }
52
+ }