Fix history. Fix invitation accept

This commit is contained in:
2026-05-04 09:28:23 +00:00
parent f978d740fe
commit 7ffb5c44b5
2 changed files with 40 additions and 153 deletions

View File

@@ -1,5 +1,9 @@
import { binToHex } from "@bitauth/libauth";
import { compileCashAssemblyString, type Engine } from "@xo-cash/engine";
import {
compileCashAssemblyString,
type Engine,
listInvitationCommitsByEntity,
} from "@xo-cash/engine";
import type { UnspentOutputData } from "@xo-cash/state";
import type {
XOInvitation,
@@ -59,6 +63,7 @@ interface InvitationContext {
invitation: Invitation;
template: XOTemplate | null;
variables: Record<string, XOInvitationVariableValue>;
walletCommits: XOInvitationCommit[];
walletEntityIdentifier?: string;
}
@@ -73,12 +78,8 @@ export class HistoryService {
private invitations: Invitation[],
) {}
async extractEntities(invitation: XOInvitation): Promise<string[]> {
const entities = new Set<string>();
for (const commit of invitation.commits) {
entities.add(commit.entityIdentifier);
}
return Array.from(entities);
extractEntities(invitation: XOInvitation): Record<string, XOInvitationCommit[]> {
return listInvitationCommitsByEntity(invitation);
}
// Entities are currently static per invitation. So, we can try to match the roles to entities by:
@@ -127,8 +128,6 @@ export class HistoryService {
async getHistory(): Promise<HistoryItem[]> {
const allUtxos = await this.engine.listUnspentOutputsData();
const ownOutpoints = new Set<string>();
const ownLockingBytecodes = new Set<string>();
const invitationByOrigin = new Map<string, UtxoOriginContext>();
const outpointValueSatoshis = new Map<string, bigint>();
@@ -137,8 +136,6 @@ export class HistoryService {
utxo.outpointTransactionHash,
utxo.outpointIndex,
);
ownOutpoints.add(outpointKey);
ownLockingBytecodes.add(utxo.scriptHash);
outpointValueSatoshis.set(outpointKey, BigInt(utxo.valueSatoshis));
}
@@ -148,15 +145,15 @@ export class HistoryService {
const template =
(await this.engine.getTemplate(invitation.data.templateIdentifier)) ??
null;
const walletEntityIdentifier = this.resolveWalletEntityIdentifier(
invitation,
ownOutpoints,
ownLockingBytecodes,
const walletCommits = await this.getWalletCommitsForInvitation(
invitation.data,
);
const walletEntityIdentifier = walletCommits[0]?.entityIdentifier;
contexts.set(invitation.data.invitationIdentifier, {
invitation,
template,
variables,
walletCommits,
walletEntityIdentifier,
});
this.indexInvitationOutputsByUtxoOrigin(invitationByOrigin, invitation);
@@ -186,7 +183,6 @@ export class HistoryService {
const invitationInputs = this.buildWalletInputItemsForInvitation(
context,
roles[0],
invitationOutputs.length > 0,
outpointValueSatoshis,
);
const invitationDescription = this.deriveInvitationDescription(
@@ -287,51 +283,25 @@ export class HistoryService {
return outputs;
}
private async getWalletCommitsForInvitation(
invitation: XOInvitation,
): Promise<XOInvitationCommit[]> {
try {
return await this.engine.getOwnCommits(invitation);
} catch {
return [];
}
}
private buildWalletInputItemsForInvitation(
context: InvitationContext,
walletRole?: string,
hasWalletOutputs: boolean = false,
outpointValueSatoshis: Map<string, bigint> = new Map(),
): HistoryUtxoItem[] {
const invitation = context.invitation.data;
const commits = invitation.commits ?? [];
const commitsByEntity = context.walletEntityIdentifier
? commits.filter(
(commit) =>
commit.entityIdentifier === context.walletEntityIdentifier,
)
: [];
const commitsByRole = walletRole
? commits.filter(
(commit) =>
this.deriveCommitRoleIdentifier(
commit,
invitation,
context.template,
) === walletRole,
)
: [];
let relevantCommits = commitsByEntity.filter(
const relevantCommits = context.walletCommits.filter(
(commit) => (commit.data.inputs?.length ?? 0) > 0,
);
if (relevantCommits.length === 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,
);
}
// Sender fallback only when no wallet outputs were matched.
if (relevantCommits.length === 0 && !hasWalletOutputs) {
relevantCommits = commits.filter(
(commit) => (commit.data.inputs?.length ?? 0) > 0,
);
}
const txDescription = this.deriveTransactionActivityDescription(
invitation,
context.template,
@@ -355,7 +325,10 @@ export class HistoryService {
context.variables,
);
const templateName = context.template?.name ?? "UnknownTemplate";
const role = walletRole ?? "sender";
const role =
this.deriveCommitRoleIdentifier(commit, invitation, context.template) ??
walletRole ??
"sender";
const inputValue = this.resolveInputSatoshis(
txHash,
inputIndex,
@@ -422,13 +395,6 @@ export class HistoryService {
};
}
/**
* TODO: This is completely incorrect. It should be deriving the roles of the user based on the entity IDs in the commits, then mapping each commit to Inputs/Outputs so we know what belongs to the user.
* There are a few changes that will need to be made to make this work:
* 1. Provide a way to derive all entity IDs of the user for the invitation (If we are going with XPub)
* 2. Provide a way to get only the User's commits (and their inputs/outputs)
* 3. (Maybe) Include role on inputs and outputs - This one might be fine with just using the commit entity id
*/
private deriveWalletRolesForInvitation(
context: InvitationContext,
outputs: HistoryUtxoItem[],
@@ -444,33 +410,20 @@ export class HistoryService {
roles.add("receiver");
}
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 (roles.size === 0) {
const inferred = this.extractInvitationRoleIdentifier(
for (const commit of context.walletCommits) {
const role = this.deriveCommitRoleIdentifier(
commit,
context.invitation.data,
context.template,
context.walletEntityIdentifier,
);
if (inferred) roles.add(inferred);
if (role) roles.add(role);
}
const hasInputCommit = context.walletCommits.some(
(c) => (c.data.inputs?.length ?? 0) > 0,
);
if (hasInputCommit) roles.add("sender");
return roles.size > 0 ? Array.from(roles) : ["unknown"];
}
@@ -538,54 +491,6 @@ export class HistoryService {
return invitationByUtxoOrigin.get(originKey)?.roleIdentifier;
}
private resolveWalletEntityIdentifier(
invitation: Invitation,
ownUtxoOutpointKeys: Set<string>,
ownLockingBytecodes: Set<string>,
): string | undefined {
const scores = new Map<string, number>();
const addScore = (entityIdentifier: string, delta: number): void => {
scores.set(entityIdentifier, (scores.get(entityIdentifier) ?? 0) + delta);
};
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)
: undefined;
if (!txHash || input.outpointIndex === undefined) continue;
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;
if (!lockingBytecodeHex) continue;
if (ownLockingBytecodes.has(lockingBytecodeHex)) {
addScore(commit.entityIdentifier, 2);
}
}
}
let bestEntity: string | undefined;
let bestScore = 0;
for (const [entity, score] of scores.entries()) {
if (score > bestScore) {
bestScore = score;
bestEntity = entity;
}
}
return bestEntity;
}
private deriveUtxoDescription(
utxo: UnspentOutputData,
template: XOTemplate | null,
@@ -715,27 +620,6 @@ export class HistoryService {
return undefined;
}
private extractInvitationRoleIdentifier(
invitation: XOInvitation,
template: XOTemplate | null,
walletEntityIdentifier?: string,
): string | undefined {
if (walletEntityIdentifier) {
const commits = invitation.commits.filter(
(commit) => commit.entityIdentifier === walletEntityIdentifier,
);
for (const commit of commits) {
const role = this.deriveCommitRoleIdentifier(
commit,
invitation,
template,
);
if (role) return role;
}
}
return undefined;
}
private inferRoleFromOutputIdentifier(
outputIdentifier: string,
): string | undefined {

View File

@@ -85,8 +85,11 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
throw new Error(`Template not found: ${invitation.templateIdentifier}`);
}
// engine invitation (I have no idea if this is required)
const engineInvitation = await dependencies.engine.acceptInvitation(invitation);
// Create the invitation
const invitationInstance = new Invitation(invitation, dependencies);
const invitationInstance = new Invitation(engineInvitation, dependencies);
// Start the invitation and its tracking
await invitationInstance.start();