Fix reported issues and add template identifier generation logic

This commit is contained in:
Kuldeep
2026-01-15 15:18:29 +00:00
parent 3d79e4869a
commit 237ddc2d28
13 changed files with 5690 additions and 5618 deletions

View File

@@ -1,8 +1,5 @@
{
"extends": [
"@generalprotocols/eslint-config/typescript",
"plugin:prettier/recommended"
],
"extends": ["@generalprotocols/eslint-config/typescript", "plugin:prettier/recommended"],
"parserOptions": {
"ecmaVersion": 2020,
"project": "./tsconfig.json",
@@ -23,7 +20,10 @@
}
],
"import/no-internal-modules": [
"error"
"error",
{
"allow": ["**/*"]
}
],
"max-len": [
"error",

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 GeneralProtocols / XO
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11004
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"docs": "typedoc --hideGenerator --categorizeByGroup",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"prettier": "prettier --write .",
"prebuild": "node scripts/generate-schema.js",
"prepublishOnly": "npm run build",
"syntax": "tsc --noEmit"
@@ -37,7 +38,7 @@
],
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.8",
"@xo-cash/types": "file:../types"
"@xo-cash/types": "0.0.1"
},
"devDependencies": {
"@generalprotocols/eslint-config": "^1.0.1",

View File

@@ -11,53 +11,44 @@ const rootDir = join(__dirname, '..');
// exec helper
function exec(command, options = {}) {
const defaultOptions = {
stdio: 'inherit',
cwd: rootDir,
shell: true,
...options,
};
return execSync(command, defaultOptions);
const defaultOptions = {
stdio: 'inherit',
cwd: rootDir,
shell: true,
...options,
};
return execSync(command, defaultOptions);
}
try {
const schemaPath = join(rootDir, 'src/parser/xo-template.schema.json');
const validatorPath = join(rootDir, 'src/parser/ajv/validate-xo-template.js');
const tsconfigPath = join(rootDir, 'tsconfig.json');
const templatePath = join(rootDir, 'node_modules/@xo-cash/types/build/template.d.ts');
const schemaPath = join(rootDir, 'src/parser/xo-template.schema.json');
const validatorPath = join(rootDir, 'src/parser/ajv/validate-xo-template.js');
const tsconfigPath = join(rootDir, 'tsconfig.json');
const templatePath = join(
rootDir,
'node_modules/@xo-cash/types/build/template.d.ts',
);
// Generate schema JSON (write to file directly instead of shell redirection)
const schemaContent = execSync(
`ts-json-schema-generator --no-ref-encode --tsconfig "${tsconfigPath}" --path "${templatePath}" --type "XOTemplate"`,
{ encoding: 'utf8', cwd: rootDir },
);
writeFileSync(schemaPath, schemaContent, 'utf8');
// Generate schema JSON (write to file directly instead of shell redirection)
const schemaContent = execSync(
`ts-json-schema-generator --no-ref-encode --tsconfig "${tsconfigPath}" --path "${templatePath}" --type "XOTemplate"`,
{ encoding: 'utf8', cwd: rootDir },
);
writeFileSync(schemaPath, schemaContent, 'utf8');
// Compile validator with AJV
exec(`ajv compile -s "${schemaPath}" --allowUnionTypes --all-errors -o "${validatorPath}"`);
// Compile validator with AJV
exec(
`ajv compile -s "${schemaPath}" --allowUnionTypes -o "${validatorPath}"`,
);
// Transform the generated validator code
let code = readFileSync(validatorPath, 'utf8');
// Transform the generated validator code
let code = readFileSync(validatorPath, 'utf8');
code = code
.replace(/"use strict";module\.exports = [^;]+;module\.exports\.default = [^;]+;/, 'export default validate20;')
.replace(/;const /g, ';\nconst ')
.replace(/;function /g, ';\nfunction ')
.replace(/\}function /g, '}\nfunction ');
code = code
.replace(
/"use strict";module\.exports = [^;]+;module\.exports\.default = [^;]+;/,
'export default validate20;',
)
.replace(/;const /g, ';\nconst ')
.replace(/;function /g, ';\nfunction ')
.replace(/\}function /g, '}\nfunction ');
writeFileSync(validatorPath, code, 'utf8');
writeFileSync(validatorPath, code, 'utf8');
console.log('Schema generation complete!');
console.log('Schema generation complete!');
} catch (error) {
console.error('Error generating schema:', error.message);
process.exit(1);
console.error('Error generating schema:', error.message);
process.exit(1);
}

15
src/errors.ts Normal file
View File

