217 lines
8.4 KiB
TypeScript
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;
|
|
};
|