Files
xo-cash-utils/source/cash-assembly/primitive-evaluations.ts
T

217 lines
8.4 KiB
TypeScript

import {
FungibleTokenAmount,
NFTCommitment,
PublicKey,
Satoshis,
SchnorrSignature,
TemplateIdentifier,
Timestamp,
TokenCategory,
TransactionHash,
} from '@xo-cash/primitives';
import { XOTemplatePrimitiveTypes } from '@xo-cash/types';
import type { XOTemplate, XOTemplatePrimitiveType } from '@xo-cash/types';
import { CashAssemblyPrimitiveMethodMissingError, CashAssemblyPrimitiveVariableMissingError } from './errors.ts';
import { CASHASSEMBLY_VARIABLE_METHOD_REFERENCE_PATTERN } from './defaults.ts';
import { convertValueToBytes } from './bytes.ts';
/**
* Template hint values mapped to a primitive class for resolving primitive method evaluations.
* Keys are values from XOTemplatePrimitiveTypes.
*/
const PRIMITIVE_BY_TEMPLATE_HINT = {
[XOTemplatePrimitiveTypes.FUNGIBLE_TOKEN_AMOUNT]: FungibleTokenAmount,
[XOTemplatePrimitiveTypes.NFT_COMMITMENT]: NFTCommitment,
[XOTemplatePrimitiveTypes.PUBLIC_KEY]: PublicKey,
[XOTemplatePrimitiveTypes.SATOSHIS]: Satoshis,
[XOTemplatePrimitiveTypes.SCHNORR_SIGNATURE]: SchnorrSignature,
[XOTemplatePrimitiveTypes.TEMPLATE_IDENTIFIER]: TemplateIdentifier,
[XOTemplatePrimitiveTypes.TIMESTAMP]: Timestamp,
[XOTemplatePrimitiveTypes.TOKEN_CATEGORY]: TokenCategory,
[XOTemplatePrimitiveTypes.TRANSACTION_HASH]: TransactionHash,
} as const;
type SupportedPrimitiveHint = keyof typeof PRIMITIVE_BY_TEMPLATE_HINT;
/**
* Inputs needed to call a primitive method for a `name.method` push.
*/
type CallPrimitiveMethodParameters = {
/**
* Full push identifier from the evaluation, for example `amount.toSatoshis`.
*/
identifier: string;
/**
* Method name to call on the constructed primitive, for example `toIso8601`.
*/
methodName: string;
/**
* Value for the variable.
*/
value: unknown;
/**
* Supported template hint that selects the primitive class.
*/
hint: SupportedPrimitiveHint;
};
/**
* Inputs needed to resolve primitive method pushes from extracted CashAssembly identifiers.
*/
export type ResolvePrimitiveMethodBytesParameters = {
/**
* Variable identifiers from {@link extractVariablesFromEvaluations}, for example
* `['amount.toSatoshis', 'fee.toSatoshis']`.
*/
identifiers: string[];
/**
* Variable names and values object.
*/
variables: Record<string, unknown>;
/**
* Template variable definitions. When omitted, no primitive methods are resolved.
* The `hint` on each entry selects the primitive class.
*/
templateVariables?: XOTemplate['variables'];
};
/**
* Returns true when `hint` maps to a supported primitive class in `PRIMITIVE_BY_TEMPLATE_HINT`.
*
* @param {XOTemplatePrimitiveType | undefined} hint - Template variable hint.
* @returns {boolean} True when the hint selects a supported primitive class.
*/
const isSupportedPrimitiveHint = (hint: XOTemplatePrimitiveType | undefined): hint is SupportedPrimitiveHint => {
return hint !== undefined && Object.hasOwn(PRIMITIVE_BY_TEMPLATE_HINT, hint) === true;
};
/**
* Returns true when `methodName` is an own function on the primitive class for `hint`.
*
* @param {SupportedPrimitiveHint} hint - Supported template hint.
* @param {string} methodName - Method name from the evaluation text, for example `toSatoshis`.
* @returns {boolean} True when that class exposes the named method.
*/
const canResolvePrimitiveMethod = (hint: SupportedPrimitiveHint, methodName: string): boolean => {
const PrimitiveClass = PRIMITIVE_BY_TEMPLATE_HINT[hint];
// Check own properties only so inherited Object.prototype names are rejected without needing a value.
if (Object.hasOwn(PrimitiveClass.prototype, methodName) === false) {
return false;
}
return typeof Reflect.get(PrimitiveClass.prototype, methodName) === 'function';
};
/**
* Constructs a primitive from a raw value and calls one instance method on it.
*
* Call only after `canResolvePrimitiveMethod` is true for the same hint and method.
*
* @param {CallPrimitiveMethodParameters} parameters - Identifier, method, value, and supported hint.
* @returns {unknown} Method return value, later encoded as CashAssembly push bytes.
* @throws {@link CashAssemblyPrimitiveMethodMissingError} When the prototype member is not a function.
* @throws When the primitive constructor rejects the raw value (validation errors from `@xo-cash/primitives`).
*/
const callPrimitiveMethod = (parameters: CallPrimitiveMethodParameters): unknown => {
const { identifier, methodName, value, hint } = parameters;
const PrimitiveClass = PRIMITIVE_BY_TEMPLATE_HINT[hint];
// Constructing runs each primitive's own input validation (range checks, hex length, etc).
// `as never` satisfies TypeScript across constructors that accept different input shapes.
const primitiveInstance = new PrimitiveClass(value as never);
// Same prototype member canResolvePrimitiveMethod already verified as an own function.
const primitiveMethod = Reflect.get(PrimitiveClass.prototype, methodName);
if (typeof primitiveMethod !== 'function') {
throw new CashAssemblyPrimitiveMethodMissingError(identifier, methodName, hint);
}
return primitiveMethod.call(primitiveInstance);
};
/**
* Resolves supported `base.method` identifiers to CashAssembly variable bytes.
*
* Each single dot identifier whose `hint` maps to a supported primitive is resolved and stored under
* the full identifier (`base.method`). Unsupported or multi dot identifiers are left for CashAssembly.
*
* When `templateVariables` is omitted, returns an empty map.
*
* @param {ResolvePrimitiveMethodBytesParameters} parameters - Identifiers, values, and optional template metadata.
* @returns {Record<string, Uint8Array>} Resolved method identifiers mapped to bytes for CashAssembly pushes.
* @throws {@link CashAssemblyPrimitiveMethodMissingError} When the hint is a supported primitive but the method is missing.
* @throws {@link CashAssemblyPrimitiveVariableMissingError} When the runtime value is missing from the variables map.
* @throws {@link CashAssemblyUnsupportedValueTypeError} When the method return type cannot be embedded.
* @throws {@link CashAssemblyNumberNotSafeIntegerError} When a method return value is a number that is not a safe integer.
*/
export const resolvePrimitiveMethodBytes = (parameters: ResolvePrimitiveMethodBytesParameters): Record<string, Uint8Array> => {
const { identifiers, templateVariables, variables } = parameters;
// Without template metadata there is no hint to select a primitive class.
if (templateVariables === undefined) {
return {};
}
const resolvedBytes: Record<string, Uint8Array> = {};
for (const identifier of identifiers) {
// The same identifier can appear more than once. Resolve it only once.
if (Object.hasOwn(resolvedBytes, identifier) === true) {
continue;
}
// Find the primitive method reference in the identifier.
const methodReferenceMatch = identifier.match(CASHASSEMBLY_VARIABLE_METHOD_REFERENCE_PATTERN);
if (methodReferenceMatch === null) {
continue;
}
const [ , baseName, methodName ] = methodReferenceMatch;
// Unknown identifiers and CashAssembly native operations such as someKey.schnorr_signature.all_outputs
// must be left for CashAssembly rather than treated as primitive failures.
if (Object.hasOwn(templateVariables, baseName) === false) {
continue;
}
const hint = templateVariables[baseName].hint;
if (isSupportedPrimitiveHint(hint) === false) {
continue;
}
// Supported hint with an unknown method should be thrown as an error.
if (canResolvePrimitiveMethod(hint, methodName) === false) {
throw new CashAssemblyPrimitiveMethodMissingError(identifier, methodName, hint);
}
// If the method is known but the runtime value is missing, throw an error.
if (Object.hasOwn(variables, baseName) === false) {
throw new CashAssemblyPrimitiveVariableMissingError(identifier, baseName);
}
const methodResult = callPrimitiveMethod({
identifier,
methodName,
value: variables[baseName],
hint,
});
// Store the result under identifier.
resolvedBytes[identifier] = convertValueToBytes(methodResult, identifier);
}
return resolvedBytes;
};