Large amount of changes. Successfully broadcasts txs

This commit is contained in:
2026-03-08 15:53:50 +00:00
parent 66e9918e04
commit 9ef1720e1f
19 changed files with 1374 additions and 352 deletions

View File

@@ -55,6 +55,8 @@ export class AppService extends EventEmitter<AppEventMap> {
await engine.importTemplate(p2pkhTemplate);
// Set default locking parameters for P2PKH
// To my knowledge, this doesnt generate any lockscript, so discovery of funds will not work automatically.
// TODO: Add discovery for funds in the first index? Or until we return 0 TXs?
await engine.setDefaultLockingParameters(
generateTemplateIdentifier(p2pkhTemplate),
'receiveOutput',
@@ -63,9 +65,10 @@ export class AppService extends EventEmitter<AppEventMap> {
// Create our own storage for the invitations
const storage = await Storage.create(config.invitationStoragePath);
const walletStorage = await storage.child(seedHash.slice(0, 8))
// Create the app service
return new AppService(engine, storage, config);
return new AppService(engine, walletStorage, config);
}
constructor(engine: Engine, storage: Storage, config: AppConfig) {

View File

@@ -6,8 +6,8 @@
* - UTXOs we own (with descriptions derived from template outputs)
*/
import type { Engine } from '@xo-cash/engine';
import type { XOInvitation, XOTemplate } from '@xo-cash/types';
import { type Engine, compileCashAssemblyString } from '@xo-cash/engine';
import type { XOInvitation, XOInvitationVariableValue, XOTemplate } from '@xo-cash/types';
import type { UnspentOutputData } from '@xo-cash/state';
import type { Invitation } from './invitation.js';
import { binToHex } from '@bitauth/libauth';
@@ -203,7 +203,7 @@ export class HistoryService {
const outputDef = template.outputs?.[utxo.outputIdentifier];
if (!outputDef) {
return `${utxo.outputIdentifier} output`;
return `[${template.name}] ${utxo.outputIdentifier} output`;
}
// Start with the output name or identifier
@@ -211,11 +211,7 @@ export class HistoryService {
// If there's a description, parse it and replace variable placeholders
if (outputDef.description) {
description = outputDef.description
// Replace <variableName> placeholders (we don't have variable values here, so just clean up)
.replace(/<([^>]+)>/g, (_, varId) => varId)
// Remove $() wrappers
.replace(/\$\(([^)]+)\)/g, '$1');
description = compileCashAssemblyString(outputDef.description, {})
}
return description;
@@ -239,14 +235,13 @@ export class HistoryService {
}
const committedVariables = invitation.commits.flatMap(c => c.data.variables ?? []);
const formattedVariables = committedVariables.reduce((acc, v) => {
acc[v.variableIdentifier ?? ''] = v.value;
return acc;
}, {} as Record<string, XOInvitationVariableValue>);
const description = compileCashAssemblyString(transaction.description, formattedVariables);
return transaction.description
// Replace <variableName> with actual values
.replace(/<([^>]+)>/g, (match, varId) => {
const variable = committedVariables.find(v => v.variableIdentifier === varId);
return variable ? String(variable.value) : match;
})
// Remove the $() wrapper around variable expressions
.replace(/\$\(([^)]+)\)/g, '$1');
return description;
}
}

View File

@@ -1,6 +1,6 @@
import type { AcceptInvitationParameters, AppendInvitationParameters, Engine, FindSuitableResourcesParameters } from '@xo-cash/engine';
import { hasInvitationExpired } from '@xo-cash/engine';
import type { XOInvitation, XOInvitationCommit, XOInvitationInput, XOInvitationOutput, XOInvitationVariable } from '@xo-cash/types';
import type { XOInvitation, XOInvitationCommit, XOInvitationInput, XOInvitationOutput, XOInvitationVariable, XOInvitationVariableValue } from '@xo-cash/types';
import type { UnspentOutputData } from '@xo-cash/state';
import type { SSEvent } from '../utils/sse-client.js';
@@ -9,6 +9,7 @@ import type { Storage } from './storage.js';
import { EventEmitter } from '../utils/event-emitter.js'
import { decodeExtendedJsonObject } from '../utils/ext-json.js';
import { compileCashAssemblyString } from '@xo-cash/engine';
export type InvitationEventMap = {
'invitation-updated': XOInvitation;
@@ -32,14 +33,12 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
// Try to get the invitation from the storage
const invitationFromStorage = await dependencies.storage.get(invitation);
if (invitationFromStorage) {
console.log(`Invitation found in storage: ${invitation}`);
return this.create(invitationFromStorage, dependencies);
}
// Try to get the invitation from the sync server
const invitationFromSyncServer = await dependencies.syncServer.getInvitation(invitation);
if (invitationFromSyncServer && invitationFromSyncServer.invitationIdentifier === invitation) {
console.log(`Invitation found in sync server: ${invitation}`);
return this.create(invitationFromSyncServer, dependencies);
}
@@ -345,6 +344,14 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
await this.syncServer.publishInvitation(this.data);
}
/**
* Generate the locking bytecode for the invitation
* TODO: Find out if this has side-effects or needs special handling
*/
async generateLockingBytecode(outputIdentifier: string, roleIdentifier?: string): Promise<string> {
return this.engine.generateLockingBytecode(this.data.templateIdentifier, outputIdentifier, roleIdentifier);
}
async addOutputs(outputs: XOInvitationOutput[]): Promise<void> {
// Add the outputs to the invitation
await this.append({ outputs });
@@ -410,4 +417,89 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
async getLockingBytecode(outputIdentifier: string, roleIdentifier?: string): Promise<string> {
return this.engine.generateLockingBytecode(this.data.templateIdentifier, outputIdentifier, roleIdentifier);
}
}
/**
* Get the sats out for the invitation
* TODO: Clean up this function. Why is it so big? Can obviously make it 2 functions instead of recursive, but still...
*/
async getSatsOut(outputIdentifier?: string): Promise<bigint> {
// If an output identifier is provided, find all outputs with that identifier, and its valueSatoshis identifier back to the variables
if (outputIdentifier) {
// Get the valueSatoshis identifier from the template
const template = await this.engine.getTemplate(this.data.templateIdentifier);
if (!template) {
throw new Error(`Template not found: ${this.data.templateIdentifier} when trying to get sats out for output: ${outputIdentifier}`);
}
const output = template.outputs[outputIdentifier];
if (!output) {
throw new Error(`Output not found: ${outputIdentifier} in template: ${this.data.templateIdentifier}`);
}
const valueSatoshisIdentifier = output.valueSatoshis;
if (!valueSatoshisIdentifier) {
throw new Error(`Value satoshis identifier not found: ${outputIdentifier} in template: ${this.data.templateIdentifier}`);
}
// Create a list of all the variables from the commits
const variables = this.data.commits.flatMap(c => c.data?.variables ?? []);
// Create a dictionary of the variables
const formattedVariables = variables.reduce((acc, v) => {
acc[v.variableIdentifier ?? ''] = v.value;
return acc;
}, {} as Record<string, XOInvitationVariableValue>);
// Compile the CashAssembly expression to get the value satoshis (It handles the variable replacement for us)
const valueSatoshis = await compileCashAssemblyString(String(valueSatoshisIdentifier), formattedVariables);
// Return the value satoshis as a bigint
// TODO: Check this of a vulnerability or crash - I assume there might be one if someone made the `valueSatoshis` a malicious expression
return BigInt(valueSatoshis);
}
// If we didnt get an output identifier, go through the action outputs and sum the valueSatoshis
const action = this.data.actionIdentifier;
if (!action) {
throw new Error(`Action not found: ${this.data.actionIdentifier} when trying to get sats out for output: ${outputIdentifier}`);
}
// Get the template
const template = await this.engine.getTemplate(this.data.templateIdentifier);
if (!template) {
throw new Error(`Template not found: ${this.data.templateIdentifier} when trying to get sats out for action: ${action}`);
}
// Get the transaction ID from the action
const transactionID = template.actions[action]?.transaction
if (!transactionID) {
throw new Error(`Transactions not found: ${action} in template: ${this.data.templateIdentifier}`);
}
// Get the transaction from the template
const transaction = template.transactions?.[transactionID];
if (!transaction) {
throw new Error(`Transaction not found: ${transactionID} in template: ${this.data.templateIdentifier}`);
}
// Get the outputs from the transaction
const outputs = transaction.outputs;
if (!outputs) {
throw new Error(`Outputs not found: ${transactionID} in template: ${this.data.templateIdentifier}`);
}
// Create a value to store the cummulative total of the outputs
let totalSats = 0n;
// Iterate through the outputs and sum the valueSatoshis
for (const output of outputs) {
if (typeof output === 'string') {
totalSats += await this.getSatsOut(output);
} else {
totalSats += await this.getSatsOut(output.output);
}
}
return totalSats;
}
}