Fix receive and send

This commit is contained in:
2026-03-16 06:48:29 +00:00
parent 9ef1720e1f
commit dd275593cd
28 changed files with 1918 additions and 769 deletions

View File

@@ -1,11 +1,13 @@
import type { AcceptInvitationParameters, AppendInvitationParameters, Engine, FindSuitableResourcesParameters } from '@xo-cash/engine';
import { hasInvitationExpired } from '@xo-cash/engine';
import { hasInvitationExpired, mergeInvitationCommits } from '@xo-cash/engine';
import type { XOInvitation, XOInvitationCommit, XOInvitationInput, XOInvitationOutput, XOInvitationVariable, XOInvitationVariableValue } from '@xo-cash/types';
import type { UnspentOutputData } from '@xo-cash/state';
import { binToHex, encodeTransaction, generateTransaction, hashTransaction, hexToBin } from '@bitauth/libauth';
import type { SSEvent } from '../utils/sse-client.js';
import type { SyncServer } from '../utils/sync-server.js';
import type { Storage } from './storage.js';
import type { ElectrumService } from './electrum.js';
import { EventEmitter } from '../utils/event-emitter.js'
import { decodeExtendedJsonObject } from '../utils/ext-json.js';
@@ -20,6 +22,7 @@ export type InvitationDependencies = {
syncServer: SyncServer;
storage: Storage;
engine: Engine;
electrum: ElectrumService;
}
export class Invitation extends EventEmitter<InvitationEventMap> {
@@ -87,16 +90,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
* TODO: This should be a composite with the sync server (probably. We currently double handle this work, which is stupid)
*/
private storage: Storage;
/**
* True after we have successfully called sign() on this invitation (session-only, not persisted).
*/
private _weHaveSigned = false;
/**
* True after we have successfully called broadcast() on this invitation (session-only, not persisted).
*/
private _broadcasted = false;
private electrum: ElectrumService;
/**
* The status of the invitation (last emitted word: pending, actionable, signed, ready, complete, expired, unknown).
@@ -116,6 +110,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
this.engine = dependencies.engine;
this.syncServer = dependencies.syncServer;
this.storage = dependencies.storage;
this.electrum = dependencies.electrum;
// I cannot express this enough, but the event handler does not need a clean up.
// There is this beautiful thing called a "garbage collector". Once this class is removed from scope (removed from the invitations array) all the references
@@ -217,21 +212,14 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
/**
* Internal status computation: returns a single word.
* NOTE: This could be a Enum-like object as well. May be a nice improvement. - DO NOT USE TS ENUM, THEY ARENT NATIVELY SUPPORTED IN NODE.JS
* - expired: any commit has expired
* - complete: we have broadcast this invitation
* - expired: any commit has expired
* - ready: no missing requirements and we have signed (ready to broadcast)
* - signed: we have signed but there are still missing parts (waiting for others)
* - actionable: you can provide data (missing requirements and/or you can sign)
* - unknown: template/action not found or error
*/
private async computeStatusInternal(): Promise<string> {
if (hasInvitationExpired(this.data)) {
return 'expired';
}
if (this._broadcasted) {
return 'complete';
}
let missingReqs;
try {
missingReqs = await this.engine.listMissingRequirements(this.data);
@@ -245,15 +233,74 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
(missingReqs.outputs?.length ?? 0) > 0 ||
(missingReqs.roles !== undefined && Object.keys(missingReqs.roles).length > 0);
if (!hasMissing && this._weHaveSigned) {
const hasSignedCommit = this.hasSignedCommitInInvitation();
if (!hasMissing) {
const transactionHash = await this.deriveTransactionHash();
if (transactionHash && await this.electrum.hasSeenTransaction(transactionHash)) {
return 'complete';
}
}
if (hasInvitationExpired(this.data)) {
return 'expired';
}
if (!hasMissing && hasSignedCommit) {
return 'ready';
}
if (hasMissing && this._weHaveSigned) {
if (hasMissing && hasSignedCommit) {
return 'signed';
}
return 'actionable';
}
private hasSignedCommitInInvitation(): boolean {
for (const commit of this.data.commits) {
for (const input of commit.data.inputs ?? []) {
if (!input.mergesWith) continue;
if (input.unlockingBytecode === undefined) continue;
return true;
}
}
return false;
}
/**
* Build the transaction to get the TX hash, this is so we can check its status on the blockchain.
* TODO: Remove this. This should be part of the engine. The code is virtually identical to `executeAction` except it doesnt throw if the invitation is expired
* @returns txHash or undefined if the transaction could not be built
*/
private async deriveTransactionHash(): Promise<string | undefined> {
try {
const template = await this.engine.getTemplate(this.data.templateIdentifier);
if (!template) return undefined;
const mergedCommit = mergeInvitationCommits(this.data, template);
if (!mergedCommit) return undefined;
const transactionResult = generateTransaction({
version: mergedCommit.transactionVersion,
locktime: mergedCommit.transactionLocktime,
// @ts-expect-error merged inputs include additional invitation metadata.
inputs: mergedCommit.inputs,
// @ts-expect-error merged outputs include additional invitation metadata.
outputs: mergedCommit.outputs,
});
if (!transactionResult.success) return undefined;
const transactionHex = binToHex(encodeTransaction(transactionResult.transaction));
const rawHash: unknown = hashTransaction(hexToBin(transactionHex));
if (typeof rawHash === 'string') return rawHash;
if (rawHash instanceof Uint8Array) return binToHex(rawHash);
return undefined;
} catch {
return undefined;
}
}
/**
* Update the status of the invitation and emit the new single-word status.
*/
@@ -291,7 +338,6 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
await this.storage.set(this.data.invitationIdentifier, signedInvitation);
this.data = signedInvitation;
this._weHaveSigned = true;
// Update the status of the invitation
await this.updateStatus();
@@ -306,8 +352,6 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
broadcastTransaction: true,
});
this._broadcasted = true;
// Update the status of the invitation
await this.updateStatus();
}