Fix reported issues and add template identifier generation logic
This commit is contained in:
@@ -1,8 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["@generalprotocols/eslint-config/typescript", "plugin:prettier/recommended"],
|
||||||
"@generalprotocols/eslint-config/typescript",
|
|
||||||
"plugin:prettier/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2020,
|
"ecmaVersion": 2020,
|
||||||
"project": "./tsconfig.json",
|
"project": "./tsconfig.json",
|
||||||
@@ -23,7 +20,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"import/no-internal-modules": [
|
"import/no-internal-modules": [
|
||||||
"error"
|
"error",
|
||||||
|
{
|
||||||
|
"allow": ["**/*"]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"max-len": [
|
"max-len": [
|
||||||
"error",
|
"error",
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
11004
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
|||||||
"docs": "typedoc --hideGenerator --categorizeByGroup",
|
"docs": "typedoc --hideGenerator --categorizeByGroup",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"lint:fix": "eslint . --ext .ts --fix",
|
"lint:fix": "eslint . --ext .ts --fix",
|
||||||
|
"prettier": "prettier --write .",
|
||||||
"prebuild": "node scripts/generate-schema.js",
|
"prebuild": "node scripts/generate-schema.js",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"syntax": "tsc --noEmit"
|
"syntax": "tsc --noEmit"
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.1.0-next.8",
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
"@xo-cash/types": "file:../types"
|
"@xo-cash/types": "0.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@generalprotocols/eslint-config": "^1.0.1",
|
"@generalprotocols/eslint-config": "^1.0.1",
|
||||||
|
|||||||
@@ -11,53 +11,44 @@ const rootDir = join(__dirname, '..');
|
|||||||
|
|
||||||
// exec helper
|
// exec helper
|
||||||
function exec(command, options = {}) {
|
function exec(command, options = {}) {
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
shell: true,
|
shell: true,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
return execSync(command, defaultOptions);
|
return execSync(command, defaultOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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');
|
// Generate schema JSON (write to file directly instead of shell redirection)
|
||||||
const validatorPath = join(rootDir, 'src/parser/ajv/validate-xo-template.js');
|
const schemaContent = execSync(
|
||||||
const tsconfigPath = join(rootDir, 'tsconfig.json');
|
`ts-json-schema-generator --no-ref-encode --tsconfig "${tsconfigPath}" --path "${templatePath}" --type "XOTemplate"`,
|
||||||
const templatePath = join(
|
{ encoding: 'utf8', cwd: rootDir },
|
||||||
rootDir,
|
);
|
||||||
'node_modules/@xo-cash/types/build/template.d.ts',
|
writeFileSync(schemaPath, schemaContent, 'utf8');
|
||||||
);
|
|
||||||
|
|
||||||
// Generate schema JSON (write to file directly instead of shell redirection)
|
// Compile validator with AJV
|
||||||
const schemaContent = execSync(
|
exec(`ajv compile -s "${schemaPath}" --allowUnionTypes --all-errors -o "${validatorPath}"`);
|
||||||
`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
|
// Transform the generated validator code
|
||||||
exec(
|
let code = readFileSync(validatorPath, 'utf8');
|
||||||
`ajv compile -s "${schemaPath}" --allowUnionTypes -o "${validatorPath}"`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Transform the generated validator code
|
code = code
|
||||||
let code = readFileSync(validatorPath, 'utf8');
|
.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
|
writeFileSync(validatorPath, code, 'utf8');
|
||||||
.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');
|
console.log('Schema generation complete!');
|
||||||
|
|
||||||
console.log('Schema generation complete!');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating schema:', error.message);
|
console.error('Error generating schema:', error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/errors.ts
Normal file
15
src/errors.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export * from './errors.js';
|
||||||
|
export * from './parser/ajv/ajv-utils.js';
|
||||||
export * from './parser/xo-template.js';
|
export * from './parser/xo-template.js';
|
||||||
export * from './parser/errors.js';
|
|
||||||
export * from './script.js';
|
export * from './script.js';
|
||||||
|
export * from './templates.js';
|
||||||
|
|||||||
@@ -3,31 +3,31 @@
|
|||||||
* https://github.com/bitauth/libauth
|
* https://github.com/bitauth/libauth
|
||||||
*/
|
*/
|
||||||
export type AjvError<Keyword = string, Params = { [paramName: string]: number | string }> = {
|
export type AjvError<Keyword = string, Params = { [paramName: string]: number | string }> = {
|
||||||
keyword: Keyword;
|
keyword: Keyword;
|
||||||
instancePath: string;
|
instancePath: string;
|
||||||
schemaPath: string;
|
schemaPath: string;
|
||||||
params: Params;
|
params: Params;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type XOAjvError =
|
export type XOAjvError =
|
||||||
| AjvError<'additionalProperties', { additionalProperty: string }>
|
| AjvError<'additionalProperties', { additionalProperty: string }>
|
||||||
| AjvError<'required', { missingProperty: string }>
|
| AjvError<'required', { missingProperty: string }>
|
||||||
| AjvError<'type', { type: string }>;
|
| AjvError<'type', { type: string }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: these types cover only XO use cases; other `ajv` error types are
|
* Note: these types cover only XO use cases; other `ajv` error types are
|
||||||
* possible using other settings.
|
* possible using other settings.
|
||||||
*/
|
*/
|
||||||
export type AjvValidator<T = unknown> = {
|
export type AjvValidator<T = unknown> = {
|
||||||
(
|
(
|
||||||
data: unknown,
|
data: unknown,
|
||||||
dataCxt?: {
|
dataCxt?: {
|
||||||
instancePath?: string;
|
instancePath?: string;
|
||||||
parentData: any;
|
parentData: any;
|
||||||
parentDataProperty: any;
|
parentDataProperty: any;
|
||||||
rootData?: any;
|
rootData?: any;
|
||||||
},
|
},
|
||||||
): data is T;
|
): data is T;
|
||||||
errors?: XOAjvError[] | null;
|
errors?: XOAjvError[] | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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:
|
* This code is inspired from https://github.com/bitauth/libauth
|
||||||
* 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 =>
|
import { lossyNormalize } from '@bitauth/libauth';
|
||||||
// TODO: translate instancePath
|
import type { AjvValidator, XOAjvError } from './ajv-types.js';
|
||||||
errors.map((error) => `${error.instancePath}: ${error.message}`).join(',');
|
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
|
* the untrusted value is of the expected shape. Note, this method first
|
||||||
* normalizes all characters in the input using `Normalization Form KC`
|
* normalizes all characters in the input using `Normalization Form KC`
|
||||||
* (Compatibility Decomposition, followed by Canonical Composition).
|
* (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>) => {
|
export const parseJson = <T>(jsonOrObject: unknown): T => {
|
||||||
try {
|
try {
|
||||||
const stringified = typeof untrustedJsonOrObject === 'string' ? untrustedJsonOrObject : JSON.stringify(untrustedJsonOrObject);
|
// If the JSON object is a string, use it directly.
|
||||||
const normalized = lossyNormalize(stringified);
|
const stringified = typeof jsonOrObject === 'string' ? jsonOrObject : JSON.stringify(jsonOrObject);
|
||||||
const parsed = JSON.parse(normalized) as unknown;
|
|
||||||
if (validator(parsed)) {
|
// Normalize the JSON string using `Normalization Form KC` (Compatibility Decomposition, followed by Canonical Composition).
|
||||||
return parsed;
|
const normalized = lossyNormalize(stringified);
|
||||||
}
|
|
||||||
// @ts-ignore
|
// Parse the normalized JSON string into the expected type.
|
||||||
return avjErrorsToDescription(xoTemplateValidator.errors!);
|
const parsed = JSON.parse(normalized);
|
||||||
} catch (e) {
|
|
||||||
return `Invalid JSON. ${String(e)}`;
|
// 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 ?? []);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export class TemplateImportFailedError extends Error {
|
|
||||||
constructor(templateIdentifier: string) {
|
|
||||||
const message = `Template import failed: ${templateIdentifier}`;
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
* This code is inspired by and adapted from:
|
* This code is inspired by and adapted from:
|
||||||
* https://github.com/bitauth/libauth
|
* https://github.com/bitauth/libauth
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { XOTemplate } from '@xo-cash/types';
|
import type { XOTemplate } from '@xo-cash/types';
|
||||||
import { ajvStandaloneJsonParse } from './ajv/ajv-utils.js';
|
import type { AjvValidator } from './ajv/ajv-types.js';
|
||||||
// eslint-disable-next-line import/no-internal-modules
|
import { parseJson, validateSchema } from './ajv/ajv-utils.js';
|
||||||
|
import { InvalidTemplateError } from '../errors.js';
|
||||||
import xoTemplateValidator from './ajv/validate-xo-template.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
|
* error message as a string or a valid {@link XOTemplate}. The
|
||||||
* template may be provided either as an untrusted JSON string or as a
|
* template may be provided either as an untrusted JSON string or as a
|
||||||
* pre-parsed object.
|
* pre-parsed object.
|
||||||
@@ -22,22 +23,29 @@ import { TemplateImportFailedError } from './errors.js';
|
|||||||
* confirmed to exist.
|
* confirmed to exist.
|
||||||
* - The derivation paths of each HdKey are validated against each other.
|
* - 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,
|
* attempting compilation, evaluating {@link XOTemplateScriptTest}s,
|
||||||
* or testing scenario generation).
|
* 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
|
* XO template
|
||||||
|
* @returns {XOTemplate} The parsed and validated template
|
||||||
|
* @throws {InvalidTemplateError} If the template is invalid
|
||||||
*/
|
*/
|
||||||
export const sanitizeTemplate = (untrustedJsonOrObject: unknown): XOTemplate => {
|
export const parseTemplate = (untrustedJsonOrObject: unknown): XOTemplate => {
|
||||||
const template = ajvStandaloneJsonParse<XOTemplate>(
|
// Parse the JSON string or object into an unknown object.
|
||||||
untrustedJsonOrObject,
|
const parsed = parseJson<unknown>(untrustedJsonOrObject);
|
||||||
// @ts-ignore
|
|
||||||
xoTemplateValidator,
|
|
||||||
);
|
|
||||||
if (typeof template === 'string') {
|
|
||||||
throw new TemplateImportFailedError(template);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { binToHex, instantiateSha256 } from '@bitauth/libauth';
|
import { binToHex, sha256 } from '@bitauth/libauth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a script to a scripthash.
|
* Converts a script to a scripthash.
|
||||||
* @param {Uint8Array} script - The script to convert.
|
* @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> => {
|
export const scriptToScriptHash = (script: Uint8Array): string => {
|
||||||
const sha256 = await instantiateSha256();
|
const hash = sha256.hash(script);
|
||||||
const hash = sha256.hash(script);
|
const reversed = hash.reverse();
|
||||||
const reversed = hash.reverse();
|
|
||||||
|
|
||||||
return binToHex(reversed);
|
return binToHex(reversed);
|
||||||
};
|
};
|
||||||
|
|||||||
14
src/templates.ts
Normal file
14
src/templates.ts
Normal 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))));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user