Refactor, Added documentation, moved from avj-cli to avj, update script and added tests

This commit is contained in:
Kuldeep
2026-04-19 09:33:48 +00:00
parent cfd4c6a43b
commit 013583e7f8
26 changed files with 4216 additions and 9970 deletions

View File

@@ -0,0 +1,90 @@
import { expect, test } from 'vitest';
import { extendedJsonReviver } from '../source/index.ts';
/**
* Tests that extendedJsonReviver reconstructs a positive bigint.
*/
const testReviverReconstructsPositiveBigint = (): void => {
// A positive bigint
const reconstructed = extendedJsonReviver('_', '<bigint: 42n>');
// The value is reconstructed as a bigint, not left as a string
expect(reconstructed).toBe(42n);
};
/**
* Tests that extendedJsonReviver reconstructs a negative bigint.
*/
const testReviverReconstructsNegativeBigint = (): void => {
// A negative bigint
const reconstructed = extendedJsonReviver('_', '<bigint: -7n>');
// The sign is preserved
expect(reconstructed).toBe(-7n);
};
/**
* Tests that extendedJsonReviver reconstructs zero as a bigint.
*/
const testReviverReconstructsZeroBigint = (): void => {
const reconstructed = extendedJsonReviver('_', '<bigint: 0n>');
// Zero is reconstructed as bigint 0n, not the number 0 or the string '0'
expect(reconstructed).toBe(0n);
};
/**
* Tests that extendedJsonReviver reconstructs a Uint8Array.
*/
const testReviverReconstructsUint8Array = (): void => {
// A Uint8Array
const reconstructed = extendedJsonReviver('_', '<uint8array: abcd>');
// The value is a Uint8Array with the correct bytes
expect(reconstructed).toStrictEqual(new Uint8Array([ 0xab, 0xcd ]));
};
/**
* Tests that extendedJsonReviver reconstructs an empty Uint8Array when the hex string is empty.
*/
const testReviverReconstructsEmptyUint8Array = (): void => {
const reconstructed = extendedJsonReviver('_', '<uint8array: >');
// An empty Uint8Array is returned, not null, undefined, or an empty string
expect(reconstructed).toStrictEqual(new Uint8Array(0));
};
/**
* Tests that extendedJsonReviver passes through plain strings.
*/
const testReviverPassesThroughPlainString = (): void => {
const reconstructed = extendedJsonReviver('_', 'just a string');
// The value is passed through unchanged
expect(reconstructed).toBe('just a string');
};
/**
* Tests that extendedJsonReviver passes through non-string values.
*/
const testReviverPassesThroughNonStringValues = (): void => {
// Numbers, booleans, null, and objects pass through unchanged
expect(extendedJsonReviver('_', 42)).toBe(42);
expect(extendedJsonReviver('_', true)).toBe(true);
expect(extendedJsonReviver('_', false)).toBe(false);
expect(extendedJsonReviver('_', null)).toBe(null);
expect(extendedJsonReviver('_', undefined)).toBe(undefined);
expect(extendedJsonReviver('_', { foo: 'bar' })).toStrictEqual({ foo: 'bar' });
};
const runTests = async (): Promise<void> => {
test('extendedJsonReviver: reconstructs a positive bigint', testReviverReconstructsPositiveBigint);
test('extendedJsonReviver: reconstructs a negative bigint', testReviverReconstructsNegativeBigint);
test('extendedJsonReviver: reconstructs zero as bigint', testReviverReconstructsZeroBigint);
test('extendedJsonReviver: reconstructs a Uint8Array from hex', testReviverReconstructsUint8Array);
test('extendedJsonReviver: reconstructs an empty Uint8Array', testReviverReconstructsEmptyUint8Array);
test('extendedJsonReviver: passes through plain strings', testReviverPassesThroughPlainString);
test('extendedJsonReviver: passes through non-string values', testReviverPassesThroughNonStringValues);
};
await runTests();

234
test/parse-template.test.ts Normal file
View File

