Big changes and fixes. Uses action history. Improve role selection. Remove unused logs

This commit is contained in:
2026-02-08 15:41:14 +00:00
parent da096af0fa
commit df57f1b9ad
16 changed files with 1250 additions and 1181 deletions

376
src/utils/templates.ts Normal file
View File

@@ -0,0 +1,376 @@
import type { XOTemplate } from "@xo-cash/types";
/**
* List of helper functions to make templates easy to develop with.
*
* Most of these are centered around the fact that the templates are very disjointed and sporadic in where the information lies.
*
* I.e. required variables ** names ** are stored in actions.roles.requirements.variables, but the variable definitions are stored in the template.variables object.
* so to make a UI out of that, you first need to iterate over the actions.roles.requirements.variables and then lookup the variable definition in the template.variables object.
* this is a pain, so these functions are here to help.
*
* Simiarly for inputs, outputs, locking scripts, etc. The get referenced in the actions, but then the actual lookup of what is actually is becomes a pain.
*/
/**
* Deepens a templates object by append the actual definitions for the objects as they are referenced in the template.
* NOTE: Whether this is better as part of the template or not can be debated endlessly.
* The decision for separating the defintions from where they are used is likely to reduce template size...
* This could be fruitless though, as its easily compressible (gzip, msgpack, etc) will yield similar results to the separated approach.
*/
type ResolutionMode = "single" | "map";
interface ResolutionRule {
/**
* Dot-separated path pattern.
* - `*` matches any key in an object.
* - `[]` iterates items in an array.
*/
path: string;
/** Root-level collection key to resolve references from. */
from: string;
/**
* - `single`: replaces a string reference with its definition.
* - `map`: converts a `string[]` into a `Record<string, definition>`.
*/
mode: ResolutionMode;
}
/**
* Rules are ordered by dependency so that each phase reads
* collections already enriched by earlier phases.
*
* Dependency graph (leaf → root):
* scripts (no deps)
* variables / roles / data / constants (no deps)
* lockingScripts ← scripts, variables
* inputs ← scripts
* outputs ← lockingScripts
* transactions ← inputs, outputs
* actions ← variables, transactions, data, roles
* lockingScripts.roles.actions ← actions, roles, variables (2nd pass)
* start ← actions, roles
*/
const RESOLUTION_RULES: ResolutionRule[] = [
// ── Phase 1: lockingScripts ← scripts, variables ──────────
{
path: "lockingScripts.*.lockingScript",
from: "scripts",
mode: "single",
},
{
path: "lockingScripts.*.unlockingScript",
from: "scripts",
mode: "single",
},
{
path: "lockingScripts.*.roles.*.state.secrets",
from: "variables",
mode: "map",
},
{
path: "lockingScripts.*.roles.*.state.variables",
from: "variables",
mode: "map",
},
// ── Phase 2: inputs ← scripts ─────────────────────────────
{ path: "inputs.*.unlockingScript", from: "scripts", mode: "single" },
// ── Phase 3: outputs ← lockingScripts (now enriched) ──────
{ path: "outputs.*.lockscript", from: "lockingScripts", mode: "single" },
// ── Phase 4: transactions ← inputs, outputs ───────────────
{ path: "transactions.*.inputs", from: "inputs", mode: "map" },
{ path: "transactions.*.outputs", from: "outputs", mode: "map" },
// ── Phase 5: actions ← variables, transactions, data, roles
{
path: "actions.*.roles.*.requirements.variables",
from: "variables",
mode: "map",
},
{
path: "actions.*.roles.*.requirements.secrets",
from: "variables",
mode: "map",
},
{
path: "actions.*.roles.*.requirements.generate",
from: "variables",
mode: "map",
},
{ path: "actions.*.transaction", from: "transactions", mode: "single" },
{ path: "actions.*.data", from: "data", mode: "map" },
{
path: "actions.*.requirements.roles.[].role",
from: "roles",
mode: "single",
},
// ── Phase 6: lockingScripts.roles.actions ← actions (now enriched), roles, variables
{
path: "lockingScripts.*.roles.*.actions.[].action",
from: "actions",
mode: "single",
},
{
path: "lockingScripts.*.roles.*.actions.[].role",
from: "roles",
mode: "single",
},
{
path: "lockingScripts.*.roles.*.actions.[].secrets",
from: "variables",
mode: "single",
},
// ── Phase 7: start ← actions, roles ───────────────────────
{ path: "start.[].action", from: "actions", mode: "single" },
{ path: "start.[].role", from: "roles", mode: "single" },
];
/**
* Recursively walks `obj` following the path pattern described by `parts`,
* and resolves the leaf reference(s) from `root[from]`.
*/
function applyRule(
obj: unknown,
root: Record<string, any>,
parts: string[],
depth: number,
from: string,
mode: ResolutionMode,
): void {
if (obj == null || typeof obj !== "object") return;
const part = parts[depth]!;
const isLast = depth === parts.length - 1;
// ── Leaf: perform the resolution ──────────────────────────
if (isLast) {
const collection = root[from] as Record<string, unknown> | undefined;
const record = obj as Record<string, unknown>;
if (!collection || !(part in record)) return;
const value = record[part];
if (mode === "single") {
if (typeof value === "string" && value in collection) {
record[part] = structuredClone(collection[value]);
}
} else {
// "map" convert string[] → Record<string, definition>
if (Array.isArray(value)) {
const resolved: Record<string, unknown> = {};
for (const ref of value) {
if (typeof ref === "string" && ref in collection) {
resolved[ref] = structuredClone(collection[ref]);
}
}
record[part] = resolved;
}
}
return;
}
// ── Intermediate path segments ────────────────────────────
if (part === "*") {
// Wildcard: iterate every key of the current object
for (const key of Object.keys(obj as Record<string, unknown>)) {
applyRule(
(obj as Record<string, unknown>)[key],
root,
parts,
depth + 1,
from,
mode,
);
}
} else if (part === "[]") {
// Array wildcard: iterate every item
if (Array.isArray(obj)) {
for (const item of obj) {
applyRule(item, root, parts, depth + 1, from, mode);
}
}
} else {
// Exact key: descend
const next = (obj as Record<string, unknown>)[part];
if (next !== undefined) {
applyRule(next, root, parts, depth + 1, from, mode);
}
}
}
/**
* Returns a deep clone of `template` with every string reference replaced
* by the full definition it points to.
*
* References are resolved in dependency order so that embedded objects
* themselves contain resolved (not string) references wherever possible.
*
* The only place resolution deliberately stops is at the circular edge
* `lockingScripts → actions → transactions → outputs → lockingScripts`:
* the lockingScript copies embedded inside output→transaction→action chains
* will have their script/variable refs resolved but will *not* re-embed
* actions (which would cause infinite nesting).
*/
export function resolveTemplateReferences(
template: XOTemplate,
): ResolvedXOTemplate {
const resolved = structuredClone(template);
for (const rule of RESOLUTION_RULES) {
applyRule(resolved, resolved, rule.path.split("."), 0, rule.from, rule.mode);
}
return resolved as unknown as ResolvedXOTemplate;
}
// ─── Base definition types (inferred from your template) ─────────
// Adjust these to match your actual @xo-cash/types definitions.
interface VariableDefinition {
name: string;
description: string;
type: string;
hint?: string;
}
interface RoleDefinition {
name: string;
description: string;
icon: string;
}
interface ScriptDefinition {
// scripts are raw strings in your template
script: string;
}
interface DataDefinition {
value: string;
type: string;
hint?: string;
}
// ─── Resolved sub-types ──────────────────────────────────────────
interface ResolvedActionRoleRequirements {
variables?: Record<string, VariableDefinition>;
secrets?: Record<string, VariableDefinition>;
generate?: Record<string, VariableDefinition>;
}
interface ResolvedActionRole {
name?: string;
description?: string;
icon?: string;
requirements?: ResolvedActionRoleRequirements;
}
interface ResolvedRoleSlot {
role: RoleDefinition; // was string
slots: { min: number; max: number | undefined };
}
interface ResolvedOutputDefinition {
name: string;
description: string;
icon: string;
lockscript: ResolvedLockingScriptDefinition; // was string
valueSatoshis?: string | null;
token?: unknown;
}
interface ResolvedInputDefinition {
name: string;
description: string;
icon: string;
unlockingScript: string; // resolved from scripts (string → string)
token?: unknown;
omitChangeAmounts?: unknown;
}
interface ResolvedTransactionDefinition {
name: string;
description: string;
icon?: string;
roles?: Record<string, unknown>;
inputs: Record<string, ResolvedInputDefinition>; // was string[]
outputs: Record<string, ResolvedOutputDefinition>; // was string[]
version: number;
locktime: number;
composable?: boolean;
}
interface ResolvedActionDefinition {
name: string;
description: string;
icon: string;
roles: Record<string, ResolvedActionRole>;
requirements: {
roles: ResolvedRoleSlot[];
};
transaction?: ResolvedTransactionDefinition; // was string
data?: Record<string, DataDefinition>; // was string[]
condition?: string;
}
interface ResolvedLockingScriptRoleAction {
action: ResolvedActionDefinition; // was string
role: RoleDefinition; // was string
secrets: VariableDefinition; // was string
}
interface ResolvedLockingScriptRole {
state?: {
variables: Record<string, VariableDefinition>; // was string[]
secrets: Record<string, VariableDefinition>; // was string[]
};
actions?: ResolvedLockingScriptRoleAction[];
selectable?: boolean;
privacy?: boolean;
}
interface ResolvedLockingScriptDefinition {
name: string;
description: string;
icon: string;
lockingType: string;
lockingScript: string; // resolved from scripts (string → string)
unlockingScript?: string;
roles?: Record<string, ResolvedLockingScriptRole>;
actions?: unknown[];
state?: unknown[];
secrets?: unknown[];
balance?: boolean;
selectable?: boolean;
privacy?: boolean;
}
interface ResolvedStartEntry {
action: ResolvedActionDefinition; // was string
role: RoleDefinition; // was string
}
// ─── The full resolved template ──────────────────────────────────
interface ResolvedXOTemplate
extends Omit<
XOTemplate,
| "actions"
| "transactions"
| "outputs"
| "inputs"
| "lockingScripts"
| "start"
> {
start: ResolvedStartEntry[];
actions: Record<string, ResolvedActionDefinition>;
transactions: Record<string, ResolvedTransactionDefinition>;
outputs: Record<string, ResolvedOutputDefinition>;
inputs: Record<string, ResolvedInputDefinition>;
lockingScripts: Record<string, ResolvedLockingScriptDefinition>;
}