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'; /** * Maps method resolvable template types to `@xo-cash/primitives` classes for `base.method` resolution. * Keys are a subset of {@link XOTemplatePrimitiveTypes}. */ const RESOLVABLE_PRIMITIVE_CLASS_BY_TYPE = { [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; /** * Template primitive types that map to an `@xo-cash/primitives` class for `base.method` resolution. * This is a subset of {@link XOTemplatePrimitiveType}, not every declared template type. */ type ResolvablePrimitiveType = keyof typeof RESOLVABLE_PRIMITIVE_CLASS_BY_TYPE; /** * 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; /** * Method resolvable template type that selects the primitive class. */ type: ResolvablePrimitiveType; }; /** * 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 `type` on each entry selects the primitive class. */ templateVariables?: XOTemplate['variables']; }; /** * Returns true when `type` maps to a primitive class in `RESOLVABLE_PRIMITIVE_CLASS_BY_TYPE` for method resolution. * * @param {XOTemplatePrimitiveType | undefined} type - Template variable type. * @returns {boolean} True when the type can resolve `base.method` through a primitive class. */ const isResolvablePrimitiveType = (type: XOTemplatePrimitiveType | undefined): type is ResolvablePrimitiveType => { return type !== undefined && Object.hasOwn(RESOLVABLE_PRIMITIVE_CLASS_BY_TYPE, type) === true; }; /** * Returns true when `methodName` is an own function on the primitive class for `type`. * * @param {ResolvablePrimitiveType} type - Method resolvable template type. * @param {string} methodName - Method name from the evaluation text, for example `toSatoshis`. * @returns {boolean} True when that class exposes the named method. */ const canResolvePrimitiveMethod = (type: ResolvablePrimitiveType, methodName: string): boolean => { const PrimitiveClass = RESOLVABLE_PRIMITIVE_CLASS_BY_TYPE[type]; // 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 type and method. * * @param {CallPrimitiveMethodParameters} parameters - Identifier, method, value, and resolvable type. * @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, type } = parameters; const PrimitiveClass = RESOLVABLE_PRIMITIVE_CLASS_BY_TYPE[type]; // 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, type); } return primitiveMethod.call(primitiveInstance); }; /** * Resolves method resolvable `base.method` identifiers to CashAssembly variable bytes. * * Each single dot identifier whose `type` is in `RESOLVABLE_PRIMITIVE_CLASS_BY_TYPE` is resolved and stored under * the full identifier (`base.method`). Other template types 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 type is method resolvable 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 type 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 type = templateVariables[baseName].type; // Template types without a mapped primitive class are left for CashAssembly. if (isResolvablePrimitiveType(type) === false) { continue; } // Method resolvable type with an unknown method should throw. if (canResolvePrimitiveMethod(type, methodName) === false) { throw new CashAssemblyPrimitiveMethodMissingError(identifier, methodName, type); } // 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], type, }); // Store the result under identifier. resolvedBytes[identifier] = convertValueToBytes(methodResult, identifier); } return resolvedBytes; };