Breaking Change: Update to latest XO-Engine #2
@@ -7,8 +7,11 @@
|
|||||||
* (names, descriptions, icons, roles, etc.).
|
* (names, descriptions, icons, roles, etc.).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { mergeInvitationCommits } from "@xo-cash/engine";
|
||||||
|
import { binToHex } from "@bitauth/libauth";
|
||||||
import type {
|
import type {
|
||||||
XOInvitation,
|
XOInvitation,
|
||||||
|
XOInvitationCommit,
|
||||||
XOInvitationInput,
|
XOInvitationInput,
|
||||||
XOInvitationOutput,
|
XOInvitationOutput,
|
||||||
XOInvitationVariable,
|
XOInvitationVariable,
|
||||||
@@ -249,12 +252,149 @@ function resolveOutput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns flattened, template-enriched invitation data for UI display.
|
* Converts hex or binary invitation bytecode fields to hex strings for display.
|
||||||
|
*/
|
||||||
|
function hexOrBinToHex(
|
||||||
|
value: string | Uint8Array | undefined,
|
||||||
|
): string | undefined {
|
||||||
|
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).
|
||||||
|
*/
|
||||||
|
function normalizeMergedInputForDisplay(input: XOInvitationInput): XOInvitationInput {
|
||||||
|
const normalized: XOInvitationInput = { ...input };
|
||||||
|
|
||||||
|
if (input.outpointTransactionHash !== undefined) {
|
||||||
|
normalized.outpointTransactionHash = hexOrBinToHex(
|
||||||
|
input.outpointTransactionHash,
|
||||||
|
) as XOInvitationInput["outpointTransactionHash"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.unlockingBytecode !== undefined) {
|
||||||
|
const isPlaceholder =
|
||||||
|
input.unlockingBytecode instanceof Uint8Array &&
|
||||||
|
input.unlockingBytecode.length === 0;
|
||||||
|
|
||||||
|
if (isPlaceholder) {
|
||||||
|
delete normalized.unlockingBytecode;
|
||||||
|
} else {
|
||||||
|
normalized.unlockingBytecode = hexOrBinToHex(
|
||||||
|
input.unlockingBytecode,
|
||||||
|
) as XOInvitationInput["unlockingBytecode"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.sequenceNumber === 0) {
|
||||||
|
delete normalized.sequenceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a merged output row for UI display (hex strings).
|
||||||
|
*/
|
||||||
|
function normalizeMergedOutputForDisplay(
|
||||||
|
output: XOInvitationOutput,
|
||||||
|
): XOInvitationOutput {
|
||||||
|
const normalized: XOInvitationOutput = { ...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.
|
||||||
|
*/
|
||||||
|
function findOutputIdentifierForMergedOutput(
|
||||||
|
commit: XOInvitationCommit | undefined,
|
||||||
|
mergedOutput: XOInvitationOutput,
|
||||||
|
): string | undefined {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (
|
||||||
|
mergedBytecodeHex !== undefined &&
|
||||||
|
commitBytecodeHex !== undefined &&
|
||||||
|
mergedBytecodeHex === commitBytecodeHex
|
||||||
|
) {
|
||||||
|
return commitOutput.outputIdentifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputsWithIdentifier = outputs.filter(
|
||||||
|
(commitOutput) => commitOutput.outputIdentifier !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (outputsWithIdentifier.length === 1) {
|
||||||
|
const soleIdentifiedOutput = outputsWithIdentifier[0];
|
||||||
|
return soleIdentifiedOutput?.outputIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether two invitation variable rows refer to the same template variable slot.
|
||||||
|
*/
|
||||||
|
function matchesInvitationVariable(
|
||||||
|
left: XOInvitationVariable,
|
||||||
|
right: XOInvitationVariable,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
function findVariableEntityIdentifier(
|
||||||
|
variable: XOInvitationVariable,
|
||||||
|
commits: XOInvitationCommit[],
|
||||||
|
): string {
|
||||||
|
let entityIdentifier = "";
|
||||||
|
|
||||||
|
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.
|
||||||
*
|
*
|
||||||
* Commits are walked in order; variables, inputs, and outputs are collected
|
* Uses {@link mergeInvitationCommits} for inputs and outputs so `mergesWith`
|
||||||
* into top-level arrays with `entityIdentifier` and template metadata attached.
|
* extensions and transaction indices are resolved. Variables come from the merged
|
||||||
* Items without a template identifier (e.g. ad-hoc change outputs) keep only
|
* result and are enriched with template metadata. Commit ordering is delegated to
|
||||||
* their committed fields.
|
* the engine merger.
|
||||||
*
|
*
|
||||||
* @param invitation - The raw invitation in standard XO format.
|
* @param invitation - The raw invitation in standard XO format.
|
||||||
* @param template - The template referenced by the invitation.
|
* @param template - The template referenced by the invitation.
|
||||||
@@ -264,26 +404,67 @@ export function resolveCommitReferences(
|
|||||||
invitation: XOInvitation,
|
invitation: XOInvitation,
|
||||||
template: XOTemplate,
|
template: XOTemplate,
|
||||||
): ResolvedInvitationData {
|
): ResolvedInvitationData {
|
||||||
const variables: ResolvedInvitationVariable[] = [];
|
const commits = invitation.commits ?? [];
|
||||||
const inputs: ResolvedInvitationInput[] = [];
|
const commitsMap = new Map(
|
||||||
const outputs: ResolvedInvitationOutput[] = [];
|
commits.map((commit) => [commit.commitIdentifier, commit]),
|
||||||
|
);
|
||||||
|
|
||||||
for (const commit of invitation.commits ?? []) {
|
const merged = mergeInvitationCommits(
|
||||||
for (const variable of commit.data?.variables ?? []) {
|
invitation as Parameters<typeof mergeInvitationCommits>[0],
|
||||||
variables.push(
|
template,
|
||||||
resolveVariable(variable, commit.entityIdentifier, template),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const input of commit.data?.inputs ?? []) {
|
if (merged === null) {
|
||||||
inputs.push(resolveInput(input, commit.entityIdentifier, template));
|
return {
|
||||||
}
|
invitationIdentifier: invitation.invitationIdentifier,
|
||||||
|
templateIdentifier: invitation.templateIdentifier,
|
||||||
for (const output of commit.data?.outputs ?? []) {
|
actionIdentifier: invitation.actionIdentifier,
|
||||||
outputs.push(resolveOutput(output, commit.entityIdentifier, template));
|
variables: [],
|
||||||
}
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variables = merged.variables.map((variable) =>
|
||||||
|
resolveVariable(
|
||||||
|
variable,
|
||||||
|
findVariableEntityIdentifier(variable, commits),
|
||||||
|
template,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputs = merged.inputs.map((mergedInput) => {
|
||||||
|
const commit = commitsMap.get(mergedInput.sourceCommitIdentifier);
|
||||||
|
const entityIdentifier = commit?.entityIdentifier ?? "";
|
||||||
|
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);
|
||||||
|
const outputForDisplay = normalizeMergedOutputForDisplay(
|
||||||
|
outputIdentifier !== undefined ? { ...output, outputIdentifier } : output,
|
||||||
|
);
|
||||||
|
|
||||||
|
return resolveOutput(outputForDisplay, entityIdentifier, template);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invitationIdentifier: invitation.invitationIdentifier,
|
invitationIdentifier: invitation.invitationIdentifier,
|
||||||
templateIdentifier: invitation.templateIdentifier,
|
templateIdentifier: invitation.templateIdentifier,
|
||||||
|
|||||||
@@ -123,6 +123,36 @@ const originalInvitation: XOInvitation = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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", () => {
|
describe("resolveCommitReferences", () => {
|
||||||
it("flattens commits and enriches items with template metadata", () => {
|
it("flattens commits and enriches items with template metadata", () => {
|
||||||
const resolved = resolveCommitReferences(
|
const resolved = resolveCommitReferences(
|
||||||
@@ -237,4 +267,21 @@ describe("resolveCommitReferences", () => {
|
|||||||
expect(resolved.outputs[1]).not.toHaveProperty("name");
|
expect(resolved.outputs[1]).not.toHaveProperty("name");
|
||||||
expect(resolved.outputs[1]).not.toHaveProperty("outputIdentifier");
|
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