222 lines
8.9 KiB
TypeScript
222 lines
8.9 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';
|
|
|
|
/**
|
|
* 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 collected CashAssembly variable names.
|
|
*/
|
|
export type ResolvePrimitiveMethodBytesParameters = {
|
|
|
|
/**
|
|
* Variable names from {@link collectVariablesUsingParseScript}, for example
|
|
* `['amount.toSatoshis', 'fee.toSatoshis']`.
|
|
*/
|
|
variableNames: string[];
|
|
|
|
/**
|
|
* Variable names and values object.
|
|
*/
|
|
variables: Record<string, unknown>;
|
|
|
|
/**
|
|
* 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<string, Uint8Array>} 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<string, Uint8Array> => {
|
|
const { variableNames, templateVariables, variables } = parameters;
|
|
|
|
// Without template metadata there is no type to select a primitive class.
|
|
if (templateVariables === undefined) {
|
|
return {};
|
|
}
|
|
|
|
const resolvedBytes: Record<string, Uint8Array> = {};
|
|
|
|
for (const variableName of variableNames) {
|
|
// The same name can appear more than once. Resolve it only once.
|
|
if (Object.hasOwn(resolvedBytes, variableName) === true) {
|
|
continue;
|
|
}
|
|
|
|
// Find the primitive method reference in the name.
|
|
const methodReferenceMatch = variableName.match(CASHASSEMBLY_VARIABLE_METHOD_REFERENCE_PATTERN);
|
|
|
|
if (methodReferenceMatch === null) {
|
|
continue;
|
|
}
|
|
|
|
const [ , baseName, methodName ] = methodReferenceMatch;
|
|
|
|
// Unknown names 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(variableName, 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(variableName, baseName);
|
|
}
|
|
|
|
const methodResult = callPrimitiveMethod({
|
|
identifier: variableName,
|
|
methodName,
|
|
value: variables[baseName],
|
|
type,
|
|
});
|
|
|
|
// CashAssembly looks up the full `base.method` name as WalletData bytes.
|
|
resolvedBytes[variableName] = convertValueToBytes(methodResult, variableName);
|
|
}
|
|
|
|
return resolvedBytes;
|
|
};
|