Format with prettier. Use screen mode for invitation import - dialog mode is broken.
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import { binToHex } from '@bitauth/libauth';
|
||||
import { compileCashAssemblyString, type Engine } from '@xo-cash/engine';
|
||||
import type { UnspentOutputData } from '@xo-cash/state';
|
||||
import type { XOInvitation, XOInvitationCommit, XOInvitationVariableValue, XOTemplate } from '@xo-cash/types';
|
||||
import type { Invitation } from './invitation.js';
|
||||
import { binToHex } from "@bitauth/libauth";
|
||||
import { compileCashAssemblyString, type Engine } from "@xo-cash/engine";
|
||||
import type { UnspentOutputData } from "@xo-cash/state";
|
||||
import type {
|
||||
XOInvitation,
|
||||
XOInvitationCommit,
|
||||
XOInvitationVariableValue,
|
||||
XOTemplate,
|
||||
} from "@xo-cash/types";
|
||||
import type { Invitation } from "./invitation.js";
|
||||
|
||||
export type HistoryEntryKind = 'invitation' | 'utxo';
|
||||
export type HistoryEntryKind = "invitation" | "utxo";
|
||||
|
||||
export interface HistoryDescriptionParts {
|
||||
template: string;
|
||||
@@ -15,7 +20,7 @@ export interface HistoryDescriptionParts {
|
||||
}
|
||||
|
||||
export interface HistoryUtxoItem {
|
||||
kind: 'utxo';
|
||||
kind: "utxo";
|
||||
id: string;
|
||||
invitationIdentifier?: string;
|
||||
templateIdentifier: string;
|
||||
@@ -26,13 +31,13 @@ export interface HistoryUtxoItem {
|
||||
};
|
||||
valueSatoshis?: bigint;
|
||||
reserved?: boolean;
|
||||
direction: 'input' | 'output' | 'standalone';
|
||||
direction: "input" | "output" | "standalone";
|
||||
description: string;
|
||||
descriptionParts: HistoryDescriptionParts;
|
||||
}
|
||||
|
||||
export interface HistoryInvitationItem {
|
||||
kind: 'invitation';
|
||||
kind: "invitation";
|
||||
id: string;
|
||||
createdAtTimestamp: number;
|
||||
templateIdentifier: string;
|
||||
@@ -65,7 +70,7 @@ interface UtxoOriginContext {
|
||||
export class HistoryService {
|
||||
constructor(
|
||||
private engine: Engine,
|
||||
private invitations: Invitation[]
|
||||
private invitations: Invitation[],
|
||||
) {}
|
||||
|
||||
async extractEntities(invitation: XOInvitation): Promise<string[]> {
|
||||
@@ -80,12 +85,15 @@ export class HistoryService {
|
||||
// Iterating through each commit, extract the entity into a Map<entityId: string, roles: string[]>.
|
||||
// While we iterate through the commits, if we see a role declaration in the commit, we save that role onto the entity's roles array.
|
||||
// After we have iterated through all the commits, we can return the Map<entityId: string, roles: string[]>.
|
||||
async matchRolesToEntities(invitation: XOInvitation, entities: string[]): Promise<Record<string, string[]>> {
|
||||
async matchRolesToEntities(
|
||||
invitation: XOInvitation,
|
||||
entities: string[],
|
||||
): Promise<Record<string, string[]>> {
|
||||
const entitiesMap = new Map<string, Set<string>>();
|
||||
for (const entity of entities) {
|
||||
entitiesMap.set(entity, new Set());
|
||||
}
|
||||
|
||||
|
||||
// First pass, we are just going to try and find roleIdentifer values in the inputs, outputs, and variables.
|
||||
// TODO: Update this once the invitations use XPubs
|
||||
for (const commit of invitation.commits) {
|
||||
@@ -125,7 +133,10 @@ export class HistoryService {
|
||||
const outpointValueSatoshis = new Map<string, bigint>();
|
||||
|
||||
for (const utxo of allUtxos) {
|
||||
const outpointKey = this.getOutpointKey(utxo.outpointTransactionHash, utxo.outpointIndex);
|
||||
const outpointKey = this.getOutpointKey(
|
||||
utxo.outpointTransactionHash,
|
||||
utxo.outpointIndex,
|
||||
);
|
||||
ownOutpoints.add(outpointKey);
|
||||
ownLockingBytecodes.add(utxo.lockingBytecode);
|
||||
outpointValueSatoshis.set(outpointKey, BigInt(utxo.valueSatoshis));
|
||||
@@ -134,8 +145,14 @@ export class HistoryService {
|
||||
const contexts = new Map<string, InvitationContext>();
|
||||
for (const invitation of this.invitations) {
|
||||
const variables = this.extractInvitationVariables(invitation.data);
|
||||
const template = await this.engine.getTemplate(invitation.data.templateIdentifier) ?? null;
|
||||
const walletEntityIdentifier = this.resolveWalletEntityIdentifier(invitation, ownOutpoints, ownLockingBytecodes);
|
||||
const template =
|
||||
(await this.engine.getTemplate(invitation.data.templateIdentifier)) ??
|
||||
null;
|
||||
const walletEntityIdentifier = this.resolveWalletEntityIdentifier(
|
||||
invitation,
|
||||
ownOutpoints,
|
||||
ownLockingBytecodes,
|
||||
);
|
||||
contexts.set(invitation.data.invitationIdentifier, {
|
||||
invitation,
|
||||
template,
|
||||
@@ -155,24 +172,32 @@ export class HistoryService {
|
||||
|
||||
for (const context of contexts.values()) {
|
||||
const invitation = context.invitation.data;
|
||||
const templateName = context.template?.name ?? 'UnknownTemplate';
|
||||
const templateName = context.template?.name ?? "UnknownTemplate";
|
||||
const invitationOutputs = this.buildWalletOutputItemsForInvitation(
|
||||
context,
|
||||
allUtxos,
|
||||
invitationByOrigin,
|
||||
usedUtxoIds,
|
||||
);
|
||||
const roles = this.deriveWalletRolesForInvitation(context, invitationOutputs);
|
||||
const roles = this.deriveWalletRolesForInvitation(
|
||||
context,
|
||||
invitationOutputs,
|
||||
);
|
||||
const invitationInputs = this.buildWalletInputItemsForInvitation(
|
||||
context,
|
||||
roles[0],
|
||||
invitationOutputs.length > 0,
|
||||
outpointValueSatoshis,
|
||||
);
|
||||
const invitationDescription = this.deriveInvitationDescription(invitation, context.template, context.variables, roles[0]);
|
||||
const invitationDescription = this.deriveInvitationDescription(
|
||||
invitation,
|
||||
context.template,
|
||||
context.variables,
|
||||
roles[0],
|
||||
);
|
||||
|
||||
invitationItems.push({
|
||||
kind: 'invitation',
|
||||
kind: "invitation",
|
||||
id: `inv-${invitation.invitationIdentifier}`,
|
||||
createdAtTimestamp: invitation.createdAtTimestamp,
|
||||
templateIdentifier: invitation.templateIdentifier,
|
||||
@@ -196,39 +221,66 @@ export class HistoryService {
|
||||
const utxoId = this.getUtxoId(utxo);
|
||||
if (usedUtxoIds.has(utxoId)) continue;
|
||||
|
||||
const template = await this.engine.getTemplate(utxo.templateIdentifier) ?? null;
|
||||
const inferredRole = this.inferRoleFromOutputIdentifier(utxo.outputIdentifier);
|
||||
const description = this.deriveUtxoDescription(utxo, template, {}, inferredRole);
|
||||
standaloneUtxos.push(this.buildUtxoHistoryItem(
|
||||
const template =
|
||||
(await this.engine.getTemplate(utxo.templateIdentifier)) ?? null;
|
||||
const inferredRole = this.inferRoleFromOutputIdentifier(
|
||||
utxo.outputIdentifier,
|
||||
);
|
||||
const description = this.deriveUtxoDescription(
|
||||
utxo,
|
||||
description,
|
||||
template?.name ?? 'UnknownTemplate',
|
||||
template,
|
||||
{},
|
||||
inferredRole,
|
||||
'standalone',
|
||||
));
|
||||
);
|
||||
standaloneUtxos.push(
|
||||
this.buildUtxoHistoryItem(
|
||||
utxo,
|
||||
description,
|
||||
template?.name ?? "UnknownTemplate",
|
||||
inferredRole,
|
||||
"standalone",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return [ ...invitationItems, ...standaloneUtxos ];
|
||||
return [...invitationItems, ...standaloneUtxos];
|
||||
}
|
||||
|
||||
private buildWalletOutputItemsForInvitation(
|
||||
context: InvitationContext,
|
||||
allUtxos: UnspentOutputData[],
|
||||
invitationByOrigin: Map<string, UtxoOriginContext>,
|
||||
usedUtxoIds: Set<string>
|
||||
usedUtxoIds: Set<string>,
|
||||
): HistoryUtxoItem[] {
|
||||
const invitationId = context.invitation.data.invitationIdentifier;
|
||||
const outputs: HistoryUtxoItem[] = [];
|
||||
|
||||
for (const utxo of allUtxos) {
|
||||
const resolvedInvitationId = this.resolveInvitationIdentifierForUtxo(utxo, invitationByOrigin);
|
||||
const resolvedInvitationId = this.resolveInvitationIdentifierForUtxo(
|
||||
utxo,
|
||||
invitationByOrigin,
|
||||
);
|
||||
if (resolvedInvitationId !== invitationId) continue;
|
||||
|
||||
const role = this.resolveRoleIdentifierForUtxo(utxo, invitationByOrigin)
|
||||
?? this.inferRoleFromOutputIdentifier(utxo.outputIdentifier)
|
||||
?? 'receiver';
|
||||
const description = this.deriveUtxoDescription(utxo, context.template, context.variables, role);
|
||||
outputs.push(this.buildUtxoHistoryItem(utxo, description, context.template?.name ?? 'UnknownTemplate', role, 'output'));
|
||||
const role =
|
||||
this.resolveRoleIdentifierForUtxo(utxo, invitationByOrigin) ??
|
||||
this.inferRoleFromOutputIdentifier(utxo.outputIdentifier) ??
|
||||
"receiver";
|
||||
const description = this.deriveUtxoDescription(
|
||||
utxo,
|
||||
context.template,
|
||||
context.variables,
|
||||
role,
|
||||
);
|
||||
outputs.push(
|
||||
this.buildUtxoHistoryItem(
|
||||
utxo,
|
||||
description,
|
||||
context.template?.name ?? "UnknownTemplate",
|
||||
role,
|
||||
"output",
|
||||
),
|
||||
);
|
||||
usedUtxoIds.add(this.getUtxoId(utxo));
|
||||
}
|
||||
|
||||
@@ -244,22 +296,40 @@ export class HistoryService {
|
||||
const invitation = context.invitation.data;
|
||||
const commits = invitation.commits ?? [];
|
||||
const commitsByEntity = context.walletEntityIdentifier
|
||||
? commits.filter((commit) => commit.entityIdentifier === context.walletEntityIdentifier)
|
||||
? commits.filter(
|
||||
(commit) =>
|
||||
commit.entityIdentifier === context.walletEntityIdentifier,
|
||||
)
|
||||
: [];
|
||||
const commitsByRole = walletRole
|
||||
? commits.filter((commit) => this.deriveCommitRoleIdentifier(commit, invitation, context.template) === walletRole)
|
||||
? commits.filter(
|
||||
(commit) =>
|
||||
this.deriveCommitRoleIdentifier(
|
||||
commit,
|
||||
invitation,
|
||||
context.template,
|
||||
) === walletRole,
|
||||
)
|
||||
: [];
|
||||
|
||||
let relevantCommits = commitsByEntity.filter((commit) => (commit.data.inputs?.length ?? 0) > 0);
|
||||
let relevantCommits = commitsByEntity.filter(
|
||||
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
||||
);
|
||||
if (relevantCommits.length === 0) {
|
||||
relevantCommits = commitsByRole.filter((commit) => (commit.data.inputs?.length ?? 0) > 0);
|
||||
relevantCommits = commitsByRole.filter(
|
||||
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
if (relevantCommits.length === 0 && walletRole === 'sender') {
|
||||
relevantCommits = commits.filter((commit) => (commit.data.inputs?.length ?? 0) > 0);
|
||||
if (relevantCommits.length === 0 && walletRole === "sender") {
|
||||
relevantCommits = commits.filter(
|
||||
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
// Sender fallback only when no wallet outputs were matched.
|
||||
if (relevantCommits.length === 0 && !hasWalletOutputs) {
|
||||
relevantCommits = commits.filter((commit) => (commit.data.inputs?.length ?? 0) > 0);
|
||||
relevantCommits = commits.filter(
|
||||
(commit) => (commit.data.inputs?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
const txDescription = this.deriveTransactionActivityDescription(
|
||||
@@ -273,19 +343,28 @@ export class HistoryService {
|
||||
for (const commit of relevantCommits) {
|
||||
for (const input of commit.data.inputs ?? []) {
|
||||
const txHash = input.outpointTransactionHash
|
||||
? (input.outpointTransactionHash instanceof Uint8Array
|
||||
? binToHex(input.outpointTransactionHash)
|
||||
: String(input.outpointTransactionHash))
|
||||
: 'unknown-tx';
|
||||
? input.outpointTransactionHash instanceof Uint8Array
|
||||
? binToHex(input.outpointTransactionHash)
|
||||
: String(input.outpointTransactionHash)
|
||||
: "unknown-tx";
|
||||
const inputIndex = input.outpointIndex ?? -1;
|
||||
const inputIdentifier = input.inputIdentifier ?? 'input';
|
||||
const inputDescription = this.deriveInputDescription(inputIdentifier, context.template, context.variables);
|
||||
const templateName = context.template?.name ?? 'UnknownTemplate';
|
||||
const role = walletRole ?? 'sender';
|
||||
const inputValue = this.resolveInputSatoshis(txHash, inputIndex, outpointValueSatoshis, context.variables);
|
||||
const inputIdentifier = input.inputIdentifier ?? "input";
|
||||
const inputDescription = this.deriveInputDescription(
|
||||
inputIdentifier,
|
||||
context.template,
|
||||
context.variables,
|
||||
);
|
||||
const templateName = context.template?.name ?? "UnknownTemplate";
|
||||
const role = walletRole ?? "sender";
|
||||
const inputValue = this.resolveInputSatoshis(
|
||||
txHash,
|
||||
inputIndex,
|
||||
outpointValueSatoshis,
|
||||
context.variables,
|
||||
);
|
||||
|
||||
inputs.push({
|
||||
kind: 'utxo',
|
||||
kind: "utxo",
|
||||
id: `input-${invitation.invitationIdentifier}-${commit.commitIdentifier}-${txHash}:${inputIndex}-${inputIdentifier}`,
|
||||
invitationIdentifier: invitation.invitationIdentifier,
|
||||
templateIdentifier: invitation.templateIdentifier,
|
||||
@@ -294,7 +373,7 @@ export class HistoryService {
|
||||
txid: txHash,
|
||||
index: inputIndex,
|
||||
},
|
||||
direction: 'input',
|
||||
direction: "input",
|
||||
valueSatoshis: inputValue,
|
||||
description: `${txDescription} - ${inputDescription}`,
|
||||
descriptionParts: {
|
||||
@@ -302,7 +381,8 @@ export class HistoryService {
|
||||
role,
|
||||
outputIdentifier: inputIdentifier,
|
||||
description: `${txDescription} - ${inputDescription}`,
|
||||
valueSatoshis: inputValue !== undefined ? Number(inputValue) : undefined,
|
||||
valueSatoshis:
|
||||
inputValue !== undefined ? Number(inputValue) : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -316,10 +396,10 @@ export class HistoryService {
|
||||
description: string,
|
||||
templateName: string,
|
||||
roleIdentifier: string | undefined,
|
||||
direction: HistoryUtxoItem['direction']
|
||||
direction: HistoryUtxoItem["direction"],
|
||||
): HistoryUtxoItem {
|
||||
return {
|
||||
kind: 'utxo',
|
||||
kind: "utxo",
|
||||
id: this.getUtxoId(utxo),
|
||||
invitationIdentifier: utxo.invitationIdentifier || undefined,
|
||||
templateIdentifier: utxo.templateIdentifier,
|
||||
@@ -334,7 +414,7 @@ export class HistoryService {
|
||||
description,
|
||||
descriptionParts: {
|
||||
template: templateName,
|
||||
role: roleIdentifier ?? 'unknown',
|
||||
role: roleIdentifier ?? "unknown",
|
||||
outputIdentifier: utxo.outputIdentifier,
|
||||
description,
|
||||
valueSatoshis: utxo.valueSatoshis,
|
||||
@@ -351,54 +431,80 @@ export class HistoryService {
|
||||
*/
|
||||
private deriveWalletRolesForInvitation(
|
||||
context: InvitationContext,
|
||||
outputs: HistoryUtxoItem[]
|
||||
outputs: HistoryUtxoItem[],
|
||||
): string[] {
|
||||
const roles = new Set<string>();
|
||||
for (const output of outputs) {
|
||||
const outputRole = output.descriptionParts.role;
|
||||
if (outputRole && outputRole !== 'unknown') {
|
||||
if (outputRole && outputRole !== "unknown") {
|
||||
roles.add(outputRole);
|
||||
}
|
||||
}
|
||||
if (roles.size === 0 && outputs.length > 0) {
|
||||
roles.add('receiver');
|
||||
roles.add("receiver");
|
||||
}
|
||||
|
||||
const hasInputCommit = (context.walletEntityIdentifier
|
||||
? context.invitation.data.commits.filter((c) => c.entityIdentifier === context.walletEntityIdentifier)
|
||||
: context.invitation.data.commits
|
||||
const hasInputCommit = (
|
||||
context.walletEntityIdentifier
|
||||
? context.invitation.data.commits.filter(
|
||||
(c) => c.entityIdentifier === context.walletEntityIdentifier,
|
||||
)
|
||||
: context.invitation.data.commits
|
||||
).some((c) => (c.data.inputs?.length ?? 0) > 0);
|
||||
|
||||
if (hasInputCommit) roles.add('sender');
|
||||
if (!hasInputCommit && outputs.length === 0 && context.invitation.data.commits.some((c) => (c.data.inputs?.length ?? 0) > 0)) {
|
||||
roles.add('sender');
|
||||
if (hasInputCommit) roles.add("sender");
|
||||
if (
|
||||
!hasInputCommit &&
|
||||
outputs.length === 0 &&
|
||||
context.invitation.data.commits.some(
|
||||
(c) => (c.data.inputs?.length ?? 0) > 0,
|
||||
)
|
||||
) {
|
||||
roles.add("sender");
|
||||
}
|
||||
if (roles.size === 0) {
|
||||
const inferred = this.extractInvitationRoleIdentifier(context.invitation.data, context.template, context.walletEntityIdentifier);
|
||||
const inferred = this.extractInvitationRoleIdentifier(
|
||||
context.invitation.data,
|
||||
context.template,
|
||||
context.walletEntityIdentifier,
|
||||
);
|
||||
if (inferred) roles.add(inferred);
|
||||
}
|
||||
|
||||
return roles.size > 0 ? Array.from(roles) : [ 'unknown' ];
|
||||
return roles.size > 0 ? Array.from(roles) : ["unknown"];
|
||||
}
|
||||
|
||||
private extractInvitationVariables(invitation: XOInvitation): Record<string, XOInvitationVariableValue> {
|
||||
const committedVariables = invitation.commits.flatMap(c => c.data.variables ?? []);
|
||||
return committedVariables.reduce((acc, variable) => {
|
||||
if (!variable.variableIdentifier) return acc;
|
||||
acc[variable.variableIdentifier] = variable.value;
|
||||
return acc;
|
||||
}, {} as Record<string, XOInvitationVariableValue>);
|
||||
private extractInvitationVariables(
|
||||
invitation: XOInvitation,
|
||||
): Record<string, XOInvitationVariableValue> {
|
||||
const committedVariables = invitation.commits.flatMap(
|
||||
(c) => c.data.variables ?? [],
|
||||
);
|
||||
return committedVariables.reduce(
|
||||
(acc, variable) => {
|
||||
if (!variable.variableIdentifier) return acc;
|
||||
acc[variable.variableIdentifier] = variable.value;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, XOInvitationVariableValue>,
|
||||
);
|
||||
}
|
||||
|
||||
private indexInvitationOutputsByUtxoOrigin(
|
||||
invitationByUtxoOrigin: Map<string, UtxoOriginContext>,
|
||||
invitation: Invitation
|
||||
invitation: Invitation,
|
||||
): void {
|
||||
for (const commit of invitation.data.commits) {
|
||||
for (const output of commit.data.outputs ?? []) {
|
||||
if (!output.outputIdentifier || !output.lockingBytecode) continue;
|
||||
const lockingBytecodeHex = this.toLockingBytecodeHex(output.lockingBytecode);
|
||||
const key = this.getUtxoOriginKey(invitation.data.templateIdentifier, output.outputIdentifier, lockingBytecodeHex);
|
||||
const lockingBytecodeHex = this.toLockingBytecodeHex(
|
||||
output.lockingBytecode,
|
||||
);
|
||||
const key = this.getUtxoOriginKey(
|
||||
invitation.data.templateIdentifier,
|
||||
output.outputIdentifier,
|
||||
lockingBytecodeHex,
|
||||
);
|
||||
invitationByUtxoOrigin.set(key, {
|
||||
invitationIdentifier: invitation.data.invitationIdentifier,
|
||||
roleIdentifier: output.roleIdentifier,
|
||||
@@ -409,25 +515,33 @@ export class HistoryService {
|
||||
|
||||
private resolveInvitationIdentifierForUtxo(
|
||||
utxo: UnspentOutputData,
|
||||
invitationByUtxoOrigin: Map<string, UtxoOriginContext>
|
||||
invitationByUtxoOrigin: Map<string, UtxoOriginContext>,
|
||||
): string | undefined {
|
||||
if (utxo.invitationIdentifier) return utxo.invitationIdentifier;
|
||||
const originKey = this.getUtxoOriginKey(utxo.templateIdentifier, utxo.outputIdentifier, utxo.lockingBytecode);
|
||||
const originKey = this.getUtxoOriginKey(
|
||||
utxo.templateIdentifier,
|
||||
utxo.outputIdentifier,
|
||||
utxo.lockingBytecode,
|
||||
);
|
||||
return invitationByUtxoOrigin.get(originKey)?.invitationIdentifier;
|
||||
}
|
||||
|
||||
private resolveRoleIdentifierForUtxo(
|
||||
utxo: UnspentOutputData,
|
||||
invitationByUtxoOrigin: Map<string, UtxoOriginContext>
|
||||
invitationByUtxoOrigin: Map<string, UtxoOriginContext>,
|
||||
): string | undefined {
|
||||
const originKey = this.getUtxoOriginKey(utxo.templateIdentifier, utxo.outputIdentifier, utxo.lockingBytecode);
|
||||
const originKey = this.getUtxoOriginKey(
|
||||
utxo.templateIdentifier,
|
||||
utxo.outputIdentifier,
|
||||
utxo.lockingBytecode,
|
||||
);
|
||||
return invitationByUtxoOrigin.get(originKey)?.roleIdentifier;
|
||||
}
|
||||
|
||||
private resolveWalletEntityIdentifier(
|
||||
invitation: Invitation,
|
||||
ownUtxoOutpointKeys: Set<string>,
|
||||
ownLockingBytecodes: Set<string>
|
||||
ownLockingBytecodes: Set<string>,
|
||||
): string | undefined {
|
||||
const scores = new Map<string, number>();
|
||||
const addScore = (entityIdentifier: string, delta: number): void => {
|
||||
@@ -437,17 +551,23 @@ export class HistoryService {
|
||||
for (const commit of invitation.data.commits) {
|
||||
for (const input of commit.data.inputs ?? []) {
|
||||
const txHash = input.outpointTransactionHash
|
||||
? (input.outpointTransactionHash instanceof Uint8Array
|
||||
? binToHex(input.outpointTransactionHash)
|
||||
: String(input.outpointTransactionHash))
|
||||
? input.outpointTransactionHash instanceof Uint8Array
|
||||
? binToHex(input.outpointTransactionHash)
|
||||
: String(input.outpointTransactionHash)
|
||||
: undefined;
|
||||
if (!txHash || input.outpointIndex === undefined) continue;
|
||||
if (ownUtxoOutpointKeys.has(this.getOutpointKey(txHash, input.outpointIndex))) {
|
||||
if (
|
||||
ownUtxoOutpointKeys.has(
|
||||
this.getOutpointKey(txHash, input.outpointIndex),
|
||||
)
|
||||
) {
|
||||
addScore(commit.entityIdentifier, 3);
|
||||
}
|
||||
}
|
||||
for (const output of commit.data.outputs ?? []) {
|
||||
const lockingBytecodeHex = output.lockingBytecode ? this.toLockingBytecodeHex(output.lockingBytecode) : undefined;
|
||||
const lockingBytecodeHex = output.lockingBytecode
|
||||
? this.toLockingBytecodeHex(output.lockingBytecode)
|
||||
: undefined;
|
||||
if (!lockingBytecodeHex) continue;
|
||||
if (ownLockingBytecodes.has(lockingBytecodeHex)) {
|
||||
addScore(commit.entityIdentifier, 2);
|
||||
@@ -457,7 +577,7 @@ export class HistoryService {
|
||||
|
||||
let bestEntity: string | undefined;
|
||||
let bestScore = 0;
|
||||
for (const [ entity, score ] of scores.entries()) {
|
||||
for (const [entity, score] of scores.entries()) {
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestEntity = entity;
|
||||
@@ -470,17 +590,20 @@ export class HistoryService {
|
||||
utxo: UnspentOutputData,
|
||||
template: XOTemplate | null,
|
||||
variables: Record<string, XOInvitationVariableValue>,
|
||||
roleIdentifier?: string
|
||||
roleIdentifier?: string,
|
||||
): string {
|
||||
const templateName = template?.name ?? 'UnknownTemplate';
|
||||
const role = roleIdentifier ?? 'unknown';
|
||||
const templateName = template?.name ?? "UnknownTemplate";
|
||||
const role = roleIdentifier ?? "unknown";
|
||||
const outputDef = template?.outputs?.[utxo.outputIdentifier];
|
||||
let detail = outputDef?.name ?? utxo.outputIdentifier;
|
||||
if (outputDef?.description) {
|
||||
try {
|
||||
detail = compileCashAssemblyString(outputDef.description, variables);
|
||||
} catch {
|
||||
detail = this.interpolateSimpleCashAssemblyVariables(outputDef.description, variables);
|
||||
detail = this.interpolateSimpleCashAssemblyVariables(
|
||||
outputDef.description,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
}
|
||||
return `[${templateName}:${role}] ${detail}`;
|
||||
@@ -490,19 +613,28 @@ export class HistoryService {
|
||||
invitation: XOInvitation,
|
||||
template: XOTemplate | null,
|
||||
variables: Record<string, XOInvitationVariableValue>,
|
||||
roleIdentifier?: string
|
||||
roleIdentifier?: string,
|
||||
): string {
|
||||
if (!template) return invitation.actionIdentifier;
|
||||
const action = template.actions?.[invitation.actionIdentifier];
|
||||
const transactionName = action?.transaction;
|
||||
const transaction = transactionName ? template.transactions?.[transactionName] : null;
|
||||
const role = roleIdentifier ?? 'unknown';
|
||||
const baseTemplate = transaction?.description ?? action?.description ?? action?.name ?? invitation.actionIdentifier;
|
||||
const transaction = transactionName
|
||||
? template.transactions?.[transactionName]
|
||||
: null;
|
||||
const role = roleIdentifier ?? "unknown";
|
||||
const baseTemplate =
|
||||
transaction?.description ??
|
||||
action?.description ??
|
||||
action?.name ??
|
||||
invitation.actionIdentifier;
|
||||
let detail = baseTemplate;
|
||||
try {
|
||||
detail = compileCashAssemblyString(baseTemplate, variables);
|
||||
} catch {
|
||||
detail = this.interpolateSimpleCashAssemblyVariables(baseTemplate, variables);
|
||||
detail = this.interpolateSimpleCashAssemblyVariables(
|
||||
baseTemplate,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
return `[${template.name}:${role}] ${detail}`;
|
||||
}
|
||||
@@ -510,16 +642,19 @@ export class HistoryService {
|
||||
private deriveInputDescription(
|
||||
inputIdentifier: string,
|
||||
template: XOTemplate | null,
|
||||
variables: Record<string, XOInvitationVariableValue>
|
||||
variables: Record<string, XOInvitationVariableValue>,
|
||||
): string {
|
||||
if (inputIdentifier === 'input') return 'Funding input';
|
||||
if (inputIdentifier === "input") return "Funding input";
|
||||
const inputDef = template?.inputs?.[inputIdentifier];
|
||||
if (!inputDef) return inputIdentifier;
|
||||
if (!inputDef.description) return inputDef.name ?? inputIdentifier;
|
||||
try {
|
||||
return compileCashAssemblyString(inputDef.description, variables);
|
||||
} catch {
|
||||
return this.interpolateSimpleCashAssemblyVariables(inputDef.description, variables);
|
||||
return this.interpolateSimpleCashAssemblyVariables(
|
||||
inputDef.description,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,30 +662,38 @@ export class HistoryService {
|
||||
invitation: XOInvitation,
|
||||
template: XOTemplate | null,
|
||||
variables: Record<string, XOInvitationVariableValue>,
|
||||
roleIdentifier?: string
|
||||
roleIdentifier?: string,
|
||||
): string {
|
||||
if (!template) return invitation.actionIdentifier;
|
||||
const action = template.actions?.[invitation.actionIdentifier];
|
||||
const transactionName = action?.transaction;
|
||||
const transaction = transactionName ? template.transactions?.[transactionName] : null;
|
||||
const roleData = roleIdentifier ? transaction?.roles?.[roleIdentifier] : undefined;
|
||||
const descriptionTemplate = roleData?.description
|
||||
?? transaction?.description
|
||||
?? roleData?.name
|
||||
?? transaction?.name
|
||||
?? action?.name
|
||||
?? invitation.actionIdentifier;
|
||||
const transaction = transactionName
|
||||
? template.transactions?.[transactionName]
|
||||
: null;
|
||||
const roleData = roleIdentifier
|
||||
? transaction?.roles?.[roleIdentifier]
|
||||
: undefined;
|
||||
const descriptionTemplate =
|
||||
roleData?.description ??
|
||||
transaction?.description ??
|
||||
roleData?.name ??
|
||||
transaction?.name ??
|
||||
action?.name ??
|
||||
invitation.actionIdentifier;
|
||||
try {
|
||||
return compileCashAssemblyString(descriptionTemplate, variables);
|
||||
} catch {
|
||||
return this.interpolateSimpleCashAssemblyVariables(descriptionTemplate, variables);
|
||||
return this.interpolateSimpleCashAssemblyVariables(
|
||||
descriptionTemplate,
|
||||
variables,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private deriveCommitRoleIdentifier(
|
||||
commit: XOInvitationCommit,
|
||||
invitation: XOInvitation,
|
||||
template: XOTemplate | null
|
||||
template: XOTemplate | null,
|
||||
): string | undefined {
|
||||
const explicitRoles = new Set<string>();
|
||||
for (const input of commit.data.inputs ?? []) {
|
||||
@@ -565,30 +708,42 @@ export class HistoryService {
|
||||
if (explicitRoles.size === 1) return Array.from(explicitRoles)[0];
|
||||
|
||||
const action = template?.actions?.[invitation.actionIdentifier];
|
||||
if ((commit.data.inputs?.length ?? 0) > 0 && action?.roles?.sender) return 'sender';
|
||||
if ((commit.data.variables?.length ?? 0) > 0 && action?.roles?.receiver) return 'receiver';
|
||||
if ((commit.data.inputs?.length ?? 0) > 0 && action?.roles?.sender)
|
||||
return "sender";
|
||||
if ((commit.data.variables?.length ?? 0) > 0 && action?.roles?.receiver)
|
||||
return "receiver";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private extractInvitationRoleIdentifier(
|
||||
invitation: XOInvitation,
|
||||
template: XOTemplate | null,
|
||||
walletEntityIdentifier?: string
|
||||
walletEntityIdentifier?: string,
|
||||
): string | undefined {
|
||||
if (walletEntityIdentifier) {
|
||||
const commits = invitation.commits.filter((commit) => commit.entityIdentifier === walletEntityIdentifier);
|
||||
const commits = invitation.commits.filter(
|
||||
(commit) => commit.entityIdentifier === walletEntityIdentifier,
|
||||
);
|
||||
for (const commit of commits) {
|
||||
const role = this.deriveCommitRoleIdentifier(commit, invitation, template);
|
||||
const role = this.deriveCommitRoleIdentifier(
|
||||
commit,
|
||||
invitation,
|
||||
template,
|
||||
);
|
||||
if (role) return role;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private inferRoleFromOutputIdentifier(outputIdentifier: string): string | undefined {
|
||||
private inferRoleFromOutputIdentifier(
|
||||
outputIdentifier: string,
|
||||
): string | undefined {
|
||||
const normalized = outputIdentifier.toLowerCase();
|
||||
if (normalized.includes('receive') || normalized.includes('request')) return 'receiver';
|
||||
if (normalized.includes('change') || normalized.includes('send')) return 'sender';
|
||||
if (normalized.includes("receive") || normalized.includes("request"))
|
||||
return "receiver";
|
||||
if (normalized.includes("change") || normalized.includes("send"))
|
||||
return "sender";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -596,7 +751,7 @@ export class HistoryService {
|
||||
txHash: string,
|
||||
index: number,
|
||||
outpointValueSatoshis: Map<string, bigint>,
|
||||
variables: Record<string, XOInvitationVariableValue>
|
||||
variables: Record<string, XOInvitationVariableValue>,
|
||||
): bigint | undefined {
|
||||
const outpointKey = this.getOutpointKey(txHash, index);
|
||||
const matchedValue = outpointValueSatoshis.get(outpointKey);
|
||||
@@ -622,22 +777,32 @@ export class HistoryService {
|
||||
return `${txid}:${index}`;
|
||||
}
|
||||
|
||||
private getUtxoOriginKey(templateIdentifier: string, outputIdentifier: string, lockingBytecodeHex: string): string {
|
||||
private getUtxoOriginKey(
|
||||
templateIdentifier: string,
|
||||
outputIdentifier: string,
|
||||
lockingBytecodeHex: string,
|
||||
): string {
|
||||
return `${templateIdentifier}:${outputIdentifier}:${lockingBytecodeHex}`;
|
||||
}
|
||||
|
||||
private toLockingBytecodeHex(lockingBytecode: string | Uint8Array): string {
|
||||
if (typeof lockingBytecode === 'string') return lockingBytecode;
|
||||
if (typeof lockingBytecode === "string") return lockingBytecode;
|
||||
return binToHex(lockingBytecode);
|
||||
}
|
||||
|
||||
private interpolateSimpleCashAssemblyVariables(
|
||||
text: string,
|
||||
variables: Record<string, XOInvitationVariableValue>
|
||||
variables: Record<string, XOInvitationVariableValue>,
|
||||
): string {
|
||||
return text.replace(/\$\(<([^>]+)>\)/g, (match, variableIdentifier: string) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(variables, variableIdentifier)) return match;
|
||||
return String(variables[variableIdentifier]);
|
||||
});
|
||||
return text.replace(
|
||||
/\$\(<([^>]+)>\)/g,
|
||||
(match, variableIdentifier: string) => {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(variables, variableIdentifier)
|
||||
)
|
||||
return match;
|
||||
return String(variables[variableIdentifier]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user