diff --git a/src/services/invitation.ts b/src/services/invitation.ts index 7a08d7c..172182e 100644 --- a/src/services/invitation.ts +++ b/src/services/invitation.ts @@ -7,8 +7,10 @@ import { generateTemplateIdentifier, hasInvitationExpired, mergeInvitationCommits, + resolveCommitReferences, serializeInvitation, deserializeInvitation, + type ResolvedInvitationData, } from "@xo-cash/engine"; import type { XOInvitation, @@ -35,13 +37,9 @@ import type { BlockchainService } from "./electrum.js"; import { EventEmitter } from "../utils/event-emitter.js"; import { decodeExtendedJsonObject } from "../utils/ext-json.js"; -import { - resolveCommitReferences, - type ResolvedInvitationData, -} from "../utils/resolve-invitation-data.js"; 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 = { "invitation-updated": XOInvitation; @@ -613,10 +611,7 @@ export class Invitation extends EventEmitter { // Append the commit to the invitation this.updateInvitationData( - await this.engine.appendInvitation( - this.data.invitationIdentifier, - data, - ), + await this.engine.appendInvitation(this.data.invitationIdentifier, data), ); // Sync the invitation to the sync server @@ -894,13 +889,15 @@ export class Invitation extends EventEmitter { /** * 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); + await this.engine.DANGEROUS_deleteStoredInvitation( + this.data.invitationIdentifier, + ); this.emit("invitation-removed", this.data.invitationIdentifier); diff --git a/src/tui/screens/invitations/InvitationScreen.tsx b/src/tui/screens/invitations/InvitationScreen.tsx index 28e3390..ba5f380 100644 --- a/src/tui/screens/invitations/InvitationScreen.tsx +++ b/src/tui/screens/invitations/InvitationScreen.tsx @@ -29,7 +29,7 @@ import { formatInvitationListItem, formatInvitationId, } 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 { compileCashAssemblyString } from '@xo-cash/engine'; diff --git a/src/utils/resolve-invitation-data.ts b/src/utils/resolve-invitation-data.ts deleted file mode 100644 index 7c31936..0000000 --- a/src/utils/resolve-invitation-data.ts +++ /dev/null @@ -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. "$()") 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: "$()"`) 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[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, - }; -}; diff --git a/tests/utils/resolve-invitation-data.test.ts b/tests/utils/resolve-invitation-data.test.ts deleted file mode 100644 index 030a734..0000000 --- a/tests/utils/resolve-invitation-data.test.ts +++ /dev/null @@ -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: "$() sats to $()", - icon: "request", - roles: { - merchant: { - name: "Payment Received", - description: - "Received $() sats for $()", - }, - customer: { - name: "Payment Sent", - description: - "Sent $() sats for $()", - }, - }, - lockingScript: "merchantReceivingLockingScript", - valueSatoshis: "$()", - 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", - }); - }); -});