Package setup, parser and a utility function
This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
**/build/
|
||||||
|
**/node_modules/
|
||||||
52
.eslintrc.json
Normal file
52
.eslintrc.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"@generalprotocols/eslint-config/typescript",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"project": "./tsconfig.json",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"js": "always"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/no-internal-modules": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"max-len": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"code": 150,
|
||||||
|
"ignoreUrls": true,
|
||||||
|
"ignoreStrings": true,
|
||||||
|
"ignoreTemplateLiterals": true,
|
||||||
|
"ignoreComments": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object-curly-newline": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"multiline": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.vscode/
|
||||||
|
build/
|
||||||
|
build/**
|
||||||
|
node_modules/
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
*.js
|
||||||
|
*.js.map
|
||||||
|
*.d.ts
|
||||||
|
*.d.ts.map
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
src/parser/xo-template.schema.json
|
||||||
|
src/parser/ajv/validate-xo-template.js
|
||||||
|
|
||||||
|
# Exception: include generate-schema.js
|
||||||
|
!scripts/generate-schema.js
|
||||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 150,
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
24
README.md
24
README.md
@@ -0,0 +1,24 @@
|
|||||||
|
# @xo-cash/utils
|
||||||
|
|
||||||
|
XO utilities and parser
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @xo-cash/utils
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { scriptToScriptHash } from '@xo-cash/utils';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Repository](https://gitlab.com/GeneralProtocols/xo/utils)
|
||||||
|
- [Issues](https://gitlab.com/GeneralProtocols/xo/utils/issues)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
5507
package-lock.json
generated
Normal file
5507
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
59
package.json
Normal file
59
package.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "@xo-cash/utils",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "XO Cash utilities",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./build/index.js"
|
||||||
|
},
|
||||||
|
"types": "./build/index.d.ts",
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run clean && tsc",
|
||||||
|
"clean": "del-cli ./build",
|
||||||
|
"docs": "typedoc --hideGenerator --categorizeByGroup",
|
||||||
|
"lint": "eslint . --ext .ts",
|
||||||
|
"lint:fix": "eslint . --ext .ts --fix",
|
||||||
|
"prebuild": "node scripts/generate-schema.js",
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
|
"syntax": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://gitlab.com/GeneralProtocols/xo/utils.git"
|
||||||
|
},
|
||||||
|
"author": "GeneralProtocols",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://gitlab.com/GeneralProtocols/xo/utils/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://gitlab.com/GeneralProtocols/xo/utils#readme",
|
||||||
|
"keywords": [
|
||||||
|
"bitcoin cash",
|
||||||
|
"xo cash"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@bitauth/libauth": "^3.1.0-next.8",
|
||||||
|
"@xo-cash/types": "file:../types"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@generalprotocols/eslint-config": "^1.0.1",
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"ajv-cli": "^5.0.0",
|
||||||
|
"del-cli": "^7.0.0",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"ts-json-schema-generator": "^2.4.0",
|
||||||
|
"typedoc": "^0.28.15",
|
||||||
|
"typedoc-plugin-coverage": "^4.0.2",
|
||||||
|
"typescript": "^5.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
63
scripts/generate-schema.js
Normal file
63
scripts/generate-schema.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const rootDir = join(__dirname, '..');
|
||||||
|
|
||||||
|
// exec helper
|
||||||
|
function exec(command, options = {}) {
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 -o "${validatorPath}"`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 ');
|
||||||
|
|
||||||
|
writeFileSync(validatorPath, code, 'utf8');
|
||||||
|
|
||||||
|
console.log('Schema generation complete!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating schema:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
3
src/index.ts
Normal file
3
src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './parser/xo-template.js';
|
||||||
|
export * from './parser/errors.js';
|
||||||
|
export * from './script.js';
|
||||||
33
src/parser/ajv/ajv-types.ts
Normal file
33
src/parser/ajv/ajv-types.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* This code is inspired by and adapted from:
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type XOAjvError =
|
||||||
|
| 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;
|
||||||
|
};
|
||||||
33
src/parser/ajv/ajv-utils.ts
Normal file
33
src/parser/ajv/ajv-utils.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* 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
|
||||||
|
*/
|
||||||
|
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(',');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an untrusted JSON string or object and an AJV validator, 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).
|
||||||
|
*/
|
||||||
|
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)}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
6
src/parser/errors.ts
Normal file
6
src/parser/errors.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export class TemplateImportFailedError extends Error {
|
||||||
|
constructor(templateIdentifier: string) {
|
||||||
|
const message = `Template import failed: ${templateIdentifier}`;
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/parser/xo-template.ts
Normal file
43
src/parser/xo-template.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 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 xoTemplateValidator from './ajv/validate-xo-template.js';
|
||||||
|
import { TemplateImportFailedError } from './errors.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely parse and validate a wallet 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.
|
||||||
|
*
|
||||||
|
* This method validates both the structure and the contents of a template:
|
||||||
|
* - All properties and sub-properties are verified to be of the expected type.
|
||||||
|
* - The template contains no unknown properties.
|
||||||
|
* - The ID of each entity, script, and scenario is confirmed to be unique.
|
||||||
|
* - Script IDs referenced by entities and other scripts (via `unlocks`) are
|
||||||
|
* 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
|
||||||
|
* attempting compilation, evaluating {@link XOTemplateScriptTest}s,
|
||||||
|
* or testing scenario generation).
|
||||||
|
*
|
||||||
|
* @param untrustedJsonOrObject - the JSON string or object to validate as a
|
||||||
|
* XO template
|
||||||
|
*/
|
||||||
|
export const sanitizeTemplate = (untrustedJsonOrObject: unknown): XOTemplate => {
|
||||||
|
const template = ajvStandaloneJsonParse<XOTemplate>(
|
||||||
|
untrustedJsonOrObject,
|
||||||
|
// @ts-ignore
|
||||||
|
xoTemplateValidator,
|
||||||
|
);
|
||||||
|
if (typeof template === 'string') {
|
||||||
|
throw new TemplateImportFailedError(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
|
};
|
||||||
14
src/script.ts
Normal file
14
src/script.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { binToHex, instantiateSha256 } 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.
|
||||||
|
*/
|
||||||
|
export const scriptToScriptHash = async (script: Uint8Array): Promise<string> => {
|
||||||
|
const sha256 = await instantiateSha256();
|
||||||
|
const hash = sha256.hash(script);
|
||||||
|
const reversed = hash.reverse();
|
||||||
|
|
||||||
|
return binToHex(reversed);
|
||||||
|
};
|
||||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "build",
|
||||||
|
"incremental": true,
|
||||||
|
"tsBuildInfoFile": "./build/.tsbuildinfo",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"traceResolution": false,
|
||||||
|
"listEmittedFiles": false,
|
||||||
|
"listFiles": false,
|
||||||
|
"pretty": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "build", "scripts"],
|
||||||
|
"compileOnSave": false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user