Compare commits
8 Commits
c7e1d69e2d
...
kiok-updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
051fc0c9ac
|
|||
|
cfcba02bb3
|
|||
|
3ee2d53766
|
|||
|
d089e909f8
|
|||
|
771968dfbb
|
|||
|
d2c37fd957
|
|||
|
bca736dab4
|
|||
|
69adee180a
|
@@ -174,7 +174,7 @@ _{{FUNC_NAME}}_completions() {
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
append|sign|broadcast|requirements|export|inspect)
|
||||
append|sign|broadcast|requirements|export|inspect|delete)
|
||||
# These subcommands expect an invitation identifier as first arg.
|
||||
local pos=$((cword - subcmd_idx))
|
||||
if [[ $pos -eq 1 ]]; then
|
||||
|
||||
@@ -298,6 +298,7 @@ ${bold("Sub-commands:")}
|
||||
- requirements <invitation-id> ${dim("Show requirements for an invitation")}
|
||||
- import <invitation-file> ${dim("Import an invitation from a file")}
|
||||
- export <invitation-id> [output-file] ${dim("Export an invitation to stdout or a file")}
|
||||
- delete <invitation-id> ${dim("Delete an invitation")}
|
||||
- inspect <invitation-id | invitation-file> ${dim("Inspect an invitation")}
|
||||
- list ${dim("List all invitations")}
|
||||
|
||||
@@ -955,6 +956,47 @@ export const handleInvitationCommand = async (
|
||||
return handleInvitationExportCommand(deps, args.slice(1), options);
|
||||
}
|
||||
|
||||
case "delete": {
|
||||
// Get the invitation identifier from the arguments
|
||||
const invitationIdentifier = args[1];
|
||||
deps.io.verbose(`Invitation identifier: ${invitationIdentifier}`);
|
||||
|
||||
// If they didnt provide us with an invitation identifier, print the help message and throw an error
|
||||
// TODO: Should probably print a specific help message for this command?
|
||||
if (!invitationIdentifier) {
|
||||
deps.io.verbose("No invitation identifier provided");
|
||||
printInvitationHelp(deps.io);
|
||||
throw new CommandError(
|
||||
"invitation.delete.identifier_missing",
|
||||
"No invitation identifier provided",
|
||||
);
|
||||
}
|
||||
|
||||
// Find the invitation instance in our list of invitations
|
||||
const invitation = deps.app.invitations.find(
|
||||
(candidate) =>
|
||||
candidate.data.invitationIdentifier === invitationIdentifier,
|
||||
);
|
||||
|
||||
// If the invitation is not found, print an error and throw an error
|
||||
if (!invitation) {
|
||||
deps.io.err(`Invitation not found: ${invitationIdentifier}`);
|
||||
throw new CommandError(
|
||||
"invitation.delete.not_found",
|
||||
`Invitation not found: ${invitationIdentifier}`,
|
||||
);
|
||||
}
|
||||
deps.io.verbose(`Invitation: ${formatObject(invitation.data)}`);
|
||||
|
||||
// Delete the invitation
|
||||
await invitation.delete();
|
||||
deps.io.verbose(`Invitation deleted: ${formatObject(invitation.data)}`);
|
||||
deps.io.out(`Invitation deleted: ${invitationIdentifier}`);
|
||||
|
||||
// Return the invitation identifier
|
||||
return { invitationIdentifier };
|
||||
}
|
||||
|
||||
case "list": {
|
||||
// List all the invitations
|
||||
const invitations = await Promise.all(
|
||||
|
||||
@@ -67,6 +67,7 @@ export class AppService extends EventEmitter<AppEventMap> {
|
||||
{
|
||||
onUpdated: (invitation: XOInvitation) => void;
|
||||
onStatusChanged: (status: string) => void;
|
||||
onRemoved: () => void;
|
||||
}
|
||||
>();
|
||||
|
||||
@@ -241,13 +242,25 @@ export class AppService extends EventEmitter<AppEventMap> {
|
||||
invitationIdentifier,
|
||||
});
|
||||
};
|
||||
const onRemoved = () => {
|
||||
this.detachInvitationListeners(invitationIdentifier);
|
||||
this.invitations.splice(this.invitations.indexOf(invitation), 1);
|
||||
this.bumpInvitationRevision(invitationIdentifier);
|
||||
this.emit("invitation-removed", invitation);
|
||||
this.emit("wallet-state-changed", {
|
||||
reason: "invitation-removed",
|
||||
invitationIdentifier: invitationIdentifier,
|
||||
});
|
||||
};
|
||||
|
||||
invitation.on("invitation-updated", onUpdated);
|
||||
invitation.on("invitation-status-changed", onStatusChanged);
|
||||
invitation.on("invitation-removed", onRemoved);
|
||||
|
||||
this.invitationEventCleanup.set(invitationIdentifier, {
|
||||
onUpdated,
|
||||
onStatusChanged,
|
||||
onRemoved,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
generateTemplateIdentifier,
|
||||
hasInvitationExpired,
|
||||
mergeInvitationCommits,
|
||||
resolveCommitReferences,
|
||||
serializeInvitation,
|
||||
deserializeInvitation,
|
||||
type ResolvedInvitationData,
|
||||
} from "@xo-cash/engine";
|
||||
import type {
|
||||
XOInvitation,
|
||||
@@ -17,6 +19,7 @@ import type {
|
||||
XOInvitationOutput,
|
||||
XOInvitationVariable,
|
||||
XOInvitationVariableValue,
|
||||
XOTemplate,
|
||||
} from "@xo-cash/types";
|
||||
import type { UnspentOutputData } from "@xo-cash/state";
|
||||
import {
|
||||
@@ -36,9 +39,12 @@ import { EventEmitter } from "../utils/event-emitter.js";
|
||||
import { decodeExtendedJsonObject } from "../utils/ext-json.js";
|
||||
import { compileCashAssemblyString } from "@xo-cash/engine";
|
||||
|
||||
export type { ResolvedInvitationData } from "@xo-cash/engine";
|
||||
|
||||
export type InvitationEventMap = {
|
||||
"invitation-updated": XOInvitation;
|
||||
"invitation-status-changed": string;
|
||||
"invitation-removed": void;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
@@ -103,11 +109,33 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
);
|
||||
|
||||
// Create the invitation
|
||||
const invitationInstance = new Invitation(engineInvitation, dependencies);
|
||||
const invitationInstance = new Invitation(
|
||||
engineInvitation,
|
||||
dependencies,
|
||||
template,
|
||||
);
|
||||
|
||||
return invitationInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattened, template-enriched view of {@link Invitation.data}.
|
||||
* Updated automatically whenever invitation data changes.
|
||||
*/
|
||||
public resolvedData: ResolvedInvitationData = {
|
||||
invitationIdentifier: "",
|
||||
templateIdentifier: "",
|
||||
actionIdentifier: "",
|
||||
variables: [],
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* The template used to enrich {@link resolvedData}.
|
||||
*/
|
||||
private template: XOTemplate;
|
||||
|
||||
/**
|
||||
* The invitation data.
|
||||
*/
|
||||
@@ -145,14 +173,19 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
/**
|
||||
* Create an invitation and start the SSE Session required for it.
|
||||
*/
|
||||
constructor(invitation: XOInvitation, dependencies: InvitationDependencies) {
|
||||
constructor(
|
||||
invitation: XOInvitation,
|
||||
dependencies: InvitationDependencies,
|
||||
template: XOTemplate,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.data = invitation;
|
||||
this.template = template;
|
||||
this.engine = dependencies.engine;
|
||||
this.syncServer = dependencies.syncServer;
|
||||
this.storage = dependencies.storage;
|
||||
this.electrum = dependencies.electrum;
|
||||
this.updateInvitationData(invitation);
|
||||
|
||||
// Apply SSE updates serially so each engine update sees the latest history.
|
||||
this.syncServer.on("message", (event) => {
|
||||
@@ -167,6 +200,14 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates raw invitation data and recomputes {@link resolvedData}.
|
||||
*/
|
||||
private updateInvitationData(invitation: XOInvitation): void {
|
||||
this.data = invitation;
|
||||
this.resolvedData = resolveCommitReferences(invitation, this.template);
|
||||
}
|
||||
|
||||
private enqueueSyncUpdate(update: () => Promise<void>): Promise<void> {
|
||||
const queuedUpdate = this.sseUpdateQueue.then(update);
|
||||
this.sseUpdateQueue = queuedUpdate.catch(() => {});
|
||||
@@ -197,19 +238,21 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
|
||||
try {
|
||||
// Prefer keeping the engine's local invitation state in sync.
|
||||
this.data = stripLocalInvitationMetadata(
|
||||
await this.engine.updateInvitation({
|
||||
...this.data,
|
||||
...invitation,
|
||||
commits: combinedCommits,
|
||||
}),
|
||||
this.updateInvitationData(
|
||||
stripLocalInvitationMetadata(
|
||||
await this.engine.updateInvitation({
|
||||
...this.data,
|
||||
...invitation,
|
||||
commits: combinedCommits,
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
this.emit(
|
||||
"error",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
this.data = { ...this.data, commits: combinedCommits };
|
||||
this.updateInvitationData({ ...this.data, commits: combinedCommits });
|
||||
}
|
||||
|
||||
await this.storage.set(this.data.invitationIdentifier, this.data);
|
||||
@@ -243,19 +286,21 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
const newCommits = this.mergeCommits(this.data.commits, invitation.commits);
|
||||
|
||||
try {
|
||||
this.data = stripLocalInvitationMetadata(
|
||||
await this.engine.updateInvitation({
|
||||
...this.data,
|
||||
...invitation,
|
||||
commits: newCommits,
|
||||
}),
|
||||
this.updateInvitationData(
|
||||
stripLocalInvitationMetadata(
|
||||
await this.engine.updateInvitation({
|
||||
...this.data,
|
||||
...invitation,
|
||||
commits: newCommits,
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
this.emit(
|
||||
"error",
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
this.data = { ...this.data, commits: newCommits };
|
||||
this.updateInvitationData({ ...this.data, commits: newCommits });
|
||||
}
|
||||
|
||||
await this.storage.set(this.data.invitationIdentifier, this.data);
|
||||
@@ -488,7 +533,9 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
*/
|
||||
async accept(acceptParams?: InvitationParameters): Promise<void> {
|
||||
// Accept the invitation
|
||||
this.data = await this.engine.acceptInvitation(this.data, acceptParams);
|
||||
this.updateInvitationData(
|
||||
await this.engine.acceptInvitation(this.data, acceptParams),
|
||||
);
|
||||
|
||||
// Sync the invitation to the sync server
|
||||
await this.publishInvitation(this.data);
|
||||
@@ -529,7 +576,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
// Store the signed invitation in the storage
|
||||
await this.storage.set(this.data.invitationIdentifier, signedInvitation);
|
||||
|
||||
this.data = signedInvitation;
|
||||
this.updateInvitationData(signedInvitation);
|
||||
|
||||
// Update the status of the invitation
|
||||
await this.updateStatus();
|
||||
@@ -563,9 +610,8 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
await this.ensureAccepted();
|
||||
|
||||
// Append the commit to the invitation
|
||||
this.data = await this.engine.appendInvitation(
|
||||
this.data.invitationIdentifier,
|
||||
data,
|
||||
this.updateInvitationData(
|
||||
await this.engine.appendInvitation(this.data.invitationIdentifier, data),
|
||||
);
|
||||
|
||||
// Sync the invitation to the sync server
|
||||
@@ -839,4 +885,23 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
|
||||
return totalSats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the invitation from the Local SQLite db as well as the Engine's internal DB
|
||||
* NOTE: This uses methods that are marked "DANGEROUSLY" inside the engine and behaviour may change
|
||||
*/
|
||||
public async delete() {
|
||||
// Remove the invitation from our local db
|
||||
this.storage.remove(this.data.invitationIdentifier);
|
||||
|
||||
// Remove the invitation from the engine's internal db
|
||||
await this.engine.DANGEROUS_deleteStoredInvitation(
|
||||
this.data.invitationIdentifier,
|
||||
);
|
||||
|
||||
this.emit("invitation-removed", this.data.invitationIdentifier);
|
||||
|
||||
// Update the status of the invitation
|
||||
await this.updateStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 '@xo-cash/engine';
|
||||
|
||||
import { InvitationImportFlow } from './invitation-import/InvitationImportFlow.js';
|
||||
import { compileCashAssemblyString } from '@xo-cash/engine';
|
||||
@@ -65,6 +63,7 @@ const actionItems: ListItemData<string>[] = [
|
||||
{ key: 'sign', label: 'Sign Transaction', value: 'sign' },
|
||||
{ key: 'broadcast', label: 'Broadcast Transaction', value: 'broadcast' },
|
||||
{ key: 'copy', label: 'Copy Invitation ID', value: 'copy' },
|
||||
{ key: 'delete', label: 'Delete Invitation', value: 'delete' },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -356,6 +355,28 @@ export function InvitationScreen(): React.ReactElement {
|
||||
}
|
||||
}, [selectedInvitation, showInfo, showError, setStatus]);
|
||||
|
||||
/**
|
||||
* Delete the selected invitation from both our SQLite db and the engine's db
|
||||
* NOTE: This uses methods marked "DANGEROUSLY" internally, and may change in the future.
|
||||
*/
|
||||
const deleteInvitation = useCallback(async () => {
|
||||
if (!selectedInvitation) return;
|
||||
|
||||
setIsLoading(true)
|
||||
setStatus('Removing invitation...')
|
||||
|
||||
try {
|
||||
await selectedInvitation.delete();
|
||||
showInfo('Invitation successfully deleted')
|
||||
setStatus('Ready')
|
||||
} catch (error) {
|
||||
showError(`Failed to delete invitation: ${error instanceof Error ? error.message : String(error)}`)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setStatus('Ready')
|
||||
}
|
||||
})
|
||||
|
||||
const copyId = useCallback(async () => {
|
||||
if (!selectedInvitation) {
|
||||
showError('No invitation selected');
|
||||
@@ -401,16 +422,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;
|
||||
@@ -516,6 +532,9 @@ export function InvitationScreen(): React.ReactElement {
|
||||
case 'broadcast':
|
||||
broadcastTransaction();
|
||||
break;
|
||||
case 'delete':
|
||||
deleteInvitation();
|
||||
break;
|
||||
}
|
||||
}, [selectedInvitation, copyId, acceptInvitation, fillRequirements, signInvitation, broadcastTransaction, navigate]);
|
||||
|
||||
@@ -595,14 +614,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 +647,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 +662,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 +728,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 +757,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 +785,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 +797,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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user