@@ -0,0 +1,15 @@
/* eslint-disable max-classes-per-file */
export class InvalidTemplateError extends Error {
constructor(errorMessage: string) {
const message = `Invalid template: ${errorMessage}`;
super(message);
}
}
export class FailedToParseTemplateError extends Error {
constructor(errorMessage: string) {
const message = `Failed to parse template: ${errorMessage}`;
super(message);
}
}

View File

@@ -1,3 +1,5 @@
export * from './errors.js';
export * from './parser/ajv/ajv-utils.js';
export * from './parser/xo-template.js';
export * from './parser/errors.js';
export * from './script.js';
export * from './templates.js';

View File

@@ -3,31 +3,31 @@
* https://github.com/bitauth/libauth
*/
export type AjvError<Keyword = string, Params = { [paramName: string]: number | string }> = {
keyword: Keyword;
instancePath: string;
schemaPath: string;
params: Params;
message: string;
keyword: Keyword;
instancePath: string;
schemaPath: string;
params: Params;
message: string;
};
export type XOAjvError =
| AjvError<'additionalProperties', { additionalProperty: string }>
| AjvError<'required', { missingProperty: string }>
| AjvError<'type', { type: string }>;
| AjvError<'additionalProperties', { additionalProperty: string }>
| AjvError<'required', { missingProperty: string }>
| AjvError<'type', { type: string }>;
/**
* Note: these types cover only XO use cases; other `ajv` error types are
* possible using other settings.
*/
export type AjvValidator<T = unknown> = {
(
data: unknown,
dataCxt?: {
instancePath?: string;
parentData: any;
parentDataProperty: any;
rootData?: any;
},
): data is T;
errors?: XOAjvError[] | null;
(
data: unknown,
dataCxt?: {
instancePath?: string;
parentData: any;
parentDataProperty: any;
rootData?: any;
},
): data is T;
errors?: XOAjvError[] | null;
};

View File

