Add resolveCommitReferences method

This commit is contained in:
2026-06-08 13:09:38 +02:00
parent c7e1d69e2d
commit 69adee180a
5 changed files with 683 additions and 93 deletions

View File

@@ -0,0 +1,295 @@
/**
* Transforms a raw XO invitation into a flattened, template-enriched structure
* suitable for UI display without manually resolving template references.
*
* The original invitation format is unchanged in storage and transport; this
* function produces a read model that merges commit data with template metadata
* (names, descriptions, icons, roles, etc.).
*/
import type {
XOInvitation,
XOInvitationInput,
XOInvitationOutput,
XOInvitationVariable,
XOInvitationVariableValue,
XOTemplate,
XOTemplateInput,
XOTemplateOutput,
XOTemplateVariable,
} from "@xo-cash/types";
/**
* View metadata copied from a template definition onto a resolved invitation item.
*/
interface TemplateViewMetadata {
name?: string;
description?: string;
icon?: string;
}
/**
* Role-specific view metadata from a template output definition.
*/
export interface ResolvedInvitationOutputRoleMetadata {
name?: string;
description?: string;
icon?: string;
}
/**
* A variable from invitation commits enriched with its template definition.
*/
export interface ResolvedInvitationVariable {
entityIdentifier: string;
variableIdentifier: string;
roleIdentifier?: string;
value: XOInvitationVariableValue;
name?: string;
description?: string;
type?: string;
hint?: string;
}
/**
* A transaction input from invitation commits enriched with its template definition.
*/
export type ResolvedInvitationInput = XOInvitationInput & {
entityIdentifier: string;
name?: string;
description?: string;
icon?: string;
unlockingScript?: string;
omitChangeAmounts?: XOTemplateInput["omitChangeAmounts"];
};
/**
* A transaction output from invitation commits enriched with its template definition.
*/
export type ResolvedInvitationOutput = XOInvitationOutput & {
entityIdentifier: string;
name?: string;
description?: string;
icon?: string;
roles?: Record<string, ResolvedInvitationOutputRoleMetadata>;
lockingScript?: string;
};
/**
* Flattened, template-enriched invitation data for UI consumption.
*/
export interface ResolvedInvitationData {
invitationIdentifier: string;
templateIdentifier: string;
actionIdentifier: string;
variables: ResolvedInvitationVariable[];
inputs: ResolvedInvitationInput[];
outputs: ResolvedInvitationOutput[];
}
/**
* Picks human-readable view fields from a template definition.
*/
function pickTemplateViewMetadata(
definition: TemplateViewMetadata | undefined,
): TemplateViewMetadata {
if (!definition) return {};
return {
...(definition.name !== undefined && { name: definition.name }),
...(definition.description !== undefined && {
description: definition.description,
}),
...(definition.icon !== undefined && { icon: definition.icon }),
};
}
/**
* Picks variable metadata from a template variable definition.
*/
function pickTemplateVariableMetadata(
definition: XOTemplateVariable | undefined,
): Pick<ResolvedInvitationVariable, "name" | "description" | "type" | "hint"> {
if (!definition) return {};
return {
...pickTemplateViewMetadata(definition),
...(definition.type !== undefined && { type: definition.type }),
...(definition.hint !== undefined && { hint: definition.hint }),
};
}
/**
* Picks input metadata from a template input definition.
*/
function pickTemplateInputMetadata(
definition: XOTemplateInput | undefined,
): Pick<
ResolvedInvitationInput,
"name" | "description" | "icon" | "unlockingScript" | "omitChangeAmounts"
> {
if (!definition) return {};
return {
...pickTemplateViewMetadata(definition),
...(definition.unlockingScript !== undefined && {
unlockingScript: definition.unlockingScript,
}),
...(definition.omitChangeAmounts !== undefined && {
omitChangeAmounts: definition.omitChangeAmounts,
}),
};
}
/**
* Template display metadata layered onto a committed output.
*/
interface TemplateOutputMetadata {
name?: string;
description?: string;
icon?: string;
roles?: Record<string, ResolvedInvitationOutputRoleMetadata>;
lockingScript?: string;
valueSatoshis?: bigint | string;
token?: XOTemplateOutput["token"];
}
/**
* Picks output metadata from a template output definition.
*
* Committed output values (e.g. lockingBytecode) take precedence over template
* defaults; display-oriented fields like name, description, and template
* valueSatoshis expressions are layered on for UI rendering.
*/
function pickTemplateOutputMetadata(
definition: XOTemplateOutput | undefined,
): TemplateOutputMetadata {
if (!definition) return {};
const roles = definition.roles
? Object.fromEntries(
Object.entries(definition.roles).map(([roleId, roleDefinition]) => [
roleId,
pickTemplateViewMetadata(roleDefinition),
]),
)
: undefined;
return {
...pickTemplateViewMetadata(definition),
...(roles !== undefined && Object.keys(roles).length > 0 && { roles }),
...(definition.lockingScript !== undefined && {
lockingScript: definition.lockingScript,
}),
...(definition.valueSatoshis !== undefined && {
valueSatoshis: definition.valueSatoshis,
}),
...(definition.token !== undefined && { token: definition.token }),
};
}
/**
* Enriches a committed variable with its template definition.
*/
function resolveVariable(
variable: XOInvitationVariable,
entityIdentifier: string,
template: XOTemplate,
): ResolvedInvitationVariable {
const definition = template.variables?.[variable.variableIdentifier];
return {
entityIdentifier,
variableIdentifier: variable.variableIdentifier,
...(variable.roleIdentifier !== undefined && {
roleIdentifier: variable.roleIdentifier,
}),
value: variable.value,
...pickTemplateVariableMetadata(definition),
};
}
/**
* Enriches a committed input with its template definition when an identifier is present.
*/
function resolveInput(
input: XOInvitationInput,
entityIdentifier: string,
template: XOTemplate,
): ResolvedInvitationInput {
const definition = input.inputIdentifier
? template.inputs?.[input.inputIdentifier]
: undefined;
return {
entityIdentifier,
...input,
...pickTemplateInputMetadata(definition),
};
}
/**
* Enriches a committed output with its template definition when an identifier is present.
*/
function resolveOutput(
output: XOInvitationOutput,
entityIdentifier: string,
template: XOTemplate,
): ResolvedInvitationOutput {
const definition = output.outputIdentifier
? template.outputs?.[output.outputIdentifier]
: undefined;
const templateMetadata = pickTemplateOutputMetadata(definition);
return {
entityIdentifier,
...output,
...templateMetadata,
} as ResolvedInvitationOutput;
}
/**
* Returns flattened, template-enriched invitation data for UI display.
*
* Commits are walked in order; variables, inputs, and outputs are collected
* into top-level arrays with `entityIdentifier` and template metadata attached.
* Items without a template identifier (e.g. ad-hoc change outputs) keep only
* their committed fields.
*
* @param invitation - The raw invitation in standard XO format.
* @param template - The template referenced by the invitation.
* @returns Resolved invitation data ready for display.
*/
export function resolveCommitReferences(
invitation: XOInvitation,
template: XOTemplate,
): ResolvedInvitationData {
const variables: ResolvedInvitationVariable[] = [];
const inputs: ResolvedInvitationInput[] = [];
const outputs: ResolvedInvitationOutput[] = [];
for (const commit of invitation.commits ?? []) {
for (const variable of commit.data?.variables ?? []) {
variables.push(
resolveVariable(variable, commit.entityIdentifier, template),
);
}
for (const input of commit.data?.inputs ?? []) {
inputs.push(resolveInput(input, commit.entityIdentifier, template));
}
for (const output of commit.data?.outputs ?? []) {
outputs.push(resolveOutput(output, commit.entityIdentifier, template));
}
}
return {
invitationIdentifier: invitation.invitationIdentifier,
templateIdentifier: invitation.templateIdentifier,
actionIdentifier: invitation.actionIdentifier,
variables,
inputs,
outputs,
};
}