Format with prettier. Use screen mode for invitation import - dialog mode is broken.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* Handles BCH Mnemonic parsing to/from URL form.
|
||||
* Pulled directly from the old stack package.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import { z } from "zod";
|
||||
|
||||
export type BCHMnemonicURLRaw = {
|
||||
entropy: Uint8Array;
|
||||
@@ -17,7 +17,7 @@ export type BCHMnemonicURLRaw = {
|
||||
* Handles BCHMnemonic URLs
|
||||
*/
|
||||
export class BCHMnemonicURL {
|
||||
static PROTOCOL = 'bch-mnemonic';
|
||||
static PROTOCOL = "bch-mnemonic";
|
||||
|
||||
/**
|
||||
* Check if a URL is a valid wallet backup URL
|
||||
@@ -48,7 +48,7 @@ export class BCHMnemonicURL {
|
||||
}
|
||||
|
||||
// Decode the entropy.
|
||||
const entropy = new Uint8Array(Buffer.from(url.pathname, 'base64'));
|
||||
const entropy = new Uint8Array(Buffer.from(url.pathname, "base64"));
|
||||
|
||||
// Pick out our encoding keys from the URL
|
||||
const params = BCHMnemonicURL.schema.parse(
|
||||
@@ -74,7 +74,7 @@ export class BCHMnemonicURL {
|
||||
static fromRaw(raw: BCHMnemonicURLRaw): BCHMnemonicURL {
|
||||
// Add entropy validation
|
||||
if (!raw.entropy || raw.entropy.length === 0) {
|
||||
throw new Error('Invalid entropy: must be non-empty');
|
||||
throw new Error("Invalid entropy: must be non-empty");
|
||||
}
|
||||
|
||||
// Validate entropy length (typically 16, 20, 24, 28, or 32 bytes for BIP39)
|
||||
@@ -100,7 +100,7 @@ export class BCHMnemonicURL {
|
||||
*/
|
||||
toURL(): string {
|
||||
// Conver the mnemonic words into the entropy used to derive the mnemonic words
|
||||
const entropyBase64 = Buffer.from(this.raw.entropy).toString('base64');
|
||||
const entropyBase64 = Buffer.from(this.raw.entropy).toString("base64");
|
||||
|
||||
// Create a new URL object with the prefix and the base64 encoded mnemonic
|
||||
const url = new URL(`${BCHMnemonicURL.PROTOCOL}:${entropyBase64}`);
|
||||
@@ -135,24 +135,24 @@ export class BCHMnemonicURL {
|
||||
}
|
||||
|
||||
static ENCODING_KEYS = {
|
||||
language: 'l',
|
||||
passphrase: 'p',
|
||||
comment: 'c',
|
||||
path: 'd',
|
||||
startHeight: 'h',
|
||||
language: "l",
|
||||
passphrase: "p",
|
||||
comment: "c",
|
||||
path: "d",
|
||||
startHeight: "h",
|
||||
} as const;
|
||||
|
||||
static SUPPORTED_LANGUAGES = [
|
||||
'en',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
'ja',
|
||||
'es',
|
||||
'pt',
|
||||
'ko',
|
||||
'fr',
|
||||
'it',
|
||||
'cs',
|
||||
"en",
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
"ja",
|
||||
"es",
|
||||
"pt",
|
||||
"ko",
|
||||
"fr",
|
||||
"it",
|
||||
"cs",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,7 +71,7 @@ export class ExponentialBackoff {
|
||||
fn: () => Promise<T>,
|
||||
onError = (_error: Error) => {},
|
||||
): Promise<T> {
|
||||
let lastError: Error = new Error('Exponential backoff: Max retries hit');
|
||||
let lastError: Error = new Error("Exponential backoff: Max retries hit");
|
||||
|
||||
let attempt = 0;
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Extended JSON encoding/decoding utilities.
|
||||
* Handles BigInt and Uint8Array serialization for communication with sync-server.
|
||||
*
|
||||
*
|
||||
* TODO: These are intended as temporary stand-ins until this functionality has been implemented directly in LibAuth.
|
||||
* We are doing this so that we may better standardize with the rest of the BCH eco-system in future.
|
||||
* See: https://github.com/bitauth/libauth/pull/108
|
||||
*/
|
||||
|
||||
import { binToHex, hexToBin } from '@bitauth/libauth';
|
||||
import { binToHex, hexToBin } from "@bitauth/libauth";
|
||||
|
||||
/**
|
||||
* Replaces BigInt and Uint8Array values with their ExtJSON string representations.
|
||||
@@ -15,7 +15,7 @@ import { binToHex, hexToBin } from '@bitauth/libauth';
|
||||
* @returns The replaced value as an ExtJSON string, or the original value
|
||||
*/
|
||||
export const extendedJsonReplacer = function (value: unknown): unknown {
|
||||
if (typeof value === 'bigint') {
|
||||
if (typeof value === "bigint") {
|
||||
return `<bigint: ${value.toString()}n>`;
|
||||
} else if (value instanceof Uint8Array) {
|
||||
return `<Uint8Array: ${binToHex(value)}>`;
|
||||
@@ -36,7 +36,7 @@ export const extendedJsonReviver = function (value: unknown): unknown {
|
||||
|
||||
// Only perform a check if the value is a string.
|
||||
// NOTE: We can skip all other values as all Extended JSON encoded fields WILL be a string.
|
||||
if (typeof value === 'string') {
|
||||
if (typeof value === "string") {
|
||||
// Check if this value matches an Extended JSON encoded bigint.
|
||||
const bigintMatch = value.match(bigIntRegex);
|
||||
if (bigintMatch) {
|
||||
@@ -70,7 +70,7 @@ export const encodeExtendedJsonObject = function (value: unknown): unknown {
|
||||
// If this is an object type (and it is not null - which is technically an "object")...
|
||||
// ... and it is not an ArrayBuffer (e.g. Uint8Array) which is also technically an "object...
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!ArrayBuffer.isView(value)
|
||||
) {
|
||||
@@ -83,7 +83,9 @@ export const encodeExtendedJsonObject = function (value: unknown): unknown {
|
||||
const encodedObject: Record<string, unknown> = {};
|
||||
|
||||
// Iterate through each entry and encode it to extended JSON.
|
||||
for (const [key, valueToEncode] of Object.entries(value as Record<string, unknown>)) {
|
||||
for (const [key, valueToEncode] of Object.entries(
|
||||
value as Record<string, unknown>,
|
||||
)) {
|
||||
encodedObject[key] = encodeExtendedJsonObject(valueToEncode);
|
||||
}
|
||||
|
||||
@@ -104,7 +106,7 @@ export const decodeExtendedJsonObject = function (value: unknown): unknown {
|
||||
// If this is an object type (and it is not null - which is technically an "object")...
|
||||
// ... and it is not an ArrayBuffer (e.g. Uint8Array) which is also technically an "object...
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!ArrayBuffer.isView(value)
|
||||
) {
|
||||
@@ -117,7 +119,9 @@ export const decodeExtendedJsonObject = function (value: unknown): unknown {
|
||||
const decodedObject: Record<string, unknown> = {};
|
||||
|
||||
// Iterate through each entry and decode it from extended JSON.
|
||||
for (const [key, valueToEncode] of Object.entries(value as Record<string, unknown>)) {
|
||||
for (const [key, valueToEncode] of Object.entries(
|
||||
value as Record<string, unknown>,
|
||||
)) {
|
||||
decodedObject[key] = decodeExtendedJsonObject(valueToEncode);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import type { HistoryItem, HistoryInvitationItem, HistoryUtxoItem } from '../services/history.js';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryInvitationItem,
|
||||
HistoryUtxoItem,
|
||||
} from "../services/history.js";
|
||||
|
||||
export type HistoryColorName = 'info' | 'warning' | 'success' | 'error' | 'muted' | 'text';
|
||||
export type HistoryColorName =
|
||||
| "info"
|
||||
| "warning"
|
||||
| "success"
|
||||
| "error"
|
||||
| "muted"
|
||||
| "text";
|
||||
|
||||
export type HistoryRowType = 'invitation' | 'invitation_input' | 'invitation_output' | 'utxo';
|
||||
export type HistoryRowType =
|
||||
| "invitation"
|
||||
| "invitation_input"
|
||||
| "invitation_output"
|
||||
| "utxo";
|
||||
|
||||
export interface HistoryDisplayRow {
|
||||
id: string;
|
||||
@@ -20,14 +34,16 @@ export function formatHistoryDate(timestamp?: number): string | undefined {
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
}
|
||||
|
||||
export function buildHistoryDisplayRows(items: HistoryItem[]): HistoryDisplayRow[] {
|
||||
export function buildHistoryDisplayRows(
|
||||
items: HistoryItem[],
|
||||
): HistoryDisplayRow[] {
|
||||
const rows: HistoryDisplayRow[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.kind === 'invitation') {
|
||||
if (item.kind === "invitation") {
|
||||
rows.push({
|
||||
id: item.id,
|
||||
type: 'invitation',
|
||||
type: "invitation",
|
||||
label: item.description,
|
||||
timestamp: item.createdAtTimestamp,
|
||||
isNested: false,
|
||||
@@ -35,10 +51,13 @@ export function buildHistoryDisplayRows(items: HistoryItem[]): HistoryDisplayRow
|
||||
});
|
||||
|
||||
for (const input of item.inputs) {
|
||||
const satsPrefix = input.valueSatoshis !== undefined ? `${input.valueSatoshis.toLocaleString()} sats ` : '';
|
||||
const satsPrefix =
|
||||
input.valueSatoshis !== undefined
|
||||
? `${input.valueSatoshis.toLocaleString()} sats `
|
||||
: "";
|
||||
rows.push({
|
||||
id: `${item.id}-input-${input.id}`,
|
||||
type: 'invitation_input',
|
||||
type: "invitation_input",
|
||||
label: `${satsPrefix}${input.outpoint.txid}:${input.outpoint.index}`,
|
||||
description: input.description,
|
||||
isNested: true,
|
||||
@@ -50,8 +69,11 @@ export function buildHistoryDisplayRows(items: HistoryItem[]): HistoryDisplayRow
|
||||
for (const output of item.outputs) {
|
||||
rows.push({
|
||||
id: `${item.id}-output-${output.id}`,
|
||||
type: 'invitation_output',
|
||||
label: output.valueSatoshis !== undefined ? `${output.valueSatoshis.toLocaleString()} sats` : 'Output',
|
||||
type: "invitation_output",
|
||||
label:
|
||||
output.valueSatoshis !== undefined
|
||||
? `${output.valueSatoshis.toLocaleString()} sats`
|
||||
: "Output",
|
||||
description: output.description,
|
||||
isNested: true,
|
||||
utxo: output,
|
||||
@@ -64,8 +86,11 @@ export function buildHistoryDisplayRows(items: HistoryItem[]): HistoryDisplayRow
|
||||
|
||||
rows.push({
|
||||
id: item.id,
|
||||
type: 'utxo',
|
||||
label: item.valueSatoshis !== undefined ? `${item.valueSatoshis.toLocaleString()} sats` : 'UTXO',
|
||||
type: "utxo",
|
||||
label:
|
||||
item.valueSatoshis !== undefined
|
||||
? `${item.valueSatoshis.toLocaleString()} sats`
|
||||
: "UTXO",
|
||||
description: item.description,
|
||||
isNested: false,
|
||||
utxo: item,
|
||||
@@ -75,18 +100,21 @@ export function buildHistoryDisplayRows(items: HistoryItem[]): HistoryDisplayRow
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function getHistoryItemColorName(row: HistoryDisplayRow, isSelected: boolean = false): HistoryColorName {
|
||||
if (isSelected) return 'info';
|
||||
export function getHistoryItemColorName(
|
||||
row: HistoryDisplayRow,
|
||||
isSelected: boolean = false,
|
||||
): HistoryColorName {
|
||||
if (isSelected) return "info";
|
||||
switch (row.type) {
|
||||
case 'invitation':
|
||||
return 'text';
|
||||
case 'invitation_input':
|
||||
return 'error';
|
||||
case 'invitation_output':
|
||||
return 'success';
|
||||
case 'utxo':
|
||||
return row.utxo?.reserved ? 'warning' : 'success';
|
||||
case "invitation":
|
||||
return "text";
|
||||
case "invitation_input":
|
||||
return "error";
|
||||
case "invitation_output":
|
||||
return "success";
|
||||
case "utxo":
|
||||
return row.utxo?.reserved ? "warning" : "success";
|
||||
default:
|
||||
return 'text';
|
||||
return "text";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { XOTemplate, XOTemplateTransactionOutput } from '@xo-cash/types';
|
||||
import type { Invitation } from '../services/invitation.js';
|
||||
import type { XOTemplate, XOTemplateTransactionOutput } from "@xo-cash/types";
|
||||
import type { Invitation } from "../services/invitation.js";
|
||||
|
||||
export interface SelectableUtxoLike {
|
||||
outpointTransactionHash: string;
|
||||
@@ -16,14 +16,17 @@ export const hasMissingRequirements = (missingRequirements: {
|
||||
roles?: Record<string, unknown>;
|
||||
}): boolean => {
|
||||
return (
|
||||
(missingRequirements.variables?.length ?? 0) > 0
|
||||
|| (missingRequirements.inputs?.length ?? 0) > 0
|
||||
|| (missingRequirements.outputs?.length ?? 0) > 0
|
||||
|| (missingRequirements.roles !== undefined && Object.keys(missingRequirements.roles).length > 0)
|
||||
(missingRequirements.variables?.length ?? 0) > 0 ||
|
||||
(missingRequirements.inputs?.length ?? 0) > 0 ||
|
||||
(missingRequirements.outputs?.length ?? 0) > 0 ||
|
||||
(missingRequirements.roles !== undefined &&
|
||||
Object.keys(missingRequirements.roles).length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
export const isInvitationRequirementsComplete = async (invitation: Invitation): Promise<boolean> => {
|
||||
export const isInvitationRequirementsComplete = async (
|
||||
invitation: Invitation,
|
||||
): Promise<boolean> => {
|
||||
const missingRequirements = await invitation.getMissingRequirements();
|
||||
return !hasMissingRequirements(missingRequirements);
|
||||
};
|
||||
@@ -34,7 +37,7 @@ export const resolveActionRoles = (
|
||||
rolesFromNavigation?: string[],
|
||||
): string[] => {
|
||||
if (rolesFromNavigation && rolesFromNavigation.length > 0) {
|
||||
return [ ...new Set(rolesFromNavigation) ];
|
||||
return [...new Set(rolesFromNavigation)];
|
||||
}
|
||||
|
||||
if (!template || !actionIdentifier) return [];
|
||||
@@ -43,7 +46,7 @@ export const resolveActionRoles = (
|
||||
.filter((entry) => entry.action === actionIdentifier)
|
||||
.map((entry) => entry.role);
|
||||
|
||||
return [ ...new Set(roleIds) ];
|
||||
return [...new Set(roleIds)];
|
||||
};
|
||||
|
||||
export const roleRequiresInputs = (
|
||||
@@ -61,26 +64,38 @@ export const roleRequiresInputs = (
|
||||
|
||||
// Some templates specify slot/input requirements at action.requirements.roles
|
||||
// instead of role.requirements. Respect those as well.
|
||||
const roleRequirement = action.requirements?.roles?.find((requirement) => requirement.role === roleIdentifier);
|
||||
const roleRequirement = action.requirements?.roles?.find(
|
||||
(requirement) => requirement.role === roleIdentifier,
|
||||
);
|
||||
const actionLevelSlotsMin = roleRequirement?.slots?.min ?? 0;
|
||||
if (actionLevelSlotsMin > 0) return true;
|
||||
|
||||
const transactionIdentifier = action.transaction;
|
||||
const transaction = transactionIdentifier ? template.transactions?.[transactionIdentifier] : undefined;
|
||||
const transaction = transactionIdentifier
|
||||
? template.transactions?.[transactionIdentifier]
|
||||
: undefined;
|
||||
const roleInputs = transaction?.roles?.[roleIdentifier]?.inputs;
|
||||
|
||||
return (roleInputs?.length ?? 0) > 0;
|
||||
};
|
||||
|
||||
export const getTransactionOutputIdentifier = (output: XOTemplateTransactionOutput): string | undefined => {
|
||||
if (typeof output === 'string') return output;
|
||||
if (output && typeof output === 'object' && 'output' in output && typeof output.output === 'string') {
|
||||
export const getTransactionOutputIdentifier = (
|
||||
output: XOTemplateTransactionOutput,
|
||||
): string | undefined => {
|
||||
if (typeof output === "string") return output;
|
||||
if (
|
||||
output &&
|
||||
typeof output === "object" &&
|
||||
"output" in output &&
|
||||
typeof output.output === "string"
|
||||
) {
|
||||
return output.output;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const normalizeLockingBytecodeHex = (value: string): string => value.trim().replace(/^0x/i, '');
|
||||
export const normalizeLockingBytecodeHex = (value: string): string =>
|
||||
value.trim().replace(/^0x/i, "");
|
||||
|
||||
export const resolveProvidedLockingBytecodeHex = (
|
||||
template: XOTemplate,
|
||||
@@ -88,18 +103,23 @@ export const resolveProvidedLockingBytecodeHex = (
|
||||
variableValues: Record<string, string>,
|
||||
): string | undefined => {
|
||||
const outputDefinition = template.outputs?.[outputIdentifier];
|
||||
if (!outputDefinition || typeof outputDefinition.lockscript !== 'string') return undefined;
|
||||
if (!outputDefinition || typeof outputDefinition.lockscript !== "string")
|
||||
return undefined;
|
||||
|
||||
const lockingScriptDefinition = (template.lockingScripts as Record<string, unknown> | undefined)?.[outputDefinition.lockscript] as
|
||||
| { lockingScript?: string }
|
||||
| undefined;
|
||||
const lockingScriptDefinition = (
|
||||
template.lockingScripts as Record<string, unknown> | undefined
|
||||
)?.[outputDefinition.lockscript] as { lockingScript?: string } | undefined;
|
||||
const scriptIdentifier = lockingScriptDefinition?.lockingScript;
|
||||
if (!scriptIdentifier) return undefined;
|
||||
|
||||
const scriptExpression = (template.scripts as Record<string, unknown> | undefined)?.[scriptIdentifier];
|
||||
if (typeof scriptExpression !== 'string') return undefined;
|
||||
const scriptExpression = (
|
||||
template.scripts as Record<string, unknown> | undefined
|
||||
)?.[scriptIdentifier];
|
||||
if (typeof scriptExpression !== "string") return undefined;
|
||||
|
||||
const directVariableMatch = scriptExpression.match(/^<\s*([A-Za-z0-9_]+)\s*>$/);
|
||||
const directVariableMatch = scriptExpression.match(
|
||||
/^<\s*([A-Za-z0-9_]+)\s*>$/,
|
||||
);
|
||||
if (!directVariableMatch) return undefined;
|
||||
|
||||
const variableIdentifier = directVariableMatch[1];
|
||||
@@ -111,15 +131,17 @@ export const resolveProvidedLockingBytecodeHex = (
|
||||
return normalizeLockingBytecodeHex(providedValue);
|
||||
};
|
||||
|
||||
export const mapUnspentOutputsToSelectable = (unspentOutputs: any[]): SelectableUtxoLike[] => {
|
||||
export const mapUnspentOutputsToSelectable = (
|
||||
unspentOutputs: any[],
|
||||
): SelectableUtxoLike[] => {
|
||||
return unspentOutputs.map((utxo: any) => ({
|
||||
outpointTransactionHash: utxo.outpointTransactionHash,
|
||||
outpointIndex: utxo.outpointIndex,
|
||||
valueSatoshis: BigInt(utxo.valueSatoshis),
|
||||
lockingBytecode: utxo.lockingBytecode
|
||||
? typeof utxo.lockingBytecode === 'string'
|
||||
? typeof utxo.lockingBytecode === "string"
|
||||
? utxo.lockingBytecode
|
||||
: Buffer.from(utxo.lockingBytecode).toString('hex')
|
||||
: Buffer.from(utxo.lockingBytecode).toString("hex")
|
||||
: undefined,
|
||||
selected: false,
|
||||
}));
|
||||
@@ -133,7 +155,10 @@ export const autoSelectGreedyUtxos = (
|
||||
const seenLockingBytecodes = new Set<string>();
|
||||
|
||||
for (const utxo of utxos) {
|
||||
if (utxo.lockingBytecode && seenLockingBytecodes.has(utxo.lockingBytecode)) {
|
||||
if (
|
||||
utxo.lockingBytecode &&
|
||||
seenLockingBytecodes.has(utxo.lockingBytecode)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (utxo.lockingBytecode) {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/**
|
||||
* Invitation utility functions.
|
||||
*
|
||||
*
|
||||
* Pure functions for parsing and formatting invitation data.
|
||||
* These functions have no React dependencies and can be used
|
||||
* in both TUI and CLI contexts.
|
||||
*/
|
||||
|
||||
import type { Invitation } from '../services/invitation.js';
|
||||
import type { XOTemplate } from '@xo-cash/types';
|
||||
import type { Invitation } from "../services/invitation.js";
|
||||
import type { XOTemplate } from "@xo-cash/types";
|
||||
|
||||
/**
|
||||
* Color names for invitation states.
|
||||
* These are semantic color names that can be mapped to actual colors
|
||||
* by the consuming application (TUI or CLI).
|
||||
*/
|
||||
export type StateColorName = 'info' | 'warning' | 'success' | 'error' | 'muted';
|
||||
export type StateColorName = "info" | "warning" | "success" | "error" | "muted";
|
||||
|
||||
/**
|
||||
* Input data extracted from invitation commits.
|
||||
@@ -61,7 +61,7 @@ export interface FormattedInvitationItem {
|
||||
|
||||
/**
|
||||
* Get the current state/status of an invitation.
|
||||
*
|
||||
*
|
||||
* @param invitation - The invitation to get state for
|
||||
* @returns The status string
|
||||
*/
|
||||
@@ -71,34 +71,34 @@ export function getInvitationState(invitation: Invitation): string {
|
||||
|
||||
/**
|
||||
* Get the semantic color name for an invitation state.
|
||||
*
|
||||
*
|
||||
* @param state - The invitation state string
|
||||
* @returns A semantic color name
|
||||
*/
|
||||
export function getStateColorName(state: string): StateColorName {
|
||||
switch (state) {
|
||||
case 'created':
|
||||
case 'published':
|
||||
return 'info';
|
||||
case 'pending':
|
||||
return 'warning';
|
||||
case 'ready':
|
||||
case 'signed':
|
||||
case 'complete':
|
||||
case 'broadcast':
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'expired':
|
||||
case 'error':
|
||||
return 'error';
|
||||
case "created":
|
||||
case "published":
|
||||
return "info";
|
||||
case "pending":
|
||||
return "warning";
|
||||
case "ready":
|
||||
case "signed":
|
||||
case "complete":
|
||||
case "broadcast":
|
||||
case "completed":
|
||||
return "success";
|
||||
case "expired":
|
||||
case "error":
|
||||
return "error";
|
||||
default:
|
||||
return 'muted';
|
||||
return "muted";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all inputs from invitation commits.
|
||||
*
|
||||
*
|
||||
* @param invitation - The invitation to extract inputs from
|
||||
* @returns Array of input data
|
||||
*/
|
||||
@@ -118,11 +118,13 @@ export function getInvitationInputs(invitation: Invitation): InvitationInput[] {
|
||||
|
||||
/**
|
||||
* Extract all outputs from invitation commits.
|
||||
*
|
||||
*
|
||||
* @param invitation - The invitation to extract outputs from
|
||||
* @returns Array of output data
|
||||
*/
|
||||
export function getInvitationOutputs(invitation: Invitation): InvitationOutput[] {
|
||||
export function getInvitationOutputs(
|
||||
invitation: Invitation,
|
||||
): InvitationOutput[] {
|
||||
const outputs: InvitationOutput[] = [];
|
||||
for (const commit of invitation.data.commits || []) {
|
||||
for (const output of commit.data?.outputs || []) {
|
||||
@@ -139,11 +141,13 @@ export function getInvitationOutputs(invitation: Invitation): InvitationOutput[]
|
||||
|
||||
/**
|
||||
* Extract all variables from invitation commits.
|
||||
*
|
||||
*
|
||||
* @param invitation - The invitation to extract variables from
|
||||
* @returns Array of variable data
|
||||
*/
|
||||
export function getInvitationVariables(invitation: Invitation): InvitationVariable[] {
|
||||
export function getInvitationVariables(
|
||||
invitation: Invitation,
|
||||
): InvitationVariable[] {
|
||||
const variables: InvitationVariable[] = [];
|
||||
for (const commit of invitation.data.commits || []) {
|
||||
for (const variable of commit.data?.variables || []) {
|
||||
@@ -160,14 +164,17 @@ export function getInvitationVariables(invitation: Invitation): InvitationVariab
|
||||
|
||||
/**
|
||||
* Get the user's role from commits (the role they have accepted).
|
||||
*
|
||||
*
|
||||
* @param invitation - The invitation to check
|
||||
* @param userEntityId - The user's entity identifier
|
||||
* @returns The role identifier if found, null otherwise
|
||||
*/
|
||||
export function getUserRole(invitation: Invitation, userEntityId: string | null): string | null {
|
||||
export function getUserRole(
|
||||
invitation: Invitation,
|
||||
userEntityId: string | null,
|
||||
): string | null {
|
||||
if (!userEntityId) return null;
|
||||
|
||||
|
||||
for (const commit of invitation.data.commits || []) {
|
||||
if (commit.entityIdentifier === userEntityId) {
|
||||
// Check inputs for role
|
||||
@@ -189,32 +196,32 @@ export function getUserRole(invitation: Invitation, userEntityId: string | null)
|
||||
|
||||
/**
|
||||
* Format an invitation for display in a list.
|
||||
*
|
||||
*
|
||||
* @param invitation - The invitation to format
|
||||
* @param template - Optional template for additional info (name)
|
||||
* @returns Formatted item data for display
|
||||
*/
|
||||
export function formatInvitationListItem(
|
||||
invitation: Invitation,
|
||||
template?: XOTemplate | null
|
||||
template?: XOTemplate | null,
|
||||
): FormattedInvitationItem {
|
||||
// Validate that we have the minimum required data
|
||||
const invitationId = invitation?.data?.invitationIdentifier;
|
||||
const actionId = invitation?.data?.actionIdentifier;
|
||||
|
||||
|
||||
if (!invitationId || !actionId) {
|
||||
return {
|
||||
label: '',
|
||||
status: 'error',
|
||||
statusColor: 'error',
|
||||
label: "",
|
||||
status: "error",
|
||||
statusColor: "error",
|
||||
isValid: false,
|
||||
};
|
||||
}
|
||||
|
||||
const state = getInvitationState(invitation);
|
||||
const templateName = template?.name ?? 'Unknown';
|
||||
const templateName = template?.name ?? "Unknown";
|
||||
const shortId = formatInvitationId(invitationId, 8);
|
||||
|
||||
|
||||
return {
|
||||
label: `[${state}] ${templateName}-${actionId} (${shortId})`,
|
||||
status: state,
|
||||
@@ -225,7 +232,7 @@ export function formatInvitationListItem(
|
||||
|
||||
/**
|
||||
* Format an invitation ID for display (truncated).
|
||||
*
|
||||
*
|
||||
* @param id - The full invitation ID
|
||||
* @param maxLength - Maximum length for display
|
||||
* @returns Truncated ID string
|
||||
@@ -238,7 +245,7 @@ export function formatInvitationId(id: string, maxLength: number = 16): string {
|
||||
|
||||
/**
|
||||
* Get all unique entity identifiers from an invitation's commits.
|
||||
*
|
||||
*
|
||||
* @param invitation - The invitation to check
|
||||
* @returns Array of unique entity identifiers
|
||||
*/
|
||||
@@ -254,12 +261,15 @@ export function getInvitationParticipants(invitation: Invitation): string[] {
|
||||
|
||||
/**
|
||||
* Check if a user is a participant in an invitation.
|
||||
*
|
||||
*
|
||||
* @param invitation - The invitation to check
|
||||
* @param userEntityId - The user's entity identifier
|
||||
* @returns True if the user has made at least one commit
|
||||
*/
|
||||
export function isUserParticipant(invitation: Invitation, userEntityId: string | null): boolean {
|
||||
export function isUserParticipant(
|
||||
invitation: Invitation,
|
||||
userEntityId: string | null,
|
||||
): boolean {
|
||||
if (!userEntityId) return false;
|
||||
return getInvitationParticipants(invitation).includes(userEntityId);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ export class Logger {
|
||||
private readonly path: string,
|
||||
) {}
|
||||
|
||||
send(level: 'log' | 'error' | 'warn' | 'info', message: string, ...metadata: unknown[]) {
|
||||
send(
|
||||
level: "log" | "error" | "warn" | "info",
|
||||
message: string,
|
||||
...metadata: unknown[]
|
||||
) {
|
||||
const data = {
|
||||
level,
|
||||
message: `${this.path}: ${message}`,
|
||||
@@ -13,34 +17,34 @@ export class Logger {
|
||||
};
|
||||
|
||||
fetch(`${this.endpoint}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': this.token,
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": this.token,
|
||||
},
|
||||
}).catch(error => {
|
||||
console.error('Failed to send log to logger:', error);
|
||||
}).catch((error) => {
|
||||
console.error("Failed to send log to logger:", error);
|
||||
});
|
||||
}
|
||||
|
||||
log(message: string, ...metadata: unknown[]) {
|
||||
this.send('log', message, ...metadata);
|
||||
this.send("log", message, ...metadata);
|
||||
}
|
||||
|
||||
error(message: string, ...metadata: unknown[]) {
|
||||
this.send('error', message, ...metadata);
|
||||
this.send("error", message, ...metadata);
|
||||
}
|
||||
|
||||
warn(message: string, ...metadata: unknown[]) {
|
||||
this.send('warn', message, ...metadata);
|
||||
this.send("warn", message, ...metadata);
|
||||
}
|
||||
|
||||
info(message: string, ...metadata: unknown[]) {
|
||||
this.send('info', message, ...metadata);
|
||||
this.send("info", message, ...metadata);
|
||||
}
|
||||
|
||||
child(path: string): Logger {
|
||||
return new Logger(this.endpoint, this.token, `${this.path}.${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { ExponentialBackoff } from './exponential-backoff.js';
|
||||
import { ExponentialBackoff } from "./exponential-backoff.js";
|
||||
|
||||
// Type declarations for browser environment (not available in Node.js)
|
||||
declare const document: {
|
||||
visibilityState: 'visible' | 'hidden';
|
||||
addEventListener: (event: string, handler: (event: Event) => void) => void;
|
||||
removeEventListener: (event: string, handler: (event: Event) => void) => void;
|
||||
} | undefined;
|
||||
declare const document:
|
||||
| {
|
||||
visibilityState: "visible" | "hidden";
|
||||
addEventListener: (
|
||||
event: string,
|
||||
handler: (event: Event) => void,
|
||||
) => void;
|
||||
removeEventListener: (
|
||||
event: string,
|
||||
handler: (event: Event) => void,
|
||||
) => void;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* A Server-Sent Events client implementation using fetch API.
|
||||
@@ -51,14 +59,14 @@ export class SSESession {
|
||||
this.options = {
|
||||
// Use default fetch function.
|
||||
fetch: (...args) => fetch(...args),
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Accept: "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
onConnected: () => {},
|
||||
onMessage: () => {},
|
||||
onError: (error) => console.error('SSESession error:', error),
|
||||
onError: (error) => console.error("SSESession error:", error),
|
||||
onDisconnected: () => {},
|
||||
onReconnect: (options) => Promise.resolve(options),
|
||||
|
||||
@@ -71,10 +79,10 @@ export class SSESession {
|
||||
this.controller = new AbortController();
|
||||
|
||||
// Set up visibility change handling if in mobile browser environment
|
||||
if (typeof document !== 'undefined') {
|
||||
if (typeof document !== "undefined") {
|
||||
this.visibilityChangeHandler = this.handleVisibilityChange.bind(this);
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
"visibilitychange",
|
||||
this.visibilityChangeHandler,
|
||||
);
|
||||
}
|
||||
@@ -85,16 +93,16 @@ export class SSESession {
|
||||
*/
|
||||
private async handleVisibilityChange(): Promise<void> {
|
||||
// Guard for Node.js environment where document is undefined
|
||||
if (typeof document === 'undefined') return;
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
// When going to background, close the current connection cleanly
|
||||
// This allows us to reconnect mobile devices when they come back after leaving the tab or browser app.
|
||||
if (document.visibilityState === 'hidden') {
|
||||
if (document.visibilityState === "hidden") {
|
||||
this.controller.abort();
|
||||
}
|
||||
|
||||
// When coming back to foreground, attempt to reconnect if not connected
|
||||
if (document.visibilityState === 'visible' && !this.connected) {
|
||||
if (document.visibilityState === "visible" && !this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
}
|
||||
@@ -115,7 +123,7 @@ export class SSESession {
|
||||
headers: headers || {},
|
||||
body: body || null,
|
||||
signal: this.controller.signal,
|
||||
cache: 'no-store',
|
||||
cache: "no-store",
|
||||
};
|
||||
|
||||
const exponentialBackoff = ExponentialBackoff.from({
|
||||
@@ -144,7 +152,7 @@ export class SSESession {
|
||||
}
|
||||
|
||||
if (!res.body) {
|
||||
throw new Error('Response body is null');
|
||||
throw new Error("Response body is null");
|
||||
}
|
||||
|
||||
return res.body.getReader();
|
||||
@@ -228,10 +236,10 @@ export class SSESession {
|
||||
const line = lines[i];
|
||||
|
||||
// Empty line signals the end of an event
|
||||
if (line === '') {
|
||||
if (line === "") {
|
||||
if (currentEvent.data) {
|
||||
// Remove trailing newline if present
|
||||
currentEvent.data = currentEvent.data.replace(/\n$/, '');
|
||||
currentEvent.data = currentEvent.data.replace(/\n$/, "");
|
||||
events.push(currentEvent as SSEvent);
|
||||
currentEvent = {};
|
||||
completeEventCount = i + 1;
|
||||
@@ -242,24 +250,24 @@ export class SSESession {
|
||||
if (!line) continue;
|
||||
|
||||
// Parse field: value format
|
||||
const colonIndex = line.indexOf(':');
|
||||
const colonIndex = line.indexOf(":");
|
||||
if (colonIndex === -1) continue;
|
||||
|
||||
const field = line.slice(0, colonIndex);
|
||||
// Skip initial space after colon if present
|
||||
const valueStartIndex =
|
||||
colonIndex + 1 + (line[colonIndex + 1] === ' ' ? 1 : 0);
|
||||
colonIndex + 1 + (line[colonIndex + 1] === " " ? 1 : 0);
|
||||
const value = line.slice(valueStartIndex);
|
||||
|
||||
if (field === 'data') {
|
||||
if (field === "data") {
|
||||
currentEvent.data = currentEvent.data
|
||||
? currentEvent.data + '\n' + value
|
||||
? currentEvent.data + "\n" + value
|
||||
: value;
|
||||
} else if (field === 'event') {
|
||||
} else if (field === "event") {
|
||||
currentEvent.event = value;
|
||||
} else if (field === 'id') {
|
||||
} else if (field === "id") {
|
||||
currentEvent.id = value;
|
||||
} else if (field === 'retry') {
|
||||
} else if (field === "retry") {
|
||||
const retryMs = parseInt(value, 10);
|
||||
if (!isNaN(retryMs)) {
|
||||
currentEvent.retry = retryMs;
|
||||
@@ -268,7 +276,7 @@ export class SSESession {
|
||||
}
|
||||
|
||||
// Store the remainder of the buffer for the next chunk
|
||||
const remainder = lines.slice(completeEventCount).join('\n');
|
||||
const remainder = lines.slice(completeEventCount).join("\n");
|
||||
this.messageBuffer = this.textEncoder.encode(remainder);
|
||||
|
||||
return events;
|
||||
@@ -291,9 +299,9 @@ export class SSESession {
|
||||
this.controller.abort();
|
||||
|
||||
// Remove the visibility handler (This is only required on browsers)
|
||||
if (this.visibilityChangeHandler && typeof document !== 'undefined') {
|
||||
if (this.visibilityChangeHandler && typeof document !== "undefined") {
|
||||
document.removeEventListener(
|
||||
'visibilitychange',
|
||||
"visibilitychange",
|
||||
this.visibilityChangeHandler,
|
||||
);
|
||||
this.visibilityChangeHandler = null;
|
||||
@@ -348,7 +356,7 @@ export interface SSESessionOptions {
|
||||
/**
|
||||
* HTTP method to use (GET or POST).
|
||||
*/
|
||||
method: 'GET' | 'POST';
|
||||
method: "GET" | "POST";
|
||||
|
||||
/**
|
||||
* HTTP headers to send with the request.
|
||||
|
||||
@@ -4,14 +4,17 @@ import { SSESession, type SSEvent } from "./sse-client.js";
|
||||
import { decodeExtendedJson, encodeExtendedJson } from "./ext-json.js";
|
||||
|
||||
export type SyncServerEventMap = {
|
||||
'connected': void;
|
||||
'disconnected': void;
|
||||
'error': Error;
|
||||
'message': SSEvent;
|
||||
}
|
||||
connected: void;
|
||||
disconnected: void;
|
||||
error: Error;
|
||||
message: SSEvent;
|
||||
};
|
||||
|
||||
export class SyncServer extends EventEmitter<SyncServerEventMap> {
|
||||
static async from(baseUrl: string, invitationIdentifier: string): Promise<SyncServer> {
|
||||
static async from(
|
||||
baseUrl: string,
|
||||
invitationIdentifier: string,
|
||||
): Promise<SyncServer> {
|
||||
const server = new SyncServer(baseUrl, invitationIdentifier);
|
||||
await server.connect();
|
||||
return server;
|
||||
@@ -19,22 +22,32 @@ export class SyncServer extends EventEmitter<SyncServerEventMap> {
|
||||
|
||||
private sse: SSESession;
|
||||
|
||||
constructor(private readonly baseUrl: string, private readonly invitationIdentifier: string) {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly invitationIdentifier: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
// Create an SSE Session
|
||||
this.sse = new SSESession(`${baseUrl}/invitations?invitationIdentifier=${invitationIdentifier}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'text/event-stream',
|
||||
},
|
||||
this.sse = new SSESession(
|
||||
`${baseUrl}/invitations?invitationIdentifier=${invitationIdentifier}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
|
||||
// Create our event bubblers
|
||||
onMessage: (event: SSEvent) => this.emit('message', event),
|
||||
onError: (error: unknown) => this.emit('error', error instanceof Error ? error : new Error(String(error))),
|
||||
onDisconnected: () => this.emit('disconnected', undefined),
|
||||
onConnected: () => this.emit('connected', undefined),
|
||||
});
|
||||
// Create our event bubblers
|
||||
onMessage: (event: SSEvent) => this.emit("message", event),
|
||||
onError: (error: unknown) =>
|
||||
this.emit(
|
||||
"error",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
),
|
||||
onDisconnected: () => this.emit("disconnected", undefined),
|
||||
onConnected: () => this.emit("connected", undefined),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,13 +73,17 @@ export class SyncServer extends EventEmitter<SyncServerEventMap> {
|
||||
*/
|
||||
async getInvitation(identifier: string): Promise<XOInvitation | undefined> {
|
||||
// Send a GET request to the sync server
|
||||
const response = await fetch(`${this.baseUrl}/invitations?invitationIdentifier=${identifier}`);
|
||||
|
||||
if(!response.ok) {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/invitations?invitationIdentifier=${identifier}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get invitation: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const invitation = decodeExtendedJson(await response.text()) as XOInvitation | undefined;
|
||||
|
||||
const invitation = decodeExtendedJson(await response.text()) as
|
||||
| XOInvitation
|
||||
| undefined;
|
||||
return invitation;
|
||||
}
|
||||
|
||||
@@ -78,10 +95,10 @@ export class SyncServer extends EventEmitter<SyncServerEventMap> {
|
||||
async publishInvitation(invitation: XOInvitation): Promise<XOInvitation> {
|
||||
// Send a POST request to the sync server
|
||||
const response = await fetch(`${this.baseUrl}/invitations`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: encodeExtendedJson(invitation),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -96,4 +113,4 @@ export class SyncServer extends EventEmitter<SyncServerEventMap> {
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Template utility functions.
|
||||
*
|
||||
*
|
||||
* Pure functions for parsing and formatting template data.
|
||||
* These functions have no React dependencies and can be used
|
||||
* in both TUI and CLI contexts.
|
||||
*/
|
||||
|
||||
import type { XOTemplate, XOTemplateAction } from '@xo-cash/types';
|
||||
import type { XOTemplate, XOTemplateAction } from "@xo-cash/types";
|
||||
|
||||
/**
|
||||
* Formatted template list item data.
|
||||
@@ -57,25 +57,25 @@ export interface TemplateRole {
|
||||
|
||||
/**
|
||||
* Format a template for display in a list.
|
||||
*
|
||||
*
|
||||
* @param template - The template to format
|
||||
* @param index - Optional index for numbered display
|
||||
* @returns Formatted item data for display
|
||||
*/
|
||||
export function formatTemplateListItem(
|
||||
template: XOTemplate | null | undefined,
|
||||
index?: number
|
||||
index?: number,
|
||||
): FormattedTemplateItem {
|
||||
if (!template) {
|
||||
return {
|
||||
label: '',
|
||||
label: "",
|
||||
description: undefined,
|
||||
isValid: false,
|
||||
};
|
||||
}
|
||||
|
||||
const name = template.name || 'Unnamed Template';
|
||||
const prefix = index !== undefined ? `${index + 1}. ` : '';
|
||||
const name = template.name || "Unnamed Template";
|
||||
const prefix = index !== undefined ? `${index + 1}. ` : "";
|
||||
|
||||
return {
|
||||
label: `${prefix}${name}`,
|
||||
@@ -86,7 +86,7 @@ export function formatTemplateListItem(
|
||||
|
||||
/**
|
||||
* Format an action for display in a list.
|
||||
*
|
||||
*
|
||||
* @param actionId - The action identifier
|
||||
* @param action - The action definition from the template
|
||||
* @param roleCount - Number of roles that can start this action
|
||||
@@ -97,11 +97,11 @@ export function formatActionListItem(
|
||||
actionId: string,
|
||||
action: XOTemplateAction | null | undefined,
|
||||
roleCount: number = 1,
|
||||
index?: number
|
||||
index?: number,
|
||||
): FormattedActionItem {
|
||||
if (!actionId) {
|
||||
return {
|
||||
label: '',
|
||||
label: "",
|
||||
description: undefined,
|
||||
roleCount: 0,
|
||||
isValid: false,
|
||||
@@ -109,8 +109,8 @@ export function formatActionListItem(
|
||||
}
|
||||
|
||||
const name = action?.name || actionId;
|
||||
const prefix = index !== undefined ? `${index + 1}. ` : '';
|
||||
const roleSuffix = roleCount > 1 ? ` (${roleCount} roles)` : '';
|
||||
const prefix = index !== undefined ? `${index + 1}. ` : "";
|
||||
const roleSuffix = roleCount > 1 ? ` (${roleCount} roles)` : "";
|
||||
|
||||
return {
|
||||
label: `${prefix}${name}${roleSuffix}`,
|
||||
@@ -124,14 +124,14 @@ export function formatActionListItem(
|
||||
* Deduplicate starting actions from a template.
|
||||
* Multiple roles that can start the same action are counted
|
||||
* but returned as a single entry.
|
||||
*
|
||||
*
|
||||
* @param template - The template to process
|
||||
* @param startingActions - Array of { action, role } pairs
|
||||
* @returns Array of unique starting actions with role counts
|
||||
*/
|
||||
export function deduplicateStartingActions(
|
||||
template: XOTemplate,
|
||||
startingActions: Array<{ action: string; role: string }>
|
||||
startingActions: Array<{ action: string; role: string }>,
|
||||
): UniqueStartingAction[] {
|
||||
const actionMap = new Map<string, UniqueStartingAction>();
|
||||
|
||||
@@ -154,7 +154,7 @@ export function deduplicateStartingActions(
|
||||
|
||||
/**
|
||||
* Get all roles from a template.
|
||||
*
|
||||
*
|
||||
* @param template - The template to process
|
||||
* @returns Array of role information
|
||||
*/
|
||||
@@ -163,7 +163,7 @@ export function getTemplateRoles(template: XOTemplate): TemplateRole[] {
|
||||
|
||||
return Object.entries(template.roles).map(([roleId, role]) => {
|
||||
// Handle case where role might be a string instead of object
|
||||
const roleObj = typeof role === 'object' ? role : null;
|
||||
const roleObj = typeof role === "object" ? role : null;
|
||||
return {
|
||||
roleId,
|
||||
name: roleObj?.name || roleId,
|
||||
@@ -174,21 +174,22 @@ export function getTemplateRoles(template: XOTemplate): TemplateRole[] {
|
||||
|
||||
/**
|
||||
* Get roles that can start a specific action.
|
||||
*
|
||||
*
|
||||
* @param template - The template to check
|
||||
* @param actionIdentifier - The action to check
|
||||
* @returns Array of role information for roles that can start this action
|
||||
*/
|
||||
export function getRolesForAction(
|
||||
template: XOTemplate,
|
||||
actionIdentifier: string
|
||||
actionIdentifier: string,
|
||||
): TemplateRole[] {
|
||||
const startEntries = (template.start ?? [])
|
||||
.filter((s) => s.action === actionIdentifier);
|
||||
const startEntries = (template.start ?? []).filter(
|
||||
(s) => s.action === actionIdentifier,
|
||||
);
|
||||
|
||||
return startEntries.map((entry) => {
|
||||
const roleDef = template.roles?.[entry.role];
|
||||
const roleObj = typeof roleDef === 'object' ? roleDef : null;
|
||||
const roleObj = typeof roleDef === "object" ? roleDef : null;
|
||||
return {
|
||||
roleId: entry.role,
|
||||
name: roleObj?.name || entry.role,
|
||||
@@ -199,48 +200,52 @@ export function getRolesForAction(
|
||||
|
||||
/**
|
||||
* Get template name safely.
|
||||
*
|
||||
*
|
||||
* @param template - The template
|
||||
* @returns The template name or a default
|
||||
*/
|
||||
export function getTemplateName(template: XOTemplate | null | undefined): string {
|
||||
return template?.name || 'Unknown Template';
|
||||
export function getTemplateName(
|
||||
template: XOTemplate | null | undefined,
|
||||
): string {
|
||||
return template?.name || "Unknown Template";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template description safely.
|
||||
*
|
||||
*
|
||||
* @param template - The template
|
||||
* @returns The template description or undefined
|
||||
*/
|
||||
export function getTemplateDescription(template: XOTemplate | null | undefined): string | undefined {
|
||||
export function getTemplateDescription(
|
||||
template: XOTemplate | null | undefined,
|
||||
): string | undefined {
|
||||
return template?.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action name safely.
|
||||
*
|
||||
*
|
||||
* @param template - The template containing the action
|
||||
* @param actionId - The action identifier
|
||||
* @returns The action name or the action ID as fallback
|
||||
*/
|
||||
export function getActionName(
|
||||
template: XOTemplate | null | undefined,
|
||||
actionId: string
|
||||
actionId: string,
|
||||
): string {
|
||||
return template?.actions?.[actionId]?.name || actionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action description safely.
|
||||
*
|
||||
*
|
||||
* @param template - The template containing the action
|
||||
* @param actionId - The action identifier
|
||||
* @returns The action description or undefined
|
||||
*/
|
||||
export function getActionDescription(
|
||||
template: XOTemplate | null | undefined,
|
||||
actionId: string
|
||||
actionId: string,
|
||||
): string | undefined {
|
||||
return template?.actions?.[actionId]?.description;
|
||||
}
|
||||
|
||||
@@ -2,19 +2,19 @@ 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.
|
||||
* 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.
|
||||
*/
|
||||
@@ -222,7 +222,14 @@ export function resolveTemplateReferences(
|
||||
const resolved = structuredClone(template);
|
||||
|
||||
for (const rule of RESOLUTION_RULES) {
|
||||
applyRule(resolved, resolved, rule.path.split("."), 0, rule.from, rule.mode);
|
||||
applyRule(
|
||||
resolved,
|
||||
resolved,
|
||||
rule.path.split("."),
|
||||
0,
|
||||
rule.from,
|
||||
rule.mode,
|
||||
);
|
||||
}
|
||||
|
||||
return resolved as unknown as ResolvedXOTemplate;
|
||||
@@ -357,20 +364,14 @@ interface ResolvedStartEntry {
|
||||
|
||||
// ─── The full resolved template ──────────────────────────────────
|
||||
|
||||
interface ResolvedXOTemplate
|
||||
extends Omit<
|
||||
XOTemplate,
|
||||
| "actions"
|
||||
| "transactions"
|
||||
| "outputs"
|
||||
| "inputs"
|
||||
| "lockingScripts"
|
||||
| "start"
|
||||
> {
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user