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

@@ -26,12 +26,10 @@ import type { XOInvitationCommit, XOInvitationVariableValue, XOTemplate } from '
import {
getInvitationState,
getStateColorName,
getInvitationInputs,
getInvitationOutputs,
getInvitationVariables,
formatInvitationListItem,
formatInvitationId,
} from '../../../utils/invitation-utils.js';
import type { ResolvedInvitationVariable } from '../../../utils/resolve-invitation-data.js';
import { InvitationImportFlow } from './invitation-import/InvitationImportFlow.js';
import { compileCashAssemblyString } from '@xo-cash/engine';
@@ -401,16 +399,11 @@ export function InvitationScreen(): React.ReactElement {
setStatus('Analyzing invitation...');
let requiredAmount = 0n;
const commits = selectedInvitation.data.commits || [];
for (const commit of commits) {
const variables = commit.data?.variables || [];
for (const variable of variables) {
if (variable.variableIdentifier?.toLowerCase().includes('satoshi')) {
requiredAmount = BigInt(variable.value?.toString() || '0');
break;
}
for (const variable of selectedInvitation.resolvedData.variables) {
if (variable.variableIdentifier.toLowerCase().includes('satoshi')) {
requiredAmount = BigInt(variable.value?.toString() || '0');
break;
}
if (requiredAmount > 0n) break;
}
const fee = 500n;
@@ -595,14 +588,17 @@ export function InvitationScreen(): React.ReactElement {
const state = getInvitationState(selectedInvitation);
const action = selectedTemplate?.actions?.[selectedInvitation.data.actionIdentifier];
const inputs = getInvitationInputs(selectedInvitation);
const outputs = getInvitationOutputs(selectedInvitation);
const variables = getInvitationVariables(selectedInvitation);
const { inputs, outputs, variables } = selectedInvitation.resolvedData;
const userEntityId = ownInvitationContext.entityIdentifier;
const userRole = ownInvitationContext.roleIdentifier;
const roleInfoRaw = userRole && selectedTemplate?.roles?.[userRole];
const roleInfo = roleInfoRaw && typeof roleInfoRaw === 'object' ? roleInfoRaw : null;
const variableValues = variables.reduce((acc, variable) => {
acc[variable.variableIdentifier] = variable.value as XOInvitationVariableValue;
return acc;
}, {} as Record<string, XOInvitationVariableValue>);
const getFiatSuffix = (satoshis: bigint): string => {
const fiatValue = formatSatoshisToFiat(satoshis);
return fiatValue ? ` (~${fiatValue})` : '';
@@ -625,11 +621,10 @@ export function InvitationScreen(): React.ReactElement {
}
};
const isSatoshisVariable = (variableIdentifier: string): boolean => {
const templateVariable = selectedTemplate?.variables?.[variableIdentifier];
const templateType = templateVariable?.type?.toLowerCase();
const templateHint = templateVariable?.hint?.toLowerCase();
const identifier = variableIdentifier.toLowerCase();
const isSatoshisVariable = (variable: ResolvedInvitationVariable): boolean => {
const templateHint = variable.hint?.toLowerCase();
const templateType = variable.type?.toLowerCase();
const identifier = variable.variableIdentifier.toLowerCase();
if (templateHint?.includes('satoshi')) {
return true;
@@ -641,6 +636,20 @@ export function InvitationScreen(): React.ReactElement {
);
};
const compileResolvedDescription = (description?: string): string | null => {
if (!description) return null;
try {
return compileCashAssemblyString({
cashAssemblyText: description,
variables: variableValues,
evaluationDecodeMode: 'bigint',
});
} catch {
return description;
}
};
return (
<Box flexDirection="column">
{/* Type & Status */}
@@ -693,28 +702,21 @@ export function InvitationScreen(): React.ReactElement {
) : (
inputs.map((input, idx) => {
const isUserInput = input.entityIdentifier === userEntityId;
const inputTemplate = selectedTemplate?.inputs?.[input.inputIdentifier ?? ''];
const inputSatoshis = (
'valueSatoshis' in input && input.valueSatoshis !== undefined
)
? parseNumberishToBigInt(input.valueSatoshis)
: null;
const inputDescription = compileResolvedDescription(input.description);
return (
<Text
key={`input-${idx}`}
color={isUserInput ? colors.success : colors.text}
>
{/* Indicator for whether this is the user's input */}
{' '}{isUserInput ? '• ' : '○ '}
{/* TODO: Why doesnt this stuff work? It just cant resolve inputs? */}
{/* Input name */}
{inputTemplate?.name ?? input.inputIdentifier ?? `Input ${idx}`}
{/* Input role */}
{input.name ?? input.inputIdentifier ?? `Input ${idx}`}
{input.roleIdentifier && ` (${input.roleIdentifier})`}
{/* Input value */}
{inputDescription && ` - ${inputDescription}`}
{inputSatoshis !== null && ` ${formatSatoshis(inputSatoshis)}${getFiatSuffix(inputSatoshis)}`}
</Text>
);
@@ -729,33 +731,18 @@ export function InvitationScreen(): React.ReactElement {
) : (
outputs.map((output, idx) => {
const isUserOutput = output.entityIdentifier === userEntityId;
const outputTemplate = selectedTemplate?.outputs?.[output.outputIdentifier ?? ''];
const outputSatoshis = output.valueSatoshis !== undefined
? parseNumberishToBigInt(output.valueSatoshis)
: null;
const outputDescription = compileResolvedDescription(output.description);
return (
<Text
key={`output-${idx}`}
color={isUserOutput ? colors.success : colors.text}
>
{/* Indicator for whether this is the user's output */}
{' '}{isUserOutput ? '• ' : '○ '}
{/* Output name */}
{outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`}
{/* Output description */}
{outputTemplate?.description && ' - ' + compileCashAssemblyString({
cashAssemblyText: outputTemplate?.description,
variables: variables.reduce((acc, variable) => {
acc[variable.variableIdentifier] = variable.value as XOInvitationVariableValue;
return acc;
}, {} as Record<string, XOInvitationVariableValue>),
evaluationDecodeMode: 'bigint'
})}
{/* Output value */}
{output.name ?? output.outputIdentifier ?? `Output ${idx}`}
{outputDescription && ` - ${outputDescription}`}
{outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)}${getFiatSuffix(outputSatoshis)})`}
</Text>
);
@@ -772,11 +759,10 @@ export function InvitationScreen(): React.ReactElement {
) : (
variables.map((variable, idx) => {
const isUserVariable = variable.entityIdentifier === userEntityId;
const varTemplate = selectedTemplate?.variables?.[variable.variableIdentifier];
const displayValue = typeof variable.value === 'bigint'
? variable.value.toString()
: String(variable.value);
const parsedVariableSatoshis = isSatoshisVariable(variable.variableIdentifier)
const parsedVariableSatoshis = isSatoshisVariable(variable)
? parseNumberishToBigInt(variable.value)
: null;
return (
@@ -785,11 +771,11 @@ export function InvitationScreen(): React.ReactElement {
color={isUserVariable ? colors.success : colors.text}
>
{' '}{isUserVariable ? '• ' : '○ '}
{varTemplate?.name ?? variable.variableIdentifier}: {displayValue}
{variable.name ?? variable.variableIdentifier}: {displayValue}
{parsedVariableSatoshis !== null &&
` (${formatSatoshis(parsedVariableSatoshis)}${getFiatSuffix(parsedVariableSatoshis)})`}
{varTemplate?.description && (
<Text color={colors.textMuted} dimColor> - {varTemplate.description}</Text>
{variable.description && (
<Text color={colors.textMuted} dimColor> - {variable.description}</Text>
)}
</Text>
);

View File

@@ -14,12 +14,29 @@ import { useLayeredInput } from '../../../../hooks/useInputLayer.js';
import {
getInvitationState,
getStateColorName,
getInvitationInputs,
getInvitationOutputs,
getInvitationVariables,
} from '../../../../../utils/invitation-utils.js';
import type { PreviewStepProps } from '../types.js';
/**
* Map a semantic color name to an actual theme color value.
*/
function parseNumberishToBigInt(value: unknown): bigint | null {
if (typeof value === 'bigint') {
return value;
}
const asString = String(value).trim();
if (!/^[-]?\d+$/.test(asString)) {
return null;
}
try {
return BigInt(asString);
} catch {
return null;
}
}
/**
* Map a semantic color name to an actual theme color value.
*/
@@ -51,16 +68,18 @@ export function PreviewInvitationStep({
const state = getInvitationState(invitation);
const action = template?.actions?.[invitation.data.actionIdentifier];
const inputs = getInvitationInputs(invitation);
const outputs = getInvitationOutputs(invitation);
const variables = getInvitationVariables(invitation);
const { inputs, outputs, variables } = invitation.resolvedData;
// Collect role identifiers that appear across all commits
// Collect role identifiers that appear across resolved invitation data
const filledRoles = new Set<string>();
for (const commit of invitation.data.commits ?? []) {
for (const input of commit.data?.inputs ?? []) {
if (input.roleIdentifier) filledRoles.add(input.roleIdentifier);
}
for (const input of inputs) {
if (input.roleIdentifier) filledRoles.add(input.roleIdentifier);
}
for (const output of outputs) {
if (output.roleIdentifier) filledRoles.add(output.roleIdentifier);
}
for (const variable of variables) {
if (variable.roleIdentifier) filledRoles.add(variable.roleIdentifier);
}
return (
@@ -143,11 +162,10 @@ export function PreviewInvitationStep({
</Box>
) : (
inputs.map((input, idx) => {
const inputTemplate = template?.inputs?.[input.inputIdentifier ?? ''];
return (
<Box key={`input-${idx}`}>
<Text color={colors.text}>
{' '} {inputTemplate?.name ?? input.inputIdentifier ?? `Input ${idx}`}
{' '} {input.name ?? input.inputIdentifier ?? `Input ${idx}`}
{input.roleIdentifier && ` (${input.roleIdentifier})`}
</Text>
</Box>
@@ -170,15 +188,17 @@ export function PreviewInvitationStep({
</Box>
) : (
outputs.map((output, idx) => {
const outputTemplate = template?.outputs?.[output.outputIdentifier ?? ''];
const fiatValue = output.valueSatoshis !== undefined
? formatSatoshisToFiat(output.valueSatoshis)
: null;
const outputSatoshis = output.valueSatoshis !== undefined
? parseNumberishToBigInt(output.valueSatoshis)
: null;
return (
<Box key={`output-${idx}`}>
<Text color={colors.text}>
{' '} {outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`}
{output.valueSatoshis !== undefined && ` (${formatSatoshis(output.valueSatoshis)})`}
{' '} {output.name ?? output.outputIdentifier ?? `Output ${idx}`}
{outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)})`}
{fiatValue && ` (~${fiatValue})`}
</Text>
</Box>
@@ -201,14 +221,13 @@ export function PreviewInvitationStep({
</Box>
) : (
variables.map((variable, idx) => {
const varTemplate = template?.variables?.[variable.variableIdentifier];
const displayValue = typeof variable.value === 'bigint'
? variable.value.toString()
: String(variable.value);
return (
<Box key={`var-${idx}`}>
<Text color={colors.text}>
{' '} {varTemplate?.name ?? variable.variableIdentifier}: {displayValue}
{' '} {variable.name ?? variable.variableIdentifier}: {displayValue}
</Text>
</Box>
);