/* eslint-disable @stylistic/newline-per-chained-call */ import { z } from 'zod'; // ============================================================ // Enums // ============================================================ /** * Validation schema for a BCH VM version identifier. Defines the set of known BCH VM versions * that XO templates declare support for. * * Zod's `z.enum` requires the exact values to be defined inline because it needs to know each * specific value at compile time to validate against them. Defining the versions here directly * satisfies that requirement and allows `z.array(bchVmVersionSchema)` to be used elsewhere * * ``` * { * "supported": [ "BCH_2025_05" ] ← each value * } * ``` */ export const bchVmVersionSchema = z.enum([ 'BCH_2020_05', 'BCH_2021_05', 'BCH_2022_05', 'BCH_2023_05', 'BCH_2024_05', 'BCH_2025_05', 'BCH_2026_05' ]); /** * Validation schema for the capability of a non-fungible token. Defines the three capability * types supported on BCH: minting tokens can create new NFTs, mutable tokens can update their * commitment, and none tokens cannot be changed after creation. * * ``` * { * "inputs|outputs": { * "[id]": { * "token": { * "nft": { * "capability": "minting" ← this schema * } * } * } * } * } * ``` */ export const xoTemplateNftCapabilitySchema = z.enum([ 'minting', 'mutable', 'none' ]); /** * Validation schema for a BCH locking script type. Defines the standard locking script types * supported on BCH. * * ``` * { * "lockingScripts": { * "[id]": { * "lockingType": "p2pkh" ← this schema * } * } * } * ``` */ export const xoTemplateLockingTypeSchema = z.enum([ 'p2s', 'p2pkh', 'p2sh' ]); /** * Validation schema for a primitive type identifier. Defines the set of primitive types * that can be declared in an XO template. Used by constants, variables, and data fields. */ export const xoTemplatePrimitiveTypeSchema = z.enum([ 'boolean', 'bytes', 'integer', 'bigint', 'string', 'private_key', 'public_key' ]); // ============================================================ // Primitives // ============================================================ /** * Validation schema for byte array fields i.e. Uint8Array instance. */ export const uint8ArraySchema = z.instanceof(Uint8Array).describe('A sequence of unsigned 8-bit integers expressed as a Uint8Array.'); /** * Validation schema for the Satoshis type i.e. bigint. */ export const satoshisSchema = z.bigint().describe('A satoshi amount expressed as a bigint.'); // ============================================================ // Shared // ============================================================ /** Maximum character length for name fields on view properties. */ export const VIEW_PROPERTIES_NAME_MAX_LENGTH = 200; /** Maximum character length for description fields on view properties. */ export const VIEW_PROPERTIES_DESCRIPTION_MAX_LENGTH = 5000; /** Maximum character length for icon fields on view properties. */ export const VIEW_PROPERTIES_ICON_MAX_LENGTH = 50; /** * Validation schema for view properties shared across many template elements i.e. name, description, icon. * Extended by most other schemas in this file. */ export const xoTemplateViewPropertiesSchema = z .object({ name: z.string().max(VIEW_PROPERTIES_NAME_MAX_LENGTH).describe('A short human-readable label for this element.'), description: z .string() .max(VIEW_PROPERTIES_DESCRIPTION_MAX_LENGTH) .describe('A human-readable explanation of what this element does and when it is relevant.'), icon: z.string().max(VIEW_PROPERTIES_ICON_MAX_LENGTH).optional().describe('An optional icon identifier or URL for this element.'), }) .strict(); // ============================================================ // Intents // ============================================================ /** * Validation schema for the base intent structure. Describes the common data parameters shared * by all intent types regardless of what they target. * * An optional templateIdentifier allows the intent to reference a target defined in a different * template, enabling cross-template interaction. * * Extended by: xoTemplateActionIntentSchema, xoTemplateOutputIntentSchema, * xoTemplateLockingScriptIntentSchema. */ export const xoTemplateIntentSchema = z .object({ templateIdentifier: z .string() .optional() .describe('Optional identifier for the template used in this intent. If not provided, uses the current template.'), role: z.string().optional().describe('Optional identifier for the role used in this intent.'), generate: z.array(z.string()).optional().describe('Identifiers for items to generate when this intent is resolved, e.g. keys or secrets.'), variables: z.array(z.record(z.string(), z.unknown())).optional().describe('Variable values to apply when this intent is resolved.'), constants: z.array(z.record(z.string(), z.unknown())).optional().describe('Constant values to apply when this intent is resolved.'), secrets: z.array(z.record(z.string(), z.unknown())).optional().describe('Secret values to apply when this intent is resolved.'), }) .strict(); /** * Validation schema for an action intent. Extends the base intent structure with an action * identifier. Used in locking script action lists and in the template's start array. * * ``` * { * "start": [ * { "action": "..." } ← this schema * ], * "lockingScripts": { * "[id]": { * "actions": [ * { "action": "..." } ← this schema * ], * "roles": { * "[roleId]": { * "actions": [ * { "action": "..." } ← this schema * ] * } * } * } * } * } * ``` */ export const xoTemplateActionIntentSchema = xoTemplateIntentSchema .extend({ action: z.string().describe('The identifier for the intended action.'), }) .strict(); /** * Validation schema for an output intent. Extends the base intent structure with an output * identifier. Used in the template's defaults block. * * ``` * { * "defaults": { * "change": { "output": "..." } ← this schema * } * } * ``` */ export const xoTemplateOutputIntentSchema = xoTemplateIntentSchema .extend({ output: z.string().describe('The identifier for the intended output.'), }) .strict(); /** * Validation schema for a locking script intent. Extends the base intent structure with * a locking script identifier. * * @todo The location of this schema in the template JSON is not yet determined. */ export const xoTemplateLockingScriptIntentSchema = xoTemplateIntentSchema .extend({ lockingScript: z.string().describe('The identifier for the intended locking script.'), }) .strict(); // ============================================================ // Actions // ============================================================ /** * Validation schema for the slot count configuration on a role requirement. Declares how many * participants of a given role are needed. min sets the lower bound and max sets the upper bound. * When max is absent, there is no upper limit. * * ``` * { * "actions": { * "[id]": { * "requirements": { * "participants": [ * { "slots": { "min": 1, "max": 1 } } ← this schema * ] * } * } * } * } * ``` */ export const xoTemplateRoleSlotsRequirementsSchema = z .object({ min: z.number().describe('Minimum number of participants required for this role.'), max: z.number().optional().describe('Maximum number of participants allowed for this role. Undefined means unlimited.'), }) .strict(); /** * Validation schema for the capability requirements declared on a role within an action. * Describes what data, secrets, or state the role is responsible for providing when participating in an action. * * ``` * { * "actions": { * "[id]": { * "roles": { * "[roleId]": { * "requirements": { "variables": [], "secrets": [] } ← this schema * } * } * } * } * } * ``` */ export const xoTemplateActionRoleRequirementsSchema = z .object({ variables: z.array(z.string()).optional().describe('List of variable identifiers required for this role.'), secrets: z.array(z.string()).optional().describe('List of secret identifiers required for this role.'), }) .strict(); /** * Validation schema for a role-specific definition within an action. * All view properties are optional. * * ``` * { * "actions": { * "[id]": { * "roles": { * "[roleId]": { } ← this schema * } * } * } * } * ``` */ export const xoTemplateActionRoleSchema = xoTemplateViewPropertiesSchema .partial() .extend({ generate: z .array(z.string()) .optional() .describe('Identifiers for data items that should be generated for this role when participating in the action.'), // Describes under what conditions this role can proceed with the action. All values listed // under requirements must be populated for the action to work. This is a developer and // author concern. It is not present on intents because intents are used to populate the // action rather than to define it, and their fields are flattened accordingly. requirements: xoTemplateActionRoleRequirementsSchema.optional().describe('The requirements for this role within this action.'), }) .strict(); /** * Validation schema for a role participation requirement in an action's requirements block. * * ``` * { * "actions": { * "[id]": { * "requirements": { * "participants": [ * { "role": "...", "slots": { } } ← this schema * ] * } * } * } * } * ``` */ export const xoTemplateRoleSlotSchema = z .object({ role: z.string().describe('The role identifier that this requirement applies to.'), slots: xoTemplateRoleSlotsRequirementsSchema.describe('Slot configuration specifying how many participants of this role are required.'), }) .strict(); /** * Validation schema for the requirements of an action. * * ``` * { * "actions": { * "[id]": { * "requirements": { "participants": [], "secrets": [] } ← this schema * } * } * } * ``` */ export const xoTemplateActionRequirementsSchema = z .object({ participants: z.array(xoTemplateRoleSlotSchema).optional().describe('The participants required for this action.'), secrets: z.array(z.string()).optional().describe('The secrets required for this action.'), }) .strict(); /** * Validation schema for an action definition. * * ``` * { * "actions": { * "[id]": { } ← this schema * } * } * ``` */ export const xoTemplateActionSchema = xoTemplateViewPropertiesSchema .extend({ roles: z.record(z.string(), xoTemplateActionRoleSchema).optional().describe('Specific context for each role participating in this action.'), requirements: xoTemplateActionRequirementsSchema.optional().describe('The requirements for this action.'), // This is a list of conditions that can influence how the action behaves. // This needs more work to be done. conditions: z.array(z.string()).optional().describe('Conditions that must be met for this action to be available.'), // A single transaction produced by the action. // In future this might be moved to a results block that can have multiple transactions. transaction: z .string() .optional() .describe("The identifier of the transaction this action produces, referencing an entry in the template's transactions."), // The data that is produced by the action. // In future this might be moved to a results block that can have multiple data fields. data: z.string().optional().describe("The identifier of the data field this action produces, referencing an entry in the template's data."), }) .strict(); // ============================================================ // Tokens & Amounts // ============================================================ /** * Validation schema for the non-fungible token configuration within a token field. * * ``` * { * "inputs|outputs": { * "[id]": { * "token": { * "nft": { } ← this schema * } * } * } * } * ``` */ export const xoTemplateNonFungibleTokenDetailsSchema = z .object({ capability: z .union([ xoTemplateNftCapabilitySchema, z.string() ]) .optional() .describe('The capability of the NFT. May be a known capability value or a CashASM expression resolving to a capability.'), commitment: z.string().optional().describe('The commitment data for the NFT, as a string or CashASM expression resolving to a commitment.'), }) .strict(); /** * Validation schema for the token configuration on inputs and outputs. * * ``` * { * "inputs|outputs": { * "[id]": { * "token": { } ← this schema * } * } * } * ``` */ export const xoTemplateTokenSchema = z .object({ category: z.string().optional().describe('The category of the token, as a string or CashASM expression resolving to a category.'), amount: z .union([ z.bigint(), z.string(), z.null() ]) .optional() .describe('The amount of fungible tokens as a bigint, a CashASM expression resolving to a bigint, or null indicating no FT is present.'), nft: xoTemplateNonFungibleTokenDetailsSchema .nullable() .optional() .describe('Non-fungible token configuration. Null indicates no NFT is present.'), }) .strict(); /** * Validation schema for the asset amounts configuration. Used by omitChangeAmounts on inputs * and by balance on locking scripts, outputs, and their roles. */ export const xoTemplateAssetAmountsSchema = z .object({ /** * The satoshi amount. * - `Satoshis`: A specific bigint amount. * - `string`: A CashASM expression that resolves to the amount. * - `true`: all, i.e. the entire amount */ satoshis: z .union([ satoshisSchema, z.string(), z.literal(true) ]) .optional() .describe('The satoshi amount. Accepts a bigint for a specific value, a CashASM expression, or true for the entire amount.'), /** * The fungible token amount. * - `FungibleTokenAmount`: A specific bigint amount. * - `string`: A CashASM expression that resolves to the amount. * - `true`: all, i.e. the entire amount */ fungibleTokens: z .union([ z.bigint(), z.string(), z.literal(true) ]) .optional() .describe('The fungible token amount. Accepts a bigint for a specific value, a CashASM expression, or true for the entire amount.'), /** * Whether a non-fungible token is present (0 for absent, 1 for present), * or a CashASM expression that evaluates to 0 or 1. * - `true`: resolves to 1 if an NFT is present, or 0 if none is present. Use this when * the NFT is optional, to express that the NFT is estimated to be part of the balance * if present, or absent from it if not. * - `0`: None, i.e. nothing is expected to be included * - `1`: present and complete, as value > 1 does not make sense for non-fungible tokens * - `string`: A CashASM expression that evaluates to 0 or 1. */ nonfungibleTokens: z .union([ z.literal(0), z.literal(1), z.string(), z.literal(true) ]) .optional() .describe('Whether an NFT is present: 0 for absent, 1 for present, a CashASM expression evaluating to 0 or 1, or true to estimate the NFT as part of the balance if present, or absent from it if not.'), }) .strict(); // ============================================================ // Locking Scripts // ============================================================ /** * Validation schema for the state configuration shared by a locking script and its individual roles. * Declares which variables and secrets are tracked in the on-chain state for a given participant. * * ``` * { * "lockingScripts": { * "[id]": { * "state": { "variables": [], "secrets": [] } ← this schema * "roles": { * "[roleId]": { * "state": { "variables": [], "secrets": [] } ← this schema * } * } * } * } * } * ``` */ export const xoTemplateStateSchema = z .object({ variables: z.array(z.string()).optional().describe('List of variable identifiers to track in state.'), secrets: z.array(z.string()).optional().describe('List of secret identifiers to track in state.'), }) .strict(); /** * Validation schema for a role definition for a locking script. * * ``` * { * "lockingScripts": { * "[id]": { * "roles": { * "[roleId]": { } ← this schema * } * } * } * } * ``` */ export const xoTemplateLockingScriptRoleSchema = xoTemplateViewPropertiesSchema .partial() .extend({ state: xoTemplateStateSchema.optional().describe('List of items to track as state for this role.'), actions: z.array(xoTemplateActionIntentSchema).optional().describe('List of action references available to this role.'), balance: xoTemplateAssetAmountsSchema.partial().optional().describe('Estimated ownership in the optional set of asset amounts specified.'), selectable: z.boolean().optional().describe('Whether outputs locked to this script should be available for coin selection.'), privacy: z .union([ z.number(), z.string() ]) .optional() .describe('Privacy level for outputs locked to this script. A numeric level or a CashASM expression that evaluates to a privacy level.'), }) .strict(); /** * Validation schema for a locking script definition. * * ``` * { * "lockingScripts": { * "[id]": { } ← this schema * } * } * ``` */ export const xoTemplateLockingScriptSchema = xoTemplateViewPropertiesSchema .extend({ lockingType: xoTemplateLockingTypeSchema.optional().describe('The type of locking mechanism. Defaults to p2s if not specified.'), lockingBytecode: z.string().describe('The locking script bytecode.'), unlockingBytecode: z.string().optional().describe('Optional default unlocking bytecode when used in automatic coin selection.'), actions: z.array(xoTemplateActionIntentSchema).optional().describe('The actions available for this locking script.'), state: xoTemplateStateSchema.optional().describe('List of items to track as state for all participants.'), balance: xoTemplateAssetAmountsSchema.partial().optional().describe('Estimated ownership in the optional set of asset amounts specified.'), selectable: z.boolean().optional().describe('Whether outputs locked to this script should be available for coin selection.'), // Might be levels or tags privacy: z .union([ z.number(), z.string() ]) .optional() .describe('Privacy level for outputs locked to this script. A numeric level or a CashASM expression that evaluates to a privacy level.'), roles: z .record(z.string(), xoTemplateLockingScriptRoleSchema) .optional() .describe('Specific context for each role participating in this locking script.'), }) .strict(); // ============================================================ // Inputs // ============================================================ /** * Validation schema for an input definition in the template. Extends view properties with optional * satoshi value, token configuration, and other transaction level fields. * * ``` * { * "inputs": { * "[id]": { } ← this schema * } * } * ``` */ export const xoTemplateInputSchema = xoTemplateViewPropertiesSchema .extend({ valueSatoshis: z .union([ satoshisSchema, z.string() ]) .optional() .describe('The amount of satoshis for this input as a bigint or a CashASM expression resolving to the amount.'), token: xoTemplateTokenSchema.nullable().optional().describe('Token configuration for this input.'), sequenceNumber: z .union([ z.number(), z.string() ]) .optional() .describe('The sequence number of this input as a specific number or a CashASM expression.'), unlockingScript: z.string().optional().describe('Identifier of the unlocking script to use for the UTXO provided for this input.'), omitChangeAmounts: xoTemplateAssetAmountsSchema .optional() .describe('Amount of change that should be omitted from the automatic change handling. WARNING: Setting this can result in loss of funds!'), }) .strict(); // ============================================================ // Outputs // ============================================================ /** * Validation schema for an output definition. Extends the locking script schema so that * every output inherits the same locking script fields and adds output-specific fields. * * ``` * { * "outputs": { * "[id]": { } ← this schema * } * } * ``` */ export const xoTemplateOutputSchema = xoTemplateLockingScriptSchema .omit({ lockingType: true, lockingBytecode: true, unlockingBytecode: true }) .extend({ lockingScript: z.string().describe('Identifier of the locking script to use for this output.'), valueSatoshis: z .union([ satoshisSchema, z.string() ]) .optional() .describe('The amount of satoshis for this output as a bigint or a CashASM expression resolving to the amount.'), token: xoTemplateTokenSchema.nullable().optional().describe('Token configuration for this output.'), }) .strict(); // ============================================================ // Transactions // ============================================================ /** * Validation schema for a transaction input reference for a transaction definition. * * ``` * { * "transactions": { * "[id]": { * "inputs": [ * { "input": "..." } ← this schema * ] * } * } * } * ``` */ export const xoTemplateTransactionInputSchema = z .object({ input: z.string().describe('The input definition identifier.'), inputIndex: z.number().optional().describe('Optional index of this input in the transaction.'), }) .strict(); /** * Validation schema for a transaction output reference for a transaction definition. * * ``` * { * "transactions": { * "[id]": { * "outputs": [ * { "output": "..." } ← this schema * ] * } * } * } * ``` */ export const xoTemplateTransactionOutputSchema = z .object({ output: z.string().describe('The output definition identifier.'), outputIndex: z.number().optional().describe('Optional index of this output in the transaction.'), }) .strict(); /** * Validation schema for role-specific data for a transaction definition. * * ``` * { * "transactions": { * "[id]": { * "roles": { * "[roleId]": { } ← this schema * } * } * } * } * ``` */ export const xoTemplateTransactionRoleDataSchema = xoTemplateViewPropertiesSchema .partial() .extend({ inputs: z.array(xoTemplateTransactionInputSchema).optional().describe('The inputs required for this role.'), outputs: z.array(xoTemplateTransactionOutputSchema).optional().describe('The outputs required for this role.'), }) .strict(); /** * Validation schema for a transaction template definition. * * ``` * { * "transactions": { * "[id]": { } ← this schema * } * } * ``` */ export const xoTemplateTransactionSchema = xoTemplateViewPropertiesSchema .extend({ version: z.number().optional().describe('The version of the transaction.'), locktime: z.number().optional().describe('The locktime for this transaction.'), inputs: z.array(xoTemplateTransactionInputSchema).describe('The inputs for this transaction.'), outputs: z.array(xoTemplateTransactionOutputSchema).describe('The outputs for this transaction.'), roles: z .record(z.string(), xoTemplateTransactionRoleDataSchema) .optional() .describe('Specific context for each role participating in this transaction.'), composable: z.boolean().optional().describe('Whether this transaction can be composed with other transactions.'), }) .strict(); // ============================================================ // Template Data // ============================================================ /** * Validation schema for a constant value definition. * * ``` * { * "constants": { * "[id]": { } ← this schema * } * } * ``` */ export const xoTemplateConstantSchema = xoTemplateViewPropertiesSchema .extend({ type: xoTemplatePrimitiveTypeSchema.describe('The data type of this constant.'), value: z.unknown().describe('The value of this constant.'), hint: z.string().optional().describe('An optional hint to help apps and users understand what this constant represents.'), }) .strict(); /** * Validation schema for a data field definition. * * ``` * { * "data": { * "[id]": { } ← this schema * } * } * ``` */ export const xoTemplateDataSchema = z .object({ type: xoTemplatePrimitiveTypeSchema.describe('The data type of this data field.'), value: z.unknown().describe('The value for this data field.'), hint: z.string().optional().describe('An optional hint to help apps and users understand this data field.'), }) .strict(); /** * Validation schema for an import default value intent. Extends the base intent with optional * view properties that the engine evaluates at runtime to produce human-readable output. * * ``` * { * "variables": { * "[id]": { * "importDefaultValue": { } ← this schema * } * } * } * ``` */ export const xoTemplateImportDefaultValueSchema = xoTemplateIntentSchema // .shape unwraps the ZodObject to the raw field map { name, description, icon } with each field made optional .extend(xoTemplateViewPropertiesSchema.partial().shape) .strict(); /** * Validation schema for a variable definition. * * ``` * { * "variables": { * "[id]": { } ← this schema * } * } * ``` */ export const xoTemplateVariableSchema = xoTemplateViewPropertiesSchema .extend({ type: xoTemplatePrimitiveTypeSchema.optional().describe('The data type of this variable.'), hint: z.string().optional().describe('A hint to help users understand what value to provide.'), // A neutral intent that the engine uses to populate the default value for this variable. // View properties (name, description, icon) may contain CashASM expressions that the // engine evaluates at runtime to produce human-readable output. The engine overrides // whatever values are set here when resolving the variable for a participant. importDefaultValue: xoTemplateImportDefaultValueSchema .optional() .describe('A neutral intent that the engine uses to populate the default value for this variable.'), }) .strict(); // ============================================================ // Template Resources // ============================================================ /** * Validation schema for a resource reference attached to a template element. Extends view * properties with a URL pointing to external documentation or tooling. * * ``` * { * "resources": [ * { "name": "...", "description": "...", "url": "..." } ← this schema * ] * } * ``` */ export const xoTemplateResourceSchema = xoTemplateViewPropertiesSchema .extend({ url: z.string().describe('The URL for this resource.'), }) .strict(); /** * Validation schema for an icon reference. * * ``` * { * "icons": [ * { "name": "...", "hash": "..." } ← this schema * ] * } * ``` */ export const xoTemplateIconSchema = xoTemplateViewPropertiesSchema .pick({ name: true }) .extend({ hash: z.string().describe('The identifier of the icon.'), }) .strict(); // ============================================================ // Defaults // ============================================================ /** * Validation schema for the defaults block of a template. * * ``` * { * "defaults": { } ← this schema * } * ``` */ export const xoTemplateDefaultsSchema = z .object({ change: xoTemplateOutputIntentSchema.optional().describe('Instructions for how to construct automated change output.'), }) .strict(); // ============================================================ // Template // ============================================================ /** * Validation schema for the full XOTemplate type. */ export const xoTemplateSchema = xoTemplateViewPropertiesSchema .extend({ $schema: z .string() .describe('The URI that identifies the JSON Schema used by this template. This enables documentation, autocompletion, and validation in JSON documents.'), version: z.string().optional().describe('A string identifying the version of this template.'), supported: z.array(bchVmVersionSchema).min(1).describe('The BCH VM versions that this template supports. At least one version is required.'), defaults: xoTemplateDefaultsSchema.optional().describe('Optional default settings used in this template.'), roles: z.record(z.string(), xoTemplateViewPropertiesSchema).describe('The roles defined in this template.'), start: z.array(xoTemplateActionIntentSchema).describe('A list of entry points defining which actions are available at the start.'), actions: z.record(z.string(), xoTemplateActionSchema).describe('The actions defined in this template.'), data: z.record(z.string(), xoTemplateDataSchema).optional().describe('The data fields defined in this template.'), transactions: z.record(z.string(), xoTemplateTransactionSchema).optional().describe('The transaction templates defined in this template.'), inputs: z.record(z.string(), xoTemplateInputSchema).describe('The inputs defined in this template.'), outputs: z.record(z.string(), xoTemplateOutputSchema).describe('The outputs defined in this template.'), lockingScripts: z.record(z.string(), xoTemplateLockingScriptSchema).describe('The locking script templates defined in this template.'), scripts: z .record(z.string(), z.string()) .describe('Scripts used in this template. Keys are script identifiers, values are bytecode or CashASM expressions.'), constants: z.record(z.string(), xoTemplateConstantSchema).optional().describe('The constants defined in this template.'), variables: z .record(z.string(), xoTemplateVariableSchema) .optional() .describe("The variables that must be provided for use in the template's scripts."), resources: z.array(xoTemplateResourceSchema).optional().describe('Resource references providing external documentation or tooling links.'), icons: z.array(xoTemplateIconSchema).optional().describe('The icons available for use throughout the template.'), scenarios: z.unknown().optional().describe('The scenarios defined in this template.'), }) .strict();