Refactor, Added documentation, moved from avj-cli to avj, update script and added tests
This commit is contained in:
90
test/extended-json.test.ts
Normal file
90
test/extended-json.test.ts
Normal 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
234
test/parse-template.test.ts
Normal 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
48
test/script.test.ts
Normal 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
72
test/templates.test.ts
Normal 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();
|
||||
Reference in New Issue
Block a user