Use engine-version of resolveInivitationReferences
This commit is contained in:
@@ -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,
|
||||||
@@ -35,13 +37,9 @@ import type { BlockchainService } from "./electrum.js";
|
|||||||
|
|
||||||
import { EventEmitter } from "../utils/event-emitter.js";
|
import { EventEmitter } from "../utils/event-emitter.js";
|
||||||
import { decodeExtendedJsonObject } from "../utils/ext-json.js";
|
import { decodeExtendedJsonObject } from "../utils/ext-json.js";
|
||||||
import {
|
|
||||||
resolveCommitReferences,
|
|
||||||
type ResolvedInvitationData,
|
|
||||||
} from "../utils/resolve-invitation-data.js";
|
|
||||||
import { compileCashAssemblyString } from "@xo-cash/engine";
|
import { compileCashAssemblyString } from "@xo-cash/engine";
|
||||||
|
|
||||||
export type { ResolvedInvitationData } from "../utils/resolve-invitation-data.js";
|
export type { ResolvedInvitationData } from "@xo-cash/engine";
|
||||||
|
|
||||||
export type InvitationEventMap = {
|
export type InvitationEventMap = {
|
||||||
"invitation-updated": XOInvitation;
|
"invitation-updated": XOInvitation;
|
||||||
@@ -613,10 +611,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
|
|
||||||
// Append the commit to the invitation
|
// Append the commit to the invitation
|
||||||
this.updateInvitationData(
|
this.updateInvitationData(
|
||||||
await this.engine.appendInvitation(
|
await this.engine.appendInvitation(this.data.invitationIdentifier, data),
|
||||||
this.data.invitationIdentifier,
|
|
||||||
data,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sync the invitation to the sync server
|
// Sync the invitation to the sync server
|
||||||
@@ -894,13 +889,15 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
|||||||
/**
|
/**
|
||||||
* Removes the invitation from the Local SQLite db as well as the Engine's internal DB
|
* 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
|
* NOTE: This uses methods that are marked "DANGEROUSLY" inside the engine and behaviour may change
|
||||||
*/
|
*/
|
||||||
public async delete() {
|
public async delete() {
|
||||||
// Remove the invitation from our local db
|
// Remove the invitation from our local db
|
||||||
this.storage.remove(this.data.invitationIdentifier);
|
this.storage.remove(this.data.invitationIdentifier);
|
||||||
|
|
||||||
// Remove the invitation from the engine's internal db
|
// Remove the invitation from the engine's internal db
|
||||||
await this.engine.DANGEROUS_deleteStoredInvitation(this.data.invitationIdentifier);
|
await this.engine.DANGEROUS_deleteStoredInvitation(
|
||||||
|
this.data.invitationIdentifier,
|
||||||
|
);
|
||||||
|
|
||||||
this.emit("invitation-removed", this.data.invitationIdentifier);
|
this.emit("invitation-removed", this.data.invitationIdentifier);
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
formatInvitationListItem,
|
formatInvitationListItem,
|
||||||
formatInvitationId,
|
formatInvitationId,
|
||||||
} from '../../../utils/invitation-utils.js';
|
} from '../../../utils/invitation-utils.js';
|
||||||
import type { ResolvedInvitationVariable } from '../../../utils/resolve-invitation-data.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';
|
||||||
|
|||||||
@@ -1,452 +0,0 @@
|
|||||||
/**
|
|
||||||
* Transforms a raw XO invitation into a flattened, template-enriched structure
|
|
||||||
* suitable for UI display without manually resolving template references.
|
|
||||||
*
|
|
||||||
* The original invitation format is unchanged in storage and transport; this
|
|
||||||
* function produces a read model that merges commit data with template metadata
|
|
||||||
* (names, descriptions, icons, roles, etc.).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { mergeInvitationCommits } from "@xo-cash/engine";
|
|
||||||
import { binToHex } from "@bitauth/libauth";
|
|
||||||
import type {
|
|
||||||
XOInvitation,
|
|
||||||
XOInvitationCommit,
|
|
||||||
XOInvitationInput,
|
|
||||||
XOInvitationOutput,
|
|
||||||
XOInvitationVariable,
|
|
||||||
XOInvitationVariableValue,
|
|
||||||
XOTemplate,
|
|
||||||
XOTemplateInput,
|
|
||||||
XOTemplateOutput,
|
|
||||||
XOTemplateVariable,
|
|
||||||
} from "@xo-cash/types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A variable from invitation commits enriched with its template definition.
|
|
||||||
*/
|
|
||||||
export interface ResolvedInvitationVariable {
|
|
||||||
entityIdentifier: string;
|
|
||||||
variableIdentifier: string;
|
|
||||||
roleIdentifier?: string;
|
|
||||||
value: XOInvitationVariableValue;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
type?: string;
|
|
||||||
hint?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A transaction input from invitation commits enriched with its template definition.
|
|
||||||
*/
|
|
||||||
export type ResolvedInvitationInput = XOInvitationInput & {
|
|
||||||
entityIdentifier: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
icon?: string;
|
|
||||||
unlockingScript?: string;
|
|
||||||
omitChangeAmounts?: XOTemplateInput["omitChangeAmounts"];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A transaction output from invitation commits enriched with its template definition.
|
|
||||||
*/
|
|
||||||
export type ResolvedInvitationOutput = XOInvitationOutput & {
|
|
||||||
entityIdentifier: string;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
icon?: string;
|
|
||||||
roles?: Record<
|
|
||||||
string,
|
|
||||||
{ name?: string; description?: string; icon?: string }
|
|
||||||
>;
|
|
||||||
lockingScript?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flattened, template-enriched invitation data for UI consumption.
|
|
||||||
*/
|
|
||||||
export interface ResolvedInvitationData {
|
|
||||||
invitationIdentifier: string;
|
|
||||||
templateIdentifier: string;
|
|
||||||
actionIdentifier: string;
|
|
||||||
variables: ResolvedInvitationVariable[];
|
|
||||||
inputs: ResolvedInvitationInput[];
|
|
||||||
outputs: ResolvedInvitationOutput[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Picks human-readable view fields from a template definition.
|
|
||||||
*/
|
|
||||||
export const pickTemplateViewMetadata = (definition?: {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
icon?: string;
|
|
||||||
}) => {
|
|
||||||
if (!definition) return {};
|
|
||||||
|
|
||||||
// Only copy fields that are present so absent template metadata does not
|
|
||||||
// overwrite committed values when this object is spread onto a commit row.
|
|
||||||
return {
|
|
||||||
...(definition.name !== undefined && { name: definition.name }),
|
|
||||||
...(definition.description !== undefined && {
|
|
||||||
description: definition.description,
|
|
||||||
}),
|
|
||||||
...(definition.icon !== undefined && { icon: definition.icon }),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Picks variable metadata from a template variable definition.
|
|
||||||
*/
|
|
||||||
export const pickTemplateVariableMetadata = (
|
|
||||||
definition?: XOTemplateVariable,
|
|
||||||
) => {
|
|
||||||
if (!definition) return {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...pickTemplateViewMetadata(definition),
|
|
||||||
...(definition.type !== undefined && { type: definition.type }),
|
|
||||||
...(definition.hint !== undefined && { hint: definition.hint }),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Picks input metadata from a template input definition.
|
|
||||||
*/
|
|
||||||
export const pickTemplateInputMetadata = (definition?: XOTemplateInput) => {
|
|
||||||
if (!definition) return {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...pickTemplateViewMetadata(definition),
|
|
||||||
...(definition.unlockingScript !== undefined && {
|
|
||||||
unlockingScript: definition.unlockingScript,
|
|
||||||
}),
|
|
||||||
...(definition.omitChangeAmounts !== undefined && {
|
|
||||||
omitChangeAmounts: definition.omitChangeAmounts,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Picks output metadata from a template output definition.
|
|
||||||
*
|
|
||||||
* Committed output values (e.g. lockingBytecode) take precedence over template
|
|
||||||
* defaults; display-oriented fields like name, description, and template
|
|
||||||
* valueSatoshis expressions are layered on for UI rendering.
|
|
||||||
*/
|
|
||||||
export const pickTemplateOutputMetadata = (definition?: XOTemplateOutput) => {
|
|
||||||
if (!definition) return {};
|
|
||||||
|
|
||||||
const roles = definition.roles
|
|
||||||
? Object.fromEntries(
|
|
||||||
Object.entries(definition.roles).map(([roleId, roleDefinition]) => [
|
|
||||||
roleId,
|
|
||||||
pickTemplateViewMetadata(roleDefinition),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...pickTemplateViewMetadata(definition),
|
|
||||||
...(roles !== undefined && Object.keys(roles).length > 0 && { roles }),
|
|
||||||
...(definition.lockingScript !== undefined && {
|
|
||||||
lockingScript: definition.lockingScript,
|
|
||||||
}),
|
|
||||||
// Keep CashAssembly expressions (e.g. "$(<totalSatoshis>)") for UI compilation;
|
|
||||||
// committed bigint values on the output row take precedence when spread later.
|
|
||||||
...(definition.valueSatoshis !== undefined && {
|
|
||||||
valueSatoshis: definition.valueSatoshis,
|
|
||||||
}),
|
|
||||||
...(definition.token !== undefined && { token: definition.token }),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enriches a committed variable with its template definition.
|
|
||||||
*/
|
|
||||||
export const resolveVariable = (
|
|
||||||
variable: XOInvitationVariable,
|
|
||||||
entityIdentifier: string,
|
|
||||||
template: XOTemplate,
|
|
||||||
): ResolvedInvitationVariable => ({
|
|
||||||
entityIdentifier,
|
|
||||||
variableIdentifier: variable.variableIdentifier,
|
|
||||||
...(variable.roleIdentifier !== undefined && {
|
|
||||||
roleIdentifier: variable.roleIdentifier,
|
|
||||||
}),
|
|
||||||
value: variable.value,
|
|
||||||
...pickTemplateVariableMetadata(
|
|
||||||
template.variables?.[variable.variableIdentifier],
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enriches a committed input with its template definition when an identifier is present.
|
|
||||||
*/
|
|
||||||
export const resolveInput = (
|
|
||||||
input: XOInvitationInput,
|
|
||||||
entityIdentifier: string,
|
|
||||||
template: XOTemplate,
|
|
||||||
): ResolvedInvitationInput => ({
|
|
||||||
entityIdentifier,
|
|
||||||
...input,
|
|
||||||
...pickTemplateInputMetadata(
|
|
||||||
input.inputIdentifier
|
|
||||||
? template.inputs?.[input.inputIdentifier]
|
|
||||||
: undefined,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enriches a committed output with its template definition when an identifier is present.
|
|
||||||
*
|
|
||||||
* Template metadata is spread after commit fields so display expressions (e.g.
|
|
||||||
* `valueSatoshis: "$(<totalSatoshis>)"`) layer on for the UI even when the merger
|
|
||||||
* already resolved a bigint for transaction encoding.
|
|
||||||
*/
|
|
||||||
export const resolveOutput = (
|
|
||||||
output: XOInvitationOutput,
|
|
||||||
entityIdentifier: string,
|
|
||||||
template: XOTemplate,
|
|
||||||
): ResolvedInvitationOutput =>
|
|
||||||
({
|
|
||||||
entityIdentifier,
|
|
||||||
...output,
|
|
||||||
...pickTemplateOutputMetadata(
|
|
||||||
output.outputIdentifier
|
|
||||||
? template.outputs?.[output.outputIdentifier]
|
|
||||||
: undefined,
|
|
||||||
),
|
|
||||||
// Template valueSatoshis may be a CashAssembly string while XOInvitationOutput
|
|
||||||
// expects bigint — the read model intentionally allows both for display.
|
|
||||||
}) as ResolvedInvitationOutput;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts hex or binary invitation bytecode fields to hex strings for display.
|
|
||||||
*/
|
|
||||||
export const hexOrBinToHex = (value?: string | Uint8Array) => {
|
|
||||||
if (value === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeof value === "string" ? value : binToHex(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes a merged input row for UI display (hex strings, no encoding placeholders).
|
|
||||||
*
|
|
||||||
* The engine merger returns libauth-ready binary fields and fills in encoding
|
|
||||||
* defaults (empty unlocking bytecode, sequence 0) that are not useful in the TUI.
|
|
||||||
*/
|
|
||||||
export const normalizeMergedInputForDisplay = (input: XOInvitationInput) => {
|
|
||||||
const normalized = { ...input };
|
|
||||||
|
|
||||||
if (input.outpointTransactionHash !== undefined) {
|
|
||||||
normalized.outpointTransactionHash = hexOrBinToHex(
|
|
||||||
input.outpointTransactionHash,
|
|
||||||
) as XOInvitationInput["outpointTransactionHash"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.unlockingBytecode !== undefined) {
|
|
||||||
// Engine uses an empty Uint8Array as a placeholder until the input is signed.
|
|
||||||
const isPlaceholder =
|
|
||||||
input.unlockingBytecode instanceof Uint8Array &&
|
|
||||||
input.unlockingBytecode.length === 0;
|
|
||||||
|
|
||||||
if (isPlaceholder) {
|
|
||||||
delete normalized.unlockingBytecode;
|
|
||||||
} else {
|
|
||||||
normalized.unlockingBytecode = hexOrBinToHex(
|
|
||||||
input.unlockingBytecode,
|
|
||||||
) as XOInvitationInput["unlockingBytecode"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default sequence from the merger is not meaningful for display.
|
|
||||||
if (normalized.sequenceNumber === 0) {
|
|
||||||
delete normalized.sequenceNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes a merged output row for UI display (hex strings).
|
|
||||||
*/
|
|
||||||
export const normalizeMergedOutputForDisplay = (output: XOInvitationOutput) => {
|
|
||||||
const normalized = { ...output };
|
|
||||||
|
|
||||||
if (output.lockingBytecode !== undefined) {
|
|
||||||
normalized.lockingBytecode = hexOrBinToHex(
|
|
||||||
output.lockingBytecode,
|
|
||||||
) as XOInvitationOutput["lockingBytecode"];
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recovers `outputIdentifier` from the source commit because the merger strips it
|
|
||||||
* after template resolution.
|
|
||||||
*/
|
|
||||||
export const findOutputIdentifierForMergedOutput = (
|
|
||||||
commit: XOInvitationCommit | undefined,
|
|
||||||
mergedOutput: XOInvitationOutput,
|
|
||||||
) => {
|
|
||||||
const outputs = commit?.data?.outputs ?? [];
|
|
||||||
const mergedBytecodeHex = hexOrBinToHex(mergedOutput.lockingBytecode);
|
|
||||||
|
|
||||||
for (const commitOutput of outputs) {
|
|
||||||
if (commitOutput.outputIdentifier === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commitBytecodeHex = hexOrBinToHex(commitOutput.lockingBytecode);
|
|
||||||
|
|
||||||
// Match merged binary bytecode back to the committed row that carried the identifier.
|
|
||||||
if (
|
|
||||||
mergedBytecodeHex !== undefined &&
|
|
||||||
commitBytecodeHex !== undefined &&
|
|
||||||
mergedBytecodeHex === commitBytecodeHex
|
|
||||||
) {
|
|
||||||
return commitOutput.outputIdentifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back when the commit has a single identified output (common case).
|
|
||||||
const outputsWithIdentifier = outputs.filter(
|
|
||||||
(commitOutput) => commitOutput.outputIdentifier !== undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (outputsWithIdentifier.length === 1) {
|
|
||||||
return outputsWithIdentifier[0]?.outputIdentifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether two invitation variable rows refer to the same template variable slot.
|
|
||||||
*/
|
|
||||||
export const matchesInvitationVariable = (
|
|
||||||
left: XOInvitationVariable,
|
|
||||||
right: XOInvitationVariable,
|
|
||||||
) =>
|
|
||||||
left.variableIdentifier === right.variableIdentifier &&
|
|
||||||
left.roleIdentifier === right.roleIdentifier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the entity that authored a merged variable by scanning invitation commits.
|
|
||||||
* Last matching commit in array order wins. Best-effort until the engine orders
|
|
||||||
* commits internally or exposes source attribution on merged variables.
|
|
||||||
*/
|
|
||||||
export const findVariableEntityIdentifier = (
|
|
||||||
variable: XOInvitationVariable,
|
|
||||||
commits: XOInvitationCommit[],
|
|
||||||
) => {
|
|
||||||
let entityIdentifier = "";
|
|
||||||
|
|
||||||
// Merged variables do not carry sourceCommitIdentifier today; walk commits and
|
|
||||||
// let the last array match win (ordering deferred to the engine merger).
|
|
||||||
for (const commit of commits) {
|
|
||||||
for (const commitVariable of commit.data?.variables ?? []) {
|
|
||||||
if (matchesInvitationVariable(commitVariable, variable)) {
|
|
||||||
entityIdentifier = commit.entityIdentifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entityIdentifier;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns template-enriched invitation data for UI display.
|
|
||||||
*
|
|
||||||
* Uses {@link mergeInvitationCommits} for inputs and outputs so `mergesWith`
|
|
||||||
* extensions and transaction indices are resolved. Variables come from the merged
|
|
||||||
* result and are enriched with template metadata. Commit ordering is delegated to
|
|
||||||
* the engine merger.
|
|
||||||
*/
|
|
||||||
export const resolveCommitReferences = (
|
|
||||||
invitation: XOInvitation,
|
|
||||||
template: XOTemplate,
|
|
||||||
): ResolvedInvitationData => {
|
|
||||||
const commits = invitation.commits ?? [];
|
|
||||||
const commitsMap = new Map(
|
|
||||||
commits.map((commit) => [commit.commitIdentifier, commit]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Merge rather than flatten so mergesWith input extensions and transactionIndex
|
|
||||||
// ordering are handled by the engine (see signing flow in engine.append/sign).
|
|
||||||
const merged = mergeInvitationCommits(
|
|
||||||
invitation as Parameters<typeof mergeInvitationCommits>[0],
|
|
||||||
template,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (merged === null) {
|
|
||||||
return {
|
|
||||||
invitationIdentifier: invitation.invitationIdentifier,
|
|
||||||
templateIdentifier: invitation.templateIdentifier,
|
|
||||||
actionIdentifier: invitation.actionIdentifier,
|
|
||||||
variables: [],
|
|
||||||
inputs: [],
|
|
||||||
outputs: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const variables = merged.variables.map((variable) =>
|
|
||||||
resolveVariable(
|
|
||||||
variable,
|
|
||||||
findVariableEntityIdentifier(variable, commits),
|
|
||||||
template,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const inputs = merged.inputs.map((mergedInput) => {
|
|
||||||
const entityIdentifier =
|
|
||||||
commitsMap.get(mergedInput.sourceCommitIdentifier)?.entityIdentifier ??
|
|
||||||
"";
|
|
||||||
// Strip merger-only fields before normalization and template enrichment.
|
|
||||||
const {
|
|
||||||
sourceCommitIdentifier: _sourceCommitIdentifier,
|
|
||||||
mergesWith: _mergesWith,
|
|
||||||
...input
|
|
||||||
} = mergedInput;
|
|
||||||
|
|
||||||
return resolveInput(
|
|
||||||
normalizeMergedInputForDisplay(input),
|
|
||||||
entityIdentifier,
|
|
||||||
template,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const outputs = merged.outputs.map((mergedOutput) => {
|
|
||||||
const commit = commitsMap.get(mergedOutput.sourceCommitIdentifier);
|
|
||||||
const entityIdentifier = commit?.entityIdentifier ?? "";
|
|
||||||
const {
|
|
||||||
sourceCommitIdentifier: _sourceCommitIdentifier,
|
|
||||||
mergesWith: _mergesWith,
|
|
||||||
...output
|
|
||||||
} = mergedOutput;
|
|
||||||
const outputIdentifier = findOutputIdentifierForMergedOutput(
|
|
||||||
commit,
|
|
||||||
output,
|
|
||||||
);
|
|
||||||
// Re-attach outputIdentifier so pickTemplateOutputMetadata can resolve names/roles.
|
|
||||||
const outputForDisplay = normalizeMergedOutputForDisplay(
|
|
||||||
outputIdentifier !== undefined ? { ...output, outputIdentifier } : output,
|
|
||||||
);
|
|
||||||
|
|
||||||
return resolveOutput(outputForDisplay, entityIdentifier, template);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
invitationIdentifier: invitation.invitationIdentifier,
|
|
||||||
templateIdentifier: invitation.templateIdentifier,
|
|
||||||
actionIdentifier: invitation.actionIdentifier,
|
|
||||||
variables,
|
|
||||||
inputs,
|
|
||||||
outputs,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import type { XOInvitation } from "@xo-cash/types";
|
|
||||||
|
|
||||||
import { vendingMachineTemplate } from "../../src/templates/vending-machine.js";
|
|
||||||
import { resolveCommitReferences } from "../../src/utils/resolve-invitation-data.js";
|
|
||||||
|
|
||||||
const MERCHANT_ENTITY =
|
|
||||||
"xpub6EUk69HMQk83Ay3QEFWhYgvqLvT6tGTnzWK33fao2fvnDyzhbBeoSc6JbQkvnKq33bH7HjqQmZ9H29hsesC53ZgxQfGBadBZL5jmSa7kbTD";
|
|
||||||
const CUSTOMER_ENTITY =
|
|
||||||
"xpub6FHRsCb1ma6VFGZpRYZL8A3X1Gwwc8JjRcaDJR2vgirrttmdvJX5VNYceA84RDVjy1c2a2oYEwuayLDZ9gssDgU52UXDGFTDa19z5ceXfFh";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimal reproduction of OriginalInvitation.json for the vending machine flow.
|
|
||||||
*/
|
|
||||||
const originalInvitation: XOInvitation = {
|
|
||||||
invitationIdentifier: "c57b1f8f8534df28b359e323c5fbd5ba",
|
|
||||||
createdAtTimestamp: 1779488689379,
|
|
||||||
templateIdentifier:
|
|
||||||
"feadd05c6566c5eded68f321efe7150cb765fda070d027c89f285e5b42a00652",
|
|
||||||
actionIdentifier: "purchaseItems",
|
|
||||||
commits: [
|
|
||||||
{
|
|
||||||
commitIdentifier: "76b935a35ca45f1065f9c66769d1a957",
|
|
||||||
previousCommitIdentifier: undefined,
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
data: {},
|
|
||||||
signature:
|
|
||||||
"5f487c045657f3939ecfeaaacf239a7cfd44b485c2be591f5280bf0cc3a6e5fe304e8ea23311d82b2afa4f0ad7e0a6d07ec1e0b1aaee9c44097613694390966b",
|
|
||||||
expiresAtTimestamp: 1779506689379,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commitIdentifier: "cbf2d6242144f6761d0efc3bbbbf6660",
|
|
||||||
previousCommitIdentifier: "76b935a35ca45f1065f9c66769d1a957",
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
data: {
|
|
||||||
variables: [
|
|
||||||
{
|
|
||||||
variableIdentifier: "totalSatoshis",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value: 3000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variableIdentifier: "orderId",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value: "eb5a30b3-ec8c-4b81-89dd-c53371f55a0e",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variableIdentifier: "merchantName",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value: "XO Snack Machine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variableIdentifier: "receiptSummary",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value: "2× Chips",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variableIdentifier: "lineItemsJson",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value:
|
|
||||||
'[{"id":"225e37f4-14f2-4b33-86fd-763018bbfd7c","name":"Chips","quantity":2,"price":1500}]',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
signature:
|
|
||||||
"7cfc53860ec81403a79a03521a7674ee8d2a11365ee031e4f7f2e36a045bd6e2999510264b29045582a74e1190f0176950a855361f02bc67ff7877fabcf794f4",
|
|
||||||
expiresAtTimestamp: 1779506689390,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commitIdentifier: "583208aa304c0aa9841d1400efe6b6aa",
|
|
||||||
previousCommitIdentifier: "cbf2d6242144f6761d0efc3bbbbf6660",
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
data: {
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
outputIdentifier: "purchaseOutput",
|
|
||||||
lockingBytecode:
|
|
||||||
"76a9146a4715fe1cc1ce228336502f1711b06045ef361088ac",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
signature:
|
|
||||||
"d9bdd3b24fef6afd13f12da92e832672c6c1b83fb372506faeb7fa4ea0e39e3a32ad74493fbe7a393aed58bc18226431dabae09948ce371ad3f77b0219cb3831",
|
|
||||||
expiresAtTimestamp: 1779506689412,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commitIdentifier: "4f3f9a3361c8070ab589cc44248a6a80",
|
|
||||||
previousCommitIdentifier: "583208aa304c0aa9841d1400efe6b6aa",
|
|
||||||
entityIdentifier: CUSTOMER_ENTITY,
|
|
||||||
data: {},
|
|
||||||
signature:
|
|
||||||
"63be8af81622da4fccc7eb6b81c6174879fe6aa113b8dae794bd42d4d5c87ae550a18be1e6cb5edf231e774bdc7883eb5a78bd02188579dce58da0d449c43865",
|
|
||||||
expiresAtTimestamp: 1779506979194,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commitIdentifier: "d18b9a0caa638eaa1d0711f333e9c114",
|
|
||||||
previousCommitIdentifier: "4f3f9a3361c8070ab589cc44248a6a80",
|
|
||||||
entityIdentifier: CUSTOMER_ENTITY,
|
|
||||||
data: {
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
outpointTransactionHash:
|
|
||||||
"b1e8f77cdc60efac19f668fc5c7177ace42a46e2532f230979559c7190c3c80a",
|
|
||||||
outpointIndex: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
signature:
|
|
||||||
"e36942eb5f147e620659d20b7059630da871944e74fe5ffb3c4ff0298a5aedb101bc7468b19750114cbcfa56b99bd4a080453a31084f18173adcd9442fca4303",
|
|
||||||
expiresAtTimestamp: 1779507006272,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commitIdentifier: "7823f7ae7a365f87f6acdfee8896f508",
|
|
||||||
previousCommitIdentifier: "d18b9a0caa638eaa1d0711f333e9c114",
|
|
||||||
entityIdentifier: CUSTOMER_ENTITY,
|
|
||||||
data: {
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
valueSatoshis: 74881n,
|
|
||||||
lockingBytecode:
|
|
||||||
"76a9141730ca066d4b9c8d542f8c9bdce645f77697d46088ac",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
signature:
|
|
||||||
"2c1d1ed1259a2e4b1bc7187b93029e99e590a4e92ff9c39031319766b7fbcdabab9c3dc20b3d27d05eee198cbc717b9aedfbef92bd3e519c62c60e4731bd936a",
|
|
||||||
expiresAtTimestamp: 1779507008169,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customer input commit extended with unlocking bytecode via mergesWith (signing flow).
|
|
||||||
*/
|
|
||||||
const invitationWithSignedInput: XOInvitation = {
|
|
||||||
...originalInvitation,
|
|
||||||
commits: [
|
|
||||||
...originalInvitation.commits.slice(0, 5),
|
|
||||||
{
|
|
||||||
commitIdentifier: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
|
||||||
previousCommitIdentifier: "d18b9a0caa638eaa1d0711f333e9c114",
|
|
||||||
entityIdentifier: CUSTOMER_ENTITY,
|
|
||||||
data: {
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
mergesWith: {
|
|
||||||
commitIdentifier: "d18b9a0caa638eaa1d0711f333e9c114",
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
unlockingBytecode:
|
|
||||||
"41226b2be7c2890c8bbde2f79e79640e56d866843f2e822ec51c469019d13db0",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
signature:
|
|
||||||
"3045022001a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456789022100fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
|
|
||||||
expiresAtTimestamp: 1779507008000,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("resolveCommitReferences", () => {
|
|
||||||
it("flattens commits and enriches items with template metadata", () => {
|
|
||||||
const resolved = resolveCommitReferences(
|
|
||||||
originalInvitation,
|
|
||||||
vendingMachineTemplate,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(resolved).toEqual({
|
|
||||||
invitationIdentifier: "c57b1f8f8534df28b359e323c5fbd5ba",
|
|
||||||
templateIdentifier:
|
|
||||||
"feadd05c6566c5eded68f321efe7150cb765fda070d027c89f285e5b42a00652",
|
|
||||||
actionIdentifier: "purchaseItems",
|
|
||||||
variables: [
|
|
||||||
{
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
name: "Total Price",
|
|
||||||
description: "Total purchase price in satoshis",
|
|
||||||
type: "integer",
|
|
||||||
hint: "satoshis",
|
|
||||||
variableIdentifier: "totalSatoshis",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value: 3000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
name: "Order ID",
|
|
||||||
description: "Unique order identifier",
|
|
||||||
type: "string",
|
|
||||||
variableIdentifier: "orderId",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value: "eb5a30b3-ec8c-4b81-89dd-c53371f55a0e",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
name: "Merchant Name",
|
|
||||||
description: "Display name of the vending machine",
|
|
||||||
type: "string",
|
|
||||||
variableIdentifier: "merchantName",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value: "XO Snack Machine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
name: "Receipt Summary",
|
|
||||||
description: "Human-readable list of purchased items",
|
|
||||||
type: "string",
|
|
||||||
variableIdentifier: "receiptSummary",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value: "2× Chips",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
name: "Line Items",
|
|
||||||
description: "JSON-encoded line items for the purchase",
|
|
||||||
type: "string",
|
|
||||||
variableIdentifier: "lineItemsJson",
|
|
||||||
roleIdentifier: "merchant",
|
|
||||||
value:
|
|
||||||
'[{"id":"225e37f4-14f2-4b33-86fd-763018bbfd7c","name":"Chips","quantity":2,"price":1500}]',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
inputs: [
|
|
||||||
{
|
|
||||||
entityIdentifier: CUSTOMER_ENTITY,
|
|
||||||
outpointTransactionHash:
|
|
||||||
"b1e8f77cdc60efac19f668fc5c7177ace42a46e2532f230979559c7190c3c80a",
|
|
||||||
outpointIndex: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
entityIdentifier: MERCHANT_ENTITY,
|
|
||||||
outputIdentifier: "purchaseOutput",
|
|
||||||
lockingBytecode: "76a9146a4715fe1cc1ce228336502f1711b06045ef361088ac",
|
|
||||||
name: "Purchase Payment",
|
|
||||||
description: "$(<totalSatoshis>) sats to $(<merchantName>)",
|
|
||||||
icon: "request",
|
|
||||||
roles: {
|
|
||||||
merchant: {
|
|
||||||
name: "Payment Received",
|
|
||||||
description:
|
|
||||||
"Received $(<totalSatoshis>) sats for $(<receiptSummary>)",
|
|
||||||
},
|
|
||||||
customer: {
|
|
||||||
name: "Payment Sent",
|
|
||||||
description:
|
|
||||||
"Sent $(<totalSatoshis>) sats for $(<receiptSummary>)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lockingScript: "merchantReceivingLockingScript",
|
|
||||||
valueSatoshis: "$(<totalSatoshis>)",
|
|
||||||
token: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
entityIdentifier: CUSTOMER_ENTITY,
|
|
||||||
valueSatoshis: 74881n,
|
|
||||||
lockingBytecode: "76a9141730ca066d4b9c8d542f8c9bdce645f77697d46088ac",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("leaves unidentified inputs and outputs without template metadata", () => {
|
|
||||||
const resolved = resolveCommitReferences(
|
|
||||||
originalInvitation,
|
|
||||||
vendingMachineTemplate,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(resolved.inputs[0]).not.toHaveProperty("name");
|
|
||||||
expect(resolved.outputs[1]).not.toHaveProperty("name");
|
|
||||||
expect(resolved.outputs[1]).not.toHaveProperty("outputIdentifier");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("merges input extension commits via mergesWith into a single input", () => {
|
|
||||||
const resolved = resolveCommitReferences(
|
|
||||||
invitationWithSignedInput,
|
|
||||||
vendingMachineTemplate,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(resolved.inputs).toHaveLength(1);
|
|
||||||
expect(resolved.inputs[0]).toMatchObject({
|
|
||||||
entityIdentifier: CUSTOMER_ENTITY,
|
|
||||||
outpointTransactionHash:
|
|
||||||
"b1e8f77cdc60efac19f668fc5c7177ace42a46e2532f230979559c7190c3c80a",
|
|
||||||
outpointIndex: 1,
|
|
||||||
unlockingBytecode:
|
|
||||||
"41226b2be7c2890c8bbde2f79e79640e56d866843f2e822ec51c469019d13db0",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user