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
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
append|sign|broadcast|requirements|export|inspect)
|
append|sign|broadcast|requirements|export|inspect|delete)
|
||||||
# These subcommands expect an invitation identifier as first arg.
|
# These subcommands expect an invitation identifier as first arg.
|
||||||
local pos=$((cword - subcmd_idx))
|
local pos=$((cword - subcmd_idx))
|
||||||
if [[ $pos -eq 1 ]]; then
|
if [[ $pos -eq 1 ]]; then
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ ${bold("Sub-commands:")}
|
|||||||
- requirements <invitation-id> ${dim("Show requirements for an invitation")}
|
- requirements <invitation-id> ${dim("Show requirements for an invitation")}
|
||||||
- import <invitation-file> ${dim("Import an invitation from a file")}
|
- import <invitation-file> ${dim("Import an invitation from a file")}
|
||||||
- export <invitation-id> [output-file] ${dim("Export an invitation to stdout or 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")}
|
- inspect <invitation-id | invitation-file> ${dim("Inspect an invitation")}
|
||||||
- list ${dim("List all invitations")}
|
- list ${dim("List all invitations")}
|
||||||
|
|
||||||
@@ -955,6 +956,47 @@ export const handleInvitationCommand = async (
|
|||||||
return handleInvitationExportCommand(deps, args.slice(1), options);
|
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": {
|
case "list": {
|
||||||
// List all the invitations
|
// List all the invitations
|
||||||
const invitations = await Promise.all(
|
const invitations = await Promise.all(
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export class AppService extends EventEmitter<AppEventMap> {
|
|||||||
{
|
{
|
||||||
onUpdated: (invitation: XOInvitation) => void;
|
onUpdated: (invitation: XOInvitation) => void;
|
||||||
onStatusChanged: (status: string) => void;
|
onStatusChanged: (status: string) => void;
|
||||||
|
onRemoved: () => void;
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@@ -241,13 +242,25 @@ export class AppService extends EventEmitter<AppEventMap> {
|
|||||||
invitationIdentifier,
|
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-updated", onUpdated);
|
||||||
invitation.on("invitation-status-changed", onStatusChanged);
|
invitation.on("invitation-status-changed", onStatusChanged);
|
||||||
|
invitation.on("invitation-removed", onRemoved);
|
||||||
|
|
||||||
this.invitationEventCleanup.set(invitationIdentifier, {
|
this.invitationEventCleanup.set(invitationIdentifier, {
|
||||||
onUpdated,
|
onUpdated,
|
||||||
onStatusChanged,
|
onStatusChanged,
|
||||||
|
onRemoved,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import {
|
|||||||
generateTemplateIdentifier,
|
generateTemplateIdentifier,
|
||||||
hasInvitationExpired,
|
hasInvitationExpired,
|
||||||
mergeInvitationCommits,
|
mergeInvitationCommits,
|
||||||
|
resolveCommitReferences,
|
||||||
serializeInvitation,
|
serializeInvitation,
|
||||||
deserializeInvitation,
|
deserializeInvitation,
|
||||||
|
type ResolvedInvitationData,
|
||||||
} from "@xo-cash/engine";
|
} from "@xo-cash/engine";
|
||||||
import type {
|
import type {
|
||||||
XOInvitation,
|
XOInvitation,
|
||||||
@@ -17,6 +19,7 @@ import type {
|
|||||||
XOInvitationOutput,
|
XOInvitationOutput,
|
||||||
XOInvitationVariable,
|
XOInvitationVariable,
|
||||||
XOInvitationVariableValue,
|
XOInvitationVariableValue,
|
||||||
|
XOTemplate,
|
||||||
} from "@xo-cash/types";
|
} from "@xo-cash/types";
|
||||||
import type { UnspentOutputData } from "@xo-cash/state";
|
import type { UnspentOutputData } from "@xo-cash/state";
|
||||||
import {
|
import {
|
||||||
@@ -36,9 +39,12 @@ import { EventEmitter } from "../utils/event-emitter.js";
|
|||||||
import { decodeExtendedJsonObject } from "../utils/ext-json.js";
|
import { decodeExtendedJsonObject } from "../utils/ext-json.js";
|
||||||
import { compileCashAssemblyString } from "@xo-cash/engine";
|
import { compileCashAssemblyString } from "@xo-cash/engine";
|
||||||
|
|
||||||
|
export type { ResolvedInvitationData } from "@xo-cash/engine";
|
||||||
|
|
||||||
export type InvitationEventMap = {
|
export type InvitationEventMap = {
|
||||||
"invitation-updated": XOInvitation;
|
"invitation-updated": XOInvitation;
|
||||||
"invitation-status-changed": string;
|
"invitation-status-changed": string;
|
||||||
|
"invitation-removed": void;
|
||||||
error: Error;
|
error: Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,11 +109,33 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create the invitation
|
// Create the invitation
|
||||||
const invitationInstance = new Invitation(engineInvitation, dependencies);
|
const invitationInstance = new Invitation(
|
||||||
|
engineInvitation,
|
||||||
|
dependencies,
|
||||||
|
template,
|
||||||
|
);
|
||||||
|
|
||||||
return invitationInstance;
|
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.
|
* The invitation data.
|
||||||
*/
|
*/
|
||||||
@@ -145,14 +173,19 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
/**
|
/**
|
||||||
* Create an invitation and start the SSE Session required for it.
|
* Create an invitation and start the SSE Session required for it.
|
||||||
*/
|
*/
|
||||||
constructor(invitation: XOInvitation, dependencies: InvitationDependencies) {
|
constructor(
|
||||||
|
invitation: XOInvitation,
|
||||||
|
dependencies: InvitationDependencies,
|
||||||
|
template: XOTemplate,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.data = invitation;
|
this.template = template;
|
||||||
this.engine = dependencies.engine;
|
this.engine = dependencies.engine;
|
||||||
this.syncServer = dependencies.syncServer;
|
this.syncServer = dependencies.syncServer;
|
||||||
this.storage = dependencies.storage;
|
this.storage = dependencies.storage;
|
||||||
this.electrum = dependencies.electrum;
|
this.electrum = dependencies.electrum;
|
||||||
|
this.updateInvitationData(invitation);
|
||||||
|
|
||||||
// Apply SSE updates serially so each engine update sees the latest history.
|
// Apply SSE updates serially so each engine update sees the latest history.
|
||||||
this.syncServer.on("message", (event) => {
|
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> {
|
private enqueueSyncUpdate(update: () => Promise<void>): Promise<void> {
|
||||||
const queuedUpdate = this.sseUpdateQueue.then(update);
|
const queuedUpdate = this.sseUpdateQueue.then(update);
|
||||||
this.sseUpdateQueue = queuedUpdate.catch(() => {});
|
this.sseUpdateQueue = queuedUpdate.catch(() => {});
|
||||||
@@ -197,19 +238,21 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Prefer keeping the engine's local invitation state in sync.
|
// Prefer keeping the engine's local invitation state in sync.
|
||||||
this.data = stripLocalInvitationMetadata(
|
this.updateInvitationData(
|
||||||
|
stripLocalInvitationMetadata(
|
||||||
await this.engine.updateInvitation({
|
await this.engine.updateInvitation({
|
||||||
...this.data,
|
...this.data,
|
||||||
...invitation,
|
...invitation,
|
||||||
commits: combinedCommits,
|
commits: combinedCommits,
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.emit(
|
this.emit(
|
||||||
"error",
|
"error",
|
||||||
error instanceof Error ? error : new Error(String(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);
|
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);
|
const newCommits = this.mergeCommits(this.data.commits, invitation.commits);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.data = stripLocalInvitationMetadata(
|
this.updateInvitationData(
|
||||||
|
stripLocalInvitationMetadata(
|
||||||
await this.engine.updateInvitation({
|
await this.engine.updateInvitation({
|
||||||
...this.data,
|
...this.data,
|
||||||
...invitation,
|
...invitation,
|
||||||
commits: newCommits,
|
commits: newCommits,
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.emit(
|
this.emit(
|
||||||
"error",
|
"error",
|
||||||
error instanceof Error ? error : new Error(String(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);
|
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> {
|
async accept(acceptParams?: InvitationParameters): Promise<void> {
|
||||||
// Accept the invitation
|
// 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
|
// Sync the invitation to the sync server
|
||||||
await this.publishInvitation(this.data);
|
await this.publishInvitation(this.data);
|
||||||
@@ -529,7 +576,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
// Store the signed invitation in the storage
|
// Store the signed invitation in the storage
|
||||||
await this.storage.set(this.data.invitationIdentifier, signedInvitation);
|
await this.storage.set(this.data.invitationIdentifier, signedInvitation);
|
||||||
|
|
||||||
this.data = signedInvitation;
|
this.updateInvitationData(signedInvitation);
|
||||||
|
|
||||||
// Update the status of the invitation
|
// Update the status of the invitation
|
||||||
await this.updateStatus();
|
await this.updateStatus();
|
||||||
@@ -563,9 +610,8 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
await this.ensureAccepted();
|
await this.ensureAccepted();
|
||||||
|
|
||||||
// Append the commit to the invitation
|
// Append the commit to the invitation
|
||||||
this.data = await this.engine.appendInvitation(
|
this.updateInvitationData(
|
||||||
this.data.invitationIdentifier,
|
await this.engine.appendInvitation(this.data.invitationIdentifier, data),
|
||||||
data,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync the invitation to the sync server
|
// Sync the invitation to the sync server
|
||||||
@@ -839,4 +885,23 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
|
|
||||||
return totalSats;
|
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 {
|
import {
|
||||||
getInvitationState,
|
getInvitationState,
|
||||||
getStateColorName,
|
getStateColorName,
|
||||||
getInvitationInputs,
|
|
||||||
getInvitationOutputs,
|
|
||||||
getInvitationVariables,
|
|
||||||
formatInvitationListItem,
|
formatInvitationListItem,
|
||||||
formatInvitationId,
|
formatInvitationId,
|
||||||
} from '../../../utils/invitation-utils.js';
|
} from '../../../utils/invitation-utils.js';
|
||||||
|
import type { ResolvedInvitationVariable } from '@xo-cash/engine';
|
||||||
|
|
||||||
import { InvitationImportFlow } from './invitation-import/InvitationImportFlow.js';
|
import { InvitationImportFlow } from './invitation-import/InvitationImportFlow.js';
|
||||||
import { compileCashAssemblyString } from '@xo-cash/engine';
|
import { compileCashAssemblyString } from '@xo-cash/engine';
|
||||||
@@ -65,6 +63,7 @@ const actionItems: ListItemData<string>[] = [
|
|||||||
{ key: 'sign', label: 'Sign Transaction', value: 'sign' },
|
{ key: 'sign', label: 'Sign Transaction', value: 'sign' },
|
||||||
{ key: 'broadcast', label: 'Broadcast Transaction', value: 'broadcast' },
|
{ key: 'broadcast', label: 'Broadcast Transaction', value: 'broadcast' },
|
||||||
{ key: 'copy', label: 'Copy Invitation ID', value: 'copy' },
|
{ 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]);
|
}, [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 () => {
|
const copyId = useCallback(async () => {
|
||||||
if (!selectedInvitation) {
|
if (!selectedInvitation) {
|
||||||
showError('No invitation selected');
|
showError('No invitation selected');
|
||||||
@@ -401,17 +422,12 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
setStatus('Analyzing invitation...');
|
setStatus('Analyzing invitation...');
|
||||||
|
|
||||||
let requiredAmount = 0n;
|
let requiredAmount = 0n;
|
||||||
const commits = selectedInvitation.data.commits || [];
|
for (const variable of selectedInvitation.resolvedData.variables) {
|
||||||
for (const commit of commits) {
|
if (variable.variableIdentifier.toLowerCase().includes('satoshi')) {
|
||||||
const variables = commit.data?.variables || [];
|
|
||||||
for (const variable of variables) {
|
|
||||||
if (variable.variableIdentifier?.toLowerCase().includes('satoshi')) {
|
|
||||||
requiredAmount = BigInt(variable.value?.toString() || '0');
|
requiredAmount = BigInt(variable.value?.toString() || '0');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (requiredAmount > 0n) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fee = 500n;
|
const fee = 500n;
|
||||||
const dust = 546n;
|
const dust = 546n;
|
||||||
@@ -516,6 +532,9 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
case 'broadcast':
|
case 'broadcast':
|
||||||
broadcastTransaction();
|
broadcastTransaction();
|
||||||
break;
|
break;
|
||||||
|
case 'delete':
|
||||||
|
deleteInvitation();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}, [selectedInvitation, copyId, acceptInvitation, fillRequirements, signInvitation, broadcastTransaction, navigate]);
|
}, [selectedInvitation, copyId, acceptInvitation, fillRequirements, signInvitation, broadcastTransaction, navigate]);
|
||||||
|
|
||||||
@@ -595,14 +614,17 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
|
|
||||||
const state = getInvitationState(selectedInvitation);
|
const state = getInvitationState(selectedInvitation);
|
||||||
const action = selectedTemplate?.actions?.[selectedInvitation.data.actionIdentifier];
|
const action = selectedTemplate?.actions?.[selectedInvitation.data.actionIdentifier];
|
||||||
const inputs = getInvitationInputs(selectedInvitation);
|
const { inputs, outputs, variables } = selectedInvitation.resolvedData;
|
||||||
const outputs = getInvitationOutputs(selectedInvitation);
|
|
||||||
const variables = getInvitationVariables(selectedInvitation);
|
|
||||||
const userEntityId = ownInvitationContext.entityIdentifier;
|
const userEntityId = ownInvitationContext.entityIdentifier;
|
||||||
const userRole = ownInvitationContext.roleIdentifier;
|
const userRole = ownInvitationContext.roleIdentifier;
|
||||||
const roleInfoRaw = userRole && selectedTemplate?.roles?.[userRole];
|
const roleInfoRaw = userRole && selectedTemplate?.roles?.[userRole];
|
||||||
const roleInfo = roleInfoRaw && typeof roleInfoRaw === 'object' ? roleInfoRaw : null;
|
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 getFiatSuffix = (satoshis: bigint): string => {
|
||||||
const fiatValue = formatSatoshisToFiat(satoshis);
|
const fiatValue = formatSatoshisToFiat(satoshis);
|
||||||
return fiatValue ? ` (~${fiatValue})` : '';
|
return fiatValue ? ` (~${fiatValue})` : '';
|
||||||
@@ -625,11 +647,10 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSatoshisVariable = (variableIdentifier: string): boolean => {
|
const isSatoshisVariable = (variable: ResolvedInvitationVariable): boolean => {
|
||||||
const templateVariable = selectedTemplate?.variables?.[variableIdentifier];
|
const templateHint = variable.hint?.toLowerCase();
|
||||||
const templateType = templateVariable?.type?.toLowerCase();
|
const templateType = variable.type?.toLowerCase();
|
||||||
const templateHint = templateVariable?.hint?.toLowerCase();
|
const identifier = variable.variableIdentifier.toLowerCase();
|
||||||
const identifier = variableIdentifier.toLowerCase();
|
|
||||||
|
|
||||||
if (templateHint?.includes('satoshi')) {
|
if (templateHint?.includes('satoshi')) {
|
||||||
return true;
|
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 (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{/* Type & Status */}
|
{/* Type & Status */}
|
||||||
@@ -693,28 +728,21 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
) : (
|
) : (
|
||||||
inputs.map((input, idx) => {
|
inputs.map((input, idx) => {
|
||||||
const isUserInput = input.entityIdentifier === userEntityId;
|
const isUserInput = input.entityIdentifier === userEntityId;
|
||||||
const inputTemplate = selectedTemplate?.inputs?.[input.inputIdentifier ?? ''];
|
|
||||||
const inputSatoshis = (
|
const inputSatoshis = (
|
||||||
'valueSatoshis' in input && input.valueSatoshis !== undefined
|
'valueSatoshis' in input && input.valueSatoshis !== undefined
|
||||||
)
|
)
|
||||||
? parseNumberishToBigInt(input.valueSatoshis)
|
? parseNumberishToBigInt(input.valueSatoshis)
|
||||||
: null;
|
: null;
|
||||||
|
const inputDescription = compileResolvedDescription(input.description);
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={`input-${idx}`}
|
key={`input-${idx}`}
|
||||||
color={isUserInput ? colors.success : colors.text}
|
color={isUserInput ? colors.success : colors.text}
|
||||||
>
|
>
|
||||||
{/* Indicator for whether this is the user's input */}
|
|
||||||
{' '}{isUserInput ? '• ' : '○ '}
|
{' '}{isUserInput ? '• ' : '○ '}
|
||||||
|
{input.name ?? input.inputIdentifier ?? `Input ${idx}`}
|
||||||
{/* TODO: Why doesnt this stuff work? It just cant resolve inputs? */}
|
|
||||||
{/* Input name */}
|
|
||||||
{inputTemplate?.name ?? input.inputIdentifier ?? `Input ${idx}`}
|
|
||||||
|
|
||||||
{/* Input role */}
|
|
||||||
{input.roleIdentifier && ` (${input.roleIdentifier})`}
|
{input.roleIdentifier && ` (${input.roleIdentifier})`}
|
||||||
|
{inputDescription && ` - ${inputDescription}`}
|
||||||
{/* Input value */}
|
|
||||||
{inputSatoshis !== null && ` ${formatSatoshis(inputSatoshis)}${getFiatSuffix(inputSatoshis)}`}
|
{inputSatoshis !== null && ` ${formatSatoshis(inputSatoshis)}${getFiatSuffix(inputSatoshis)}`}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -729,33 +757,18 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
) : (
|
) : (
|
||||||
outputs.map((output, idx) => {
|
outputs.map((output, idx) => {
|
||||||
const isUserOutput = output.entityIdentifier === userEntityId;
|
const isUserOutput = output.entityIdentifier === userEntityId;
|
||||||
const outputTemplate = selectedTemplate?.outputs?.[output.outputIdentifier ?? ''];
|
|
||||||
const outputSatoshis = output.valueSatoshis !== undefined
|
const outputSatoshis = output.valueSatoshis !== undefined
|
||||||
? parseNumberishToBigInt(output.valueSatoshis)
|
? parseNumberishToBigInt(output.valueSatoshis)
|
||||||
: null;
|
: null;
|
||||||
|
const outputDescription = compileResolvedDescription(output.description);
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
key={`output-${idx}`}
|
key={`output-${idx}`}
|
||||||
color={isUserOutput ? colors.success : colors.text}
|
color={isUserOutput ? colors.success : colors.text}
|
||||||
>
|
>
|
||||||
{/* Indicator for whether this is the user's output */}
|
|
||||||
{' '}{isUserOutput ? '• ' : '○ '}
|
{' '}{isUserOutput ? '• ' : '○ '}
|
||||||
|
{output.name ?? output.outputIdentifier ?? `Output ${idx}`}
|
||||||
{/* Output name */}
|
{outputDescription && ` - ${outputDescription}`}
|
||||||
{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 */}
|
|
||||||
{outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)}${getFiatSuffix(outputSatoshis)})`}
|
{outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)}${getFiatSuffix(outputSatoshis)})`}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -772,11 +785,10 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
) : (
|
) : (
|
||||||
variables.map((variable, idx) => {
|
variables.map((variable, idx) => {
|
||||||
const isUserVariable = variable.entityIdentifier === userEntityId;
|
const isUserVariable = variable.entityIdentifier === userEntityId;
|
||||||
const varTemplate = selectedTemplate?.variables?.[variable.variableIdentifier];
|
|
||||||
const displayValue = typeof variable.value === 'bigint'
|
const displayValue = typeof variable.value === 'bigint'
|
||||||
? variable.value.toString()
|
? variable.value.toString()
|
||||||
: String(variable.value);
|
: String(variable.value);
|
||||||
const parsedVariableSatoshis = isSatoshisVariable(variable.variableIdentifier)
|
const parsedVariableSatoshis = isSatoshisVariable(variable)
|
||||||
? parseNumberishToBigInt(variable.value)
|
? parseNumberishToBigInt(variable.value)
|
||||||
: null;
|
: null;
|
||||||
return (
|
return (
|
||||||
@@ -785,11 +797,11 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
color={isUserVariable ? colors.success : colors.text}
|
color={isUserVariable ? colors.success : colors.text}
|
||||||
>
|
>
|
||||||
{' '}{isUserVariable ? '• ' : '○ '}
|
{' '}{isUserVariable ? '• ' : '○ '}
|
||||||
{varTemplate?.name ?? variable.variableIdentifier}: {displayValue}
|
{variable.name ?? variable.variableIdentifier}: {displayValue}
|
||||||
{parsedVariableSatoshis !== null &&
|
{parsedVariableSatoshis !== null &&
|
||||||
` (${formatSatoshis(parsedVariableSatoshis)}${getFiatSuffix(parsedVariableSatoshis)})`}
|
` (${formatSatoshis(parsedVariableSatoshis)}${getFiatSuffix(parsedVariableSatoshis)})`}
|
||||||
{varTemplate?.description && (
|
{variable.description && (
|
||||||
<Text color={colors.textMuted} dimColor> - {varTemplate.description}</Text>
|
<Text color={colors.textMuted} dimColor> - {variable.description}</Text>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,12 +14,29 @@ import { useLayeredInput } from '../../../../hooks/useInputLayer.js';
|
|||||||
import {
|
import {
|
||||||
getInvitationState,
|
getInvitationState,
|
||||||
getStateColorName,
|
getStateColorName,
|
||||||
getInvitationInputs,
|
|
||||||
getInvitationOutputs,
|
|
||||||
getInvitationVariables,
|
|
||||||
} from '../../../../../utils/invitation-utils.js';
|
} from '../../../../../utils/invitation-utils.js';
|
||||||
import type { PreviewStepProps } from '../types.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.
|
* Map a semantic color name to an actual theme color value.
|
||||||
*/
|
*/
|
||||||
@@ -51,16 +68,18 @@ export function PreviewInvitationStep({
|
|||||||
|
|
||||||
const state = getInvitationState(invitation);
|
const state = getInvitationState(invitation);
|
||||||
const action = template?.actions?.[invitation.data.actionIdentifier];
|
const action = template?.actions?.[invitation.data.actionIdentifier];
|
||||||
const inputs = getInvitationInputs(invitation);
|
const { inputs, outputs, variables } = invitation.resolvedData;
|
||||||
const outputs = getInvitationOutputs(invitation);
|
|
||||||
const variables = getInvitationVariables(invitation);
|
|
||||||
|
|
||||||
// Collect role identifiers that appear across all commits
|
// Collect role identifiers that appear across resolved invitation data
|
||||||
const filledRoles = new Set<string>();
|
const filledRoles = new Set<string>();
|
||||||
for (const commit of invitation.data.commits ?? []) {
|
for (const input of inputs) {
|
||||||
for (const input of commit.data?.inputs ?? []) {
|
|
||||||
if (input.roleIdentifier) filledRoles.add(input.roleIdentifier);
|
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 (
|
return (
|
||||||
@@ -143,11 +162,10 @@ export function PreviewInvitationStep({
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
inputs.map((input, idx) => {
|
inputs.map((input, idx) => {
|
||||||
const inputTemplate = template?.inputs?.[input.inputIdentifier ?? ''];
|
|
||||||
return (
|
return (
|
||||||
<Box key={`input-${idx}`}>
|
<Box key={`input-${idx}`}>
|
||||||
<Text color={colors.text}>
|
<Text color={colors.text}>
|
||||||
{' '}• {inputTemplate?.name ?? input.inputIdentifier ?? `Input ${idx}`}
|
{' '}• {input.name ?? input.inputIdentifier ?? `Input ${idx}`}
|
||||||
{input.roleIdentifier && ` (${input.roleIdentifier})`}
|
{input.roleIdentifier && ` (${input.roleIdentifier})`}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -170,15 +188,17 @@ export function PreviewInvitationStep({
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
outputs.map((output, idx) => {
|
outputs.map((output, idx) => {
|
||||||
const outputTemplate = template?.outputs?.[output.outputIdentifier ?? ''];
|
|
||||||
const fiatValue = output.valueSatoshis !== undefined
|
const fiatValue = output.valueSatoshis !== undefined
|
||||||
? formatSatoshisToFiat(output.valueSatoshis)
|
? formatSatoshisToFiat(output.valueSatoshis)
|
||||||
: null;
|
: null;
|
||||||
|
const outputSatoshis = output.valueSatoshis !== undefined
|
||||||
|
? parseNumberishToBigInt(output.valueSatoshis)
|
||||||
|
: null;
|
||||||
return (
|
return (
|
||||||
<Box key={`output-${idx}`}>
|
<Box key={`output-${idx}`}>
|
||||||
<Text color={colors.text}>
|
<Text color={colors.text}>
|
||||||
{' '}• {outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`}
|
{' '}• {output.name ?? output.outputIdentifier ?? `Output ${idx}`}
|
||||||
{output.valueSatoshis !== undefined && ` (${formatSatoshis(output.valueSatoshis)})`}
|
{outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)})`}
|
||||||
{fiatValue && ` (~${fiatValue})`}
|
{fiatValue && ` (~${fiatValue})`}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -201,14 +221,13 @@ export function PreviewInvitationStep({
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
variables.map((variable, idx) => {
|
variables.map((variable, idx) => {
|
||||||
const varTemplate = template?.variables?.[variable.variableIdentifier];
|
|
||||||
const displayValue = typeof variable.value === 'bigint'
|
const displayValue = typeof variable.value === 'bigint'
|
||||||
? variable.value.toString()
|
? variable.value.toString()
|
||||||
: String(variable.value);
|
: String(variable.value);
|
||||||
return (
|
return (
|
||||||
<Box key={`var-${idx}`}>
|
<Box key={`var-${idx}`}>
|
||||||
<Text color={colors.text}>
|
<Text color={colors.text}>
|
||||||
{' '}• {varTemplate?.name ?? variable.variableIdentifier}: {displayValue}
|
{' '}• {variable.name ?? variable.variableIdentifier}: {displayValue}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user