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; /** * 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} 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 => { const { identifiers, templateVariables, variables } = parameters; // Without template metadata there is no hint to select a primitive class. if (templateVariables === undefined) { return {}; } const resolvedBytes: Record = {}; 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; };