@@ -1,33 +1,60 @@
/* eslint-disable implicit-arrow-linebreak, @typescript-eslint/explicit-function-return-type, newline-before-return */
/**
* This code is inspired by and adapted from:
* https://github.com/bitauth/libauth
* This code is inspired from https://github.com/bitauth/libauth
*/
import { lossyNormalize } from '@bitauth/libauth';
import xoTemplateValidator from './validate-xo-template.js';
import type { AjvValidator, XOAjvError } from './ajv-types.js';
const avjErrorsToDescription = (errors: XOAjvError[]): string =>
// TODO: translate instancePath
errors.map((error) => `${error.instancePath}: ${error.message}`).join(',');
import { lossyNormalize } from '@bitauth/libauth';
import type { AjvValidator, XOAjvError } from './ajv-types.js';
import { FailedToParseTemplateError } from '../../errors.js';
const errorsToDescription = (errors: XOAjvError[]): string => {
// TODO: translate instancePath and make errors more descriptive.
const descriptions = errors.map((error) => `${error.instancePath}: ${error.message}`).join(',');
return descriptions;
};
/**
* Given an untrusted JSON string or object and an AJV validator, verify that
* Given an untrusted JSON string or object, verify that
* the untrusted value is of the expected shape. Note, this method first
* normalizes all characters in the input using `Normalization Form KC`
* (Compatibility Decomposition, followed by Canonical Composition).
*
* @param {unknown} jsonOrObject - The JSON string or object to parse
* @returns {T} The parsed object
* @throws {FailedToParseTemplateError} If JSON parsing fails
*/
export const ajvStandaloneJsonParse = <T>(untrustedJsonOrObject: unknown, validator: AjvValidator<T>) => {
try {
const stringified = typeof untrustedJsonOrObject === 'string' ? untrustedJsonOrObject : JSON.stringify(untrustedJsonOrObject);
const normalized = lossyNormalize(stringified);
const parsed = JSON.parse(normalized) as unknown;
if (validator(parsed)) {
return parsed;
}
// @ts-ignore
return avjErrorsToDescription(xoTemplateValidator.errors!);
} catch (e) {
return `Invalid JSON. ${String(e)}`;
}
export const parseJson = <T>(jsonOrObject: unknown): T => {
try {
// If the JSON object is a string, use it directly.
const stringified = typeof jsonOrObject === 'string' ? jsonOrObject : JSON.stringify(jsonOrObject);
// Normalize the JSON string using `Normalization Form KC` (Compatibility Decomposition, followed by Canonical Composition).
const normalized = lossyNormalize(stringified);
// Parse the normalized JSON string into the expected type.
const parsed = JSON.parse(normalized);
// Return the parsed object.
return parsed;
} catch (error) {
// Error wrapping. If the JSON parsing fails, throw an error.
throw new FailedToParseTemplateError(error instanceof Error ? error.message : String(error));
}
};
/**
* Validates a parsed object against an AJV schema.
*
* @param parsed - The parsed object to validate
* @param validator - The AJV validator function
* @returns The validated object if valid, or an error message string if invalid
*/
export const validateSchema = <T>(parsed: unknown, validator: AjvValidator<T>): T | string => {
if (validator(parsed)) {
return parsed;
}
// If the object is invalid, return the error message.
return errorsToDescription(validator.errors ?? []);
};

View File

@@ -1,6 +0,0 @@
export class TemplateImportFailedError extends Error {
constructor(templateIdentifier: string) {
const message = `Template import failed: ${templateIdentifier}`;
super(message);
}
}

View File

@@ -2,14 +2,15 @@
* This code is inspired by and adapted from:
* https://github.com/bitauth/libauth
*/
import type { XOTemplate } from '@xo-cash/types';
import { ajvStandaloneJsonParse } from './ajv/ajv-utils.js';
// eslint-disable-next-line import/no-internal-modules
import type { AjvValidator } from './ajv/ajv-types.js';
import { parseJson, validateSchema } from './ajv/ajv-utils.js';
import { InvalidTemplateError } from '../errors.js';
import xoTemplateValidator from './ajv/validate-xo-template.js';
import { TemplateImportFailedError } from './errors.js';
/**
* Safely parse and validate a wallet template, returning either an
* Safely parse and validate a XO template, returning either an
* error message as a string or a valid {@link XOTemplate}. The
* template may be provided either as an untrusted JSON string or as a
* pre-parsed object.
@@ -22,22 +23,29 @@ import { TemplateImportFailedError } from './errors.js';
* confirmed to exist.
* - The derivation paths of each HdKey are validated against each other.
*
* This method does not validate the CashAssembly contents of scripts (by
* This method does not perform the following:
* - TODO: Validate the CashAssembly contents of scripts (by
* attempting compilation, evaluating {@link XOTemplateScriptTest}s,
* or testing scenario generation).
* - TODO: Perform search to verify references.
*
* @param untrustedJsonOrObject - the JSON string or object to validate as a
* @param {unknown} untrustedJsonOrObject - the JSON string or object to validate as a
* XO template
* @returns {XOTemplate} The parsed and validated template
* @throws {InvalidTemplateError} If the template is invalid
*/
export const sanitizeTemplate = (untrustedJsonOrObject: unknown): XOTemplate => {
const template = ajvStandaloneJsonParse<XOTemplate>(
untrustedJsonOrObject,
// @ts-ignore
xoTemplateValidator,
);
if (typeof template === 'string') {
throw new TemplateImportFailedError(template);
}
export const parseTemplate = (untrustedJsonOrObject: unknown): XOTemplate => {
// Parse the JSON string or object into an unknown object.
const parsed = parseJson<unknown>(untrustedJsonOrObject);
return template;
// Validate the unknown object against the XO template schema.
const templateOrError = validateSchema(parsed, xoTemplateValidator as AjvValidator<XOTemplate>);
// If the template is invalid, throw an error.
if (typeof templateOrError === 'string') {
throw new InvalidTemplateError(templateOrError);
}
// Return the validated template.
return templateOrError;
};

View File

@@ -1,14 +1,13 @@
import { binToHex, instantiateSha256 } from '@bitauth/libauth';
import { binToHex, sha256 } from '@bitauth/libauth';
/**
* Converts a script to a scripthash.
* @param {Uint8Array} script - The script to convert.
* @returns {Promise<string>} The scripthash as a hexadecimal string.
* @returns {string} The scripthash as a hexadecimal string.
*/
export const scriptToScriptHash = async (script: Uint8Array): Promise<string> => {
const sha256 = await instantiateSha256();
const hash = sha256.hash(script);
const reversed = hash.reverse();
export const scriptToScriptHash = (script: Uint8Array): string => {
const hash = sha256.hash(script);
const reversed = hash.reverse();
return binToHex(reversed);
return binToHex(reversed);
};

14
src/templates.ts Normal file
View File

@@ -0,0 +1,14 @@
import { binToHex, sha256, utf8ToBin, stringify } from '@bitauth/libauth';
import type { XOTemplate } from '@xo-cash/types';
/**
* Generates a deterministic template identifier by hashing the template.
*
* @param template - The template to generate an identifier for.
* @returns The template identifier for the template.
*/
export const generateTemplateIdentifier = (template: XOTemplate): string => {
// TODO: Finalise on the stringify method and ensure unique identifier generation is sufficient.
return binToHex(sha256.hash(utf8ToBin(stringify(template))));
};