@@ -0,0 +1,234 @@
import { expect, test } from 'vitest';
import type { XOTemplate } from '@xo-cash/types';
import { p2pkhTemplate } from '@xo-cash/templates';
import { TemplateInvalidError, parseTemplate, serializeTemplate } from '../source/index.ts';
/**
* Tests that parseTemplate accepts a valid XOTemplate object and returns it unchanged.
*/
const testParseTemplateAcceptsValidTemplate = (): void => {
// Parse the template
const parsedObjectTemplate: XOTemplate = parseTemplate(p2pkhTemplate);
// Parse the template from a string
const parsedStringTemplate = parseTemplate(serializeTemplate(p2pkhTemplate));
// The parsed template should be equal to the original template
expect(parsedObjectTemplate).toEqual(p2pkhTemplate);
expect(parsedStringTemplate).toEqual(p2pkhTemplate);
};
/**
* Tests that parseTemplate accepts a simple bigint value in a satoshi field and preserves it
* through the serialize/deserialize round-trip.
*/
const testParseTemplateAcceptsBigintSatoshis = (): void => {
// Get the first output key
const firstOutputKey = Object.keys(p2pkhTemplate.outputs)[0];
if (firstOutputKey === undefined) {
throw new Error('p2pkhTemplate has no outputs, test fixture is invalid');
}
const firstOutput = p2pkhTemplate.outputs[firstOutputKey];
if (firstOutput === undefined) {
throw new Error('p2pkhTemplate first output is undefined, test fixture is invalid');
}
const templateWithBigint = {
...p2pkhTemplate,
outputs: {
...p2pkhTemplate.outputs,
[firstOutputKey]: { ...firstOutput, valueSatoshis: 1000n },
},
};
// Parse the template
const parsedTemplate = parseTemplate(templateWithBigint);
// The parsed template should have the bigint value
expect(parsedTemplate.outputs[firstOutputKey]!.valueSatoshis).toBe(1000n);
};
/**
* Tests that parseTemplate preserves bigint precision for values exceeding Number.MAX_SAFE_INTEGER.
* Uses 2^54 + 1, so any naive Number conversion in the serialize/deserialize round-trip would produce the wrong value
* but BigInt conversion would preserve the exact value.
*/
const testParseTemplatePreservesBigintPrecisionBeyondMaxSafeInteger = (): void => {
// Get the first output key
const firstOutputKey = Object.keys(p2pkhTemplate.outputs)[0];
if (firstOutputKey === undefined) {
throw new Error('p2pkhTemplate has no outputs, test fixture is invalid');
}
const firstOutput = p2pkhTemplate.outputs[firstOutputKey];
if (firstOutput === undefined) {
throw new Error('p2pkhTemplate first output is undefined, test fixture is invalid');
}
const templateWithBigint = {
...p2pkhTemplate,
outputs: {
...p2pkhTemplate.outputs,
[firstOutputKey]: { ...firstOutput, valueSatoshis: 18014398509481985n },
},
};
// Parse the template
const parsedTemplate = parseTemplate(templateWithBigint);
// The parsed template should have the exact bigint value
expect(parsedTemplate.outputs[firstOutputKey]!.valueSatoshis).toBe(18014398509481985n);
};
/**
* Tests that parseTemplate throws TemplateInvalidError listing all missing required fields
*/
const testParseTemplateThrowsOnMissingRequiredFields = (): void => {
let thrownError: unknown;
try {
parseTemplate({} as XOTemplate);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage =
'Template invalid: \n'
+ '- name: expected string, received undefined\n'
+ '- description: expected string, received undefined\n'
+ '- $schema: expected string, received undefined\n'
+ '- supported: expected array, received undefined\n'
+ '- roles: expected record, received undefined\n'
+ '- start: expected array, received undefined\n'
+ '- actions: expected record, received undefined\n'
+ '- inputs: expected record, received undefined\n'
+ '- outputs: expected record, received undefined\n'
+ '- lockingScripts: expected record, received undefined\n'
+ '- scripts: expected record, received undefined';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
/**
* Tests that parseTemplate throws TemplateInvalidError naming the exact field path when a
* single required field is removed from a valid template.
*/
const testParseTemplateThrowsOnMissingScriptsField = (): void => {
// Set scripts to undefined to test the field path for a missing record
const templateWithoutScripts = { ...p2pkhTemplate, scripts: undefined };
let thrownError: unknown;
try {
parseTemplate(templateWithoutScripts);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage = 'Template invalid: \n' + '- scripts: expected record, received undefined';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
/**
* Tests that parseTemplate throws TemplateInvalidError reporting both field paths for type violations
*/
const testParseTemplateThrowsOnFieldTypeViolations = (): void => {
// Replace version with a number and name with a number to produce two simultaneous type errors.
// @ts-expect-error - version and name are intentionally wrong types for this test case
const templateWithWrongFieldTypes: XOTemplate = { ...p2pkhTemplate, version: 42, name: 42 };
let thrownError: unknown;
try {
parseTemplate(templateWithWrongFieldTypes);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage = 'Template invalid: \n' + '- name: expected string, received number\n' + '- version: expected string, received number';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
/**
* Tests that parseTemplate throws TemplateInvalidError with the unknown key path for an unrecognized property at the top level
*/
const testParseTemplateThrowsOnUnknownProperties = (): void => {
const templateWithUnknownProperty = { ...p2pkhTemplate, unknownProperty: 'unexpected' };
let thrownError: unknown;
try {
parseTemplate(templateWithUnknownProperty);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage = 'Template invalid: \n' + '- (root): Unrecognized key: "unknownProperty"';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
/**
* Tests that parseTemplate throws TemplateInvalidError reporting both unrecognized keys for unknown properties nested inside an action definition
*/
const testParseTemplateThrowsOnDeepUnknownProperties = (): void => {
if (p2pkhTemplate.actions.receive === undefined) {
throw new Error('p2pkhTemplate has no "receive" action, test fixture is invalid');
}
const templateWithDeepUnknown = {
...p2pkhTemplate,
actions: {
...p2pkhTemplate.actions,
receive: { ...p2pkhTemplate.actions.receive, new: 42, new2: 'test' },
},
};
let thrownError: unknown;
try {
parseTemplate(templateWithDeepUnknown);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage = 'Template invalid: \n' + '- actions.receive: Unrecognized keys: "new", "new2"';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
const runTests = async (): Promise<void> => {
test('parseTemplate: accepts a valid template', testParseTemplateAcceptsValidTemplate);
test('parseTemplate: accepts native bigint in satoshi fields', testParseTemplateAcceptsBigintSatoshis);
test('parseTemplate: preserves bigint precision beyond Number.MAX_SAFE_INTEGER', testParseTemplatePreservesBigintPrecisionBeyondMaxSafeInteger);
test('parseTemplate: throws TemplateInvalidError on missing required fields', testParseTemplateThrowsOnMissingRequiredFields);
test('parseTemplate: throws TemplateInvalidError on missing scripts field', testParseTemplateThrowsOnMissingScriptsField);
test('parseTemplate: throws TemplateInvalidError on field type violations', testParseTemplateThrowsOnFieldTypeViolations);
test('parseTemplate: throws TemplateInvalidError on unknown top-level property', testParseTemplateThrowsOnUnknownProperties);
test('parseTemplate: throws TemplateInvalidError on deep unknown properties', testParseTemplateThrowsOnDeepUnknownProperties);
};
await runTests();

48
test/script.test.ts Normal file
View File

@@ -0,0 +1,48 @@
import { expect, test } from 'vitest';
import { isHex } from '@bitauth/libauth';
import { scriptToScriptHash } from '../source/index.ts';
/**
* Tests that scriptToScriptHash produces the correct reversed SHA256 for a standard P2PKH locking script
* and returns a 64 character hex string.
*/
const testScriptHashForP2pkhScript = (): void => {
// Standard P2PKH locking script: OP_DUP OP_HASH160 <20 zero bytes> OP_EQUALVERIFY OP_CHECKSIG
const p2pkhScript = new Uint8Array([ 0x76, 0xa9, 0x14, ...new Uint8Array(20).fill(0), 0x88, 0xac ]);
// Precomputed reversed SHA256 of the above script bytes
const expectedHash = 'acb87996319dca2c2e2afd6c0f7514b18e72e204069718976e1abdc8fcf5de75';
// Generate the script hash
const scriptHash = scriptToScriptHash(p2pkhScript);
// The script hash must match the known-good precomputed value exactly
expect(scriptHash).toBe(expectedHash);
// The script hash is 64 characters long
expect(scriptHash).toHaveLength(64);
// The script hash is a valid hex string
expect(isHex(scriptHash)).toBe(true);
};
/**
* Tests that two different scripts produce different script hashes.
*/
const testScriptHashIsDifferentForDifferentScripts = (): void => {
// Generate the first script hash
const hashA = scriptToScriptHash(new Uint8Array([ 0x01 ]));
// Generate the second script hash
const hashB = scriptToScriptHash(new Uint8Array([ 0x02 ]));
// The different scripts produce different script hashes
expect(hashA).not.toBe(hashB);
};
const runTests = async (): Promise<void> => {
test('scriptToScriptHash: produces reversed SHA256 for a P2PKH locking script', testScriptHashForP2pkhScript);
test('scriptToScriptHash: produces different hashes for different scripts', testScriptHashIsDifferentForDifferentScripts);
};
await runTests();

72
test/templates.test.ts Normal file
View File

@@ -0,0 +1,72 @@
import { expect, test } from 'vitest';
import { isHex } from '@bitauth/libauth';
import { p2pkhTemplate } from '@xo-cash/templates';
import { generateTemplateIdentifier, parseTemplate } from '../source/index.ts';
/**
* Tests that generateTemplateIdentifier returns a valid hex string of 64 characters.
*/
const testGenerateTemplateIdentifierOutputFormat = (): void => {
// Parse the template
const parsedTemplate = parseTemplate(p2pkhTemplate);
// Generate the template identifier
const identifier = generateTemplateIdentifier(parsedTemplate);
// The identifier is 64 characters long
expect(identifier).toHaveLength(64);
// The identifier is a valid hex string
expect(isHex(identifier)).toBe(true);
};
/**
* Tests that generateTemplateIdentifier is deterministic.
*/
const testGenerateTemplateIdentifierIsDeterministic = (): void => {
// Parse the template
const parsedTemplate = parseTemplate(p2pkhTemplate);
// Generate the template identifier once
const firstTemplateIdentifier = generateTemplateIdentifier(parsedTemplate);
// Generate the template identifier again
const secondTemplateIdentifier = generateTemplateIdentifier(parsedTemplate);
// The same template produces the same identifier
expect(firstTemplateIdentifier).toBe(secondTemplateIdentifier);
};
/**
* Tests that two templates with different content produce different identifiers.
*/
const testGenerateTemplateIdentifierDiffersForDifferentTemplates = (): void => {
// Parse the original template
const parsedTemplate = parseTemplate(p2pkhTemplate);
// Generate the original template identifier
const originalTemplateIdentifier = generateTemplateIdentifier(parsedTemplate);
// Create a modified template with a different name
const modifiedTemplate = { ...p2pkhTemplate, name: `${p2pkhTemplate.name} (modified)` };
// Parse the modified template
const parsedModifiedTemplate = parseTemplate(modifiedTemplate);
// Generate the modified template identifier
const modifiedTemplateIdentifier = generateTemplateIdentifier(parsedModifiedTemplate);
// The original and modified templates produce different identifiers
expect(originalTemplateIdentifier).not.toBe(modifiedTemplateIdentifier);
};
const runTests = async (): Promise<void> => {
test('generateTemplateIdentifier: returns a 64-character valid hex string', testGenerateTemplateIdentifierOutputFormat);
test('generateTemplateIdentifier: is deterministic', testGenerateTemplateIdentifierIsDeterministic);
test(
'generateTemplateIdentifier: produces different identifiers for different templates',
testGenerateTemplateIdentifierDiffersForDifferentTemplates,
);
};
await runTests();