Breaking Change: Update to latest XO-Engine #2

Open
Harvmaster wants to merge 22 commits from kiok-update into main
5 changed files with 56 additions and 34 deletions
Showing only changes of commit 3d6518e465 - Show all commits

View File

@@ -105,7 +105,7 @@ export class AppService extends EventEmitter<AppEventMap> {
templates.forEach(async (template) => { templates.forEach(async (template) => {
// engine.updateUnspentOutputsForTemplate(generateTemplateIdentifier(template)); // engine.updateUnspentOutputsForTemplate(generateTemplateIdentifier(template));
engine.subscribeToLockingBytecodesForTemplate(generateTemplateIdentifier(template)); engine.subscribeToScriptHashForTemplate(generateTemplateIdentifier(template));
}); });
}; };
@@ -114,11 +114,11 @@ export class AppService extends EventEmitter<AppEventMap> {
// Set default locking parameters for P2PKH // Set default locking parameters for P2PKH
// To my knowledge, this doesnt generate any lockscript, so discovery of funds will not work automatically. // 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? // TODO: Add discovery for funds in the first index? Or until we return 0 TXs?
await engine.setDefaultLockingParameters( // await engine.setDefaultLockingParameters(
generateTemplateIdentifier(parseTemplate(p2pkhTemplate)), // generateTemplateIdentifier(parseTemplate(p2pkhTemplate)),
"receiveOutput", // "receiveOutput",
"receiver", // "receiver",
); // );
// Create our own storage for the invitations // Create our own storage for the invitations
const storage = await Storage.create(config.invitationStoragePath); const storage = await Storage.create(config.invitationStoragePath);

View File

@@ -165,7 +165,7 @@ export class HistoryService {
const templateIdentifiers = new Set<string>(); const templateIdentifiers = new Set<string>();
for (const utxo of allUtxos) { for (const utxo of allUtxos) {
templateIdentifiers.add(utxo.templateIdentifier); templateIdentifiers.add(utxo.scriptHash);
} }
for (const invitation of this.invitations) { for (const invitation of this.invitations) {
templateIdentifiers.add(invitation.data.templateIdentifier); templateIdentifiers.add(invitation.data.templateIdentifier);
@@ -186,7 +186,7 @@ export class HistoryService {
metadataIndex: WalletMetadataIndex, metadataIndex: WalletMetadataIndex,
): Promise<UtxoContext> { ): Promise<UtxoContext> {
const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(utxo.scriptHash); const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(utxo.scriptHash);
const templateIdentifier = scriptHashData?.templateIdentifier ?? utxo.templateIdentifier; const templateIdentifier = scriptHashData?.templateIdentifier!;
const template = (await this.engine.getTemplate(templateIdentifier)) ?? null; const template = (await this.engine.getTemplate(templateIdentifier)) ?? null;
return { return {
@@ -299,7 +299,7 @@ export class HistoryService {
if (!matchingContext) continue; if (!matchingContext) continue;
const lockingBytecode = this.getOutputLockingBytecodeHex(output) ?? matchingContext.scriptHashData?.lockingBytecode; const lockingBytecode = this.getOutputLockingBytecodeHex(output) ?? matchingContext.scriptHashData?.lockingBytecode;
const outputIdentifier = output.outputIdentifier ?? matchingContext.scriptHashData?.outputIdentifier ?? matchingContext.utxo.outputIdentifier; const outputIdentifier = output.outputIdentifier ?? matchingContext.scriptHashData?.outputIdentifier;
const role = const role =
output.roleIdentifier ?? output.roleIdentifier ??
this.getFirstEntityRole(entityRoles, commit.entityIdentifier) ?? this.getFirstEntityRole(entityRoles, commit.entityIdentifier) ??
@@ -380,20 +380,21 @@ export class HistoryService {
if (usedUtxoIds.has(this.getUtxoId(context.utxo))) return false; if (usedUtxoIds.has(this.getUtxoId(context.utxo))) return false;
if (scriptHash && context.utxo.scriptHash === scriptHash) return true; if (scriptHash && context.utxo.scriptHash === scriptHash) return true;
if (lockingBytecode && context.scriptHashData?.lockingBytecode === lockingBytecode) return true; if (lockingBytecode && context.scriptHashData?.lockingBytecode === lockingBytecode) return true;
if (output.outputIdentifier && context.utxo.outputIdentifier === output.outputIdentifier) return true;
if (output.outputIdentifier && context.scriptHashData?.outputIdentifier === output.outputIdentifier) return true;
return false; return false;
}); });
} }
private projectStandaloneUtxo(context: UtxoContext): WalletHistoryItem { private projectStandaloneUtxo(context: UtxoContext): WalletHistoryItem {
const output = this.projectUtxoOutput(context); const output = this.projectUtxoOutput(context);
const templateIdentifier = context.scriptHashData?.templateIdentifier ?? context.utxo.templateIdentifier; const templateIdentifier = context.scriptHashData?.templateIdentifier;
const role = output.role; const role = output.role;
return { return {
id: `utxo-${context.utxo.outpointTransactionHash}:${context.utxo.outpointIndex}`, id: `utxo-${context.utxo.outpointTransactionHash}:${context.utxo.outpointIndex}`,
source: "utxo", source: "utxo",
templateIdentifier, templateIdentifier: templateIdentifier ?? "",
template: context.template?.name ?? "UnknownTemplate", template: context.template?.name ?? "UnknownTemplate",
roles: role ? [role] : ["unknown"], roles: role ? [role] : ["unknown"],
description: output.description, description: output.description,
@@ -404,7 +405,7 @@ export class HistoryService {
} }
private projectUtxoOutput(context: UtxoContext): WalletHistoryOutput { private projectUtxoOutput(context: UtxoContext): WalletHistoryOutput {
const outputIdentifier = context.scriptHashData?.outputIdentifier ?? context.utxo.outputIdentifier; const outputIdentifier = context.scriptHashData?.outputIdentifier;
const role = context.scriptHashData?.roleIdentifier; const role = context.scriptHashData?.roleIdentifier;
return { return {
@@ -557,7 +558,7 @@ export class HistoryService {
variables: Record<string, XOInvitationVariableValue>, variables: Record<string, XOInvitationVariableValue>,
): string { ): string {
try { try {
return compileCashAssemblyString(description, variables); return compileCashAssemblyString({ cashAssemblyText: description, variables, evaluationDecodeMode: 'utf8' });
} catch { } catch {
return this.interpolateSimpleCashAssemblyVariables(description, variables); return this.interpolateSimpleCashAssemblyVariables(description, variables);
} }

View File

@@ -15,6 +15,10 @@ import type {
} from "@xo-cash/types"; } from "@xo-cash/types";
import type { UnspentOutputData } from "@xo-cash/state"; import type { UnspentOutputData } from "@xo-cash/state";
import { import {
bigIntToBinUint64LE,
bigIntToBinUintBE,
bigIntToBinUintLE,
bigIntToVmNumber,
binToHex, binToHex,
encodeTransaction, encodeTransaction,
generateTransaction, generateTransaction,
@@ -28,7 +32,7 @@ import type { BaseStorage } from "./storage.js";
import type { BlockchainService } from "./electrum.js"; import type { BlockchainService } from "./electrum.js";
import { EventEmitter } from "../utils/event-emitter.js"; import { EventEmitter } from "../utils/event-emitter.js";
import { decodeExtendedJsonObject } from "../utils/ext-json.js"; import { decodeExtendedJson, decodeExtendedJsonObject, encodeExtendedJson } from "../utils/ext-json.js";
import { compileCashAssemblyString } from "@xo-cash/engine"; import { compileCashAssemblyString } from "@xo-cash/engine";
export type InvitationEventMap = { export type InvitationEventMap = {
@@ -279,7 +283,8 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
private async computeStatusInternal(): Promise<string> { private async computeStatusInternal(): Promise<string> {
let missingReqs; let missingReqs;
try { try {
missingReqs = await this.engine.listMissingRequirements(this.data); const missingRequirements = await this.engine.listMissingRequirements(this.data.invitationIdentifier);
missingReqs = missingRequirements.templateRequirements;
} catch { } catch {
return "unknown"; return "unknown";
} }
@@ -394,7 +399,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
*/ */
async sign(): Promise<void> { async sign(): Promise<void> {
// Sign the invitation // Sign the invitation
const signedInvitation = await this.engine.signInvitation(this.data); const signedInvitation = await this.engine.signInvitation(this.data.invitationIdentifier);
// Publish the signed invitation to the sync server // Publish the signed invitation to the sync server
this.publishInvitation(signedInvitation); this.publishInvitation(signedInvitation);
@@ -413,7 +418,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
* @returns The transaction hash returned by the network after broadcast. * @returns The transaction hash returned by the network after broadcast.
*/ */
async broadcast(): Promise<string> { async broadcast(): Promise<string> {
const txHash = await this.engine.executeAction(this.data, { const txHash = await this.engine.executeAction(this.data.invitationIdentifier, {
broadcastTransaction: true, broadcastTransaction: true,
}); });
@@ -431,7 +436,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
*/ */
async append(data: AppendInvitationParameters): Promise<void> { async append(data: AppendInvitationParameters): Promise<void> {
// Append the commit to the invitation // Append the commit to the invitation
this.data = await this.engine.appendInvitation(this.data, data); this.data = await this.engine.appendInvitation(this.data.invitationIdentifier, data);
// Sync the invitation to the sync server // Sync the invitation to the sync server
await this.publishInvitation(this.data); await this.publishInvitation(this.data);
@@ -546,7 +551,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
* Get the missing requirements for the invitation * Get the missing requirements for the invitation
*/ */
async getMissingRequirements() { async getMissingRequirements() {
return this.engine.listMissingRequirements(this.data); return this.engine.listMissingRequirements(this.data.invitationIdentifier);
} }
/** /**
@@ -608,33 +613,41 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
); );
} }
const valueSatoshisIdentifier = output.valueSatoshis; const valueSatoshisExpression = output.valueSatoshis;
if (!valueSatoshisIdentifier) { if (!valueSatoshisExpression) {
throw new Error( throw new Error(
`Value satoshis identifier not found: ${outputIdentifier} in template: ${this.data.templateIdentifier}`, `Value satoshis identifier not found: ${outputIdentifier} in template: ${this.data.templateIdentifier}`,
); );
} }
console.dir(this.data, { depth: null });
// Create a list of all the variables from the commits // Create a list of all the variables from the commits
const variables = this.data.commits.flatMap( const variables = this.data.commits.flatMap(
(c) => c.data?.variables ?? [], (c) => c.data?.variables ?? [],
); );
console.dir(variables, { depth: null });
// Create a dictionary of the variables // Create a dictionary of the variables
const formattedVariables = variables.reduce( const formattedVariables = variables.reduce(
(acc, v) => { (acc, v) => {
acc[v.variableIdentifier ?? ""] = v.value; const { variableIdentifier, value } = v;
console.log(typeof value);
acc[variableIdentifier ?? ""] = value;
return acc; return acc;
}, },
{} as Record<string, XOInvitationVariableValue>, {} as Record<string, XOInvitationVariableValue>,
); );
console.dir(formattedVariables, { depth: null });
// Compile the CashAssembly expression to get the value satoshis (It handles the variable replacement for us) // Compile the CashAssembly expression to get the value satoshis (It handles the variable replacement for us)
const valueSatoshis = await compileCashAssemblyString( const valueSatoshis = compileCashAssemblyString(
String(valueSatoshisIdentifier), { cashAssemblyText: String(valueSatoshisExpression), variables: formattedVariables, evaluationDecodeMode: 'bigint' },
formattedVariables,
); );
console.dir(valueSatoshis, { depth: null });
// Return the value satoshis as a bigint // 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 // 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); return BigInt(valueSatoshis);
@@ -688,9 +701,13 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
// Iterate through the outputs and sum the valueSatoshis // Iterate through the outputs and sum the valueSatoshis
for (const output of outputs) { for (const output of outputs) {
if (typeof output === "string") { if (typeof output === "string") {
totalSats += await this.getSatsOut(output); const sats = await this.getSatsOut(output);
console.log(`Sats for output: ${output} is ${sats}`);
totalSats += sats
} else { } else {
totalSats += await this.getSatsOut(output.output); const sats = await this.getSatsOut(output.output);
console.log(`Sats for output: ${output.output} is ${sats}`);
totalSats += sats;
} }
} }

View File

@@ -221,7 +221,7 @@ export function InvitationScreen(): React.ReactElement {
let isCurrent = true; let isCurrent = true;
appService.engine.getOwnCommits(selectedInvitation.data) appService.engine.findOwnCommits(selectedInvitation.data.invitationIdentifier)
.then((ownCommits) => { .then((ownCommits) => {
if (!isCurrent) return; if (!isCurrent) return;
@@ -723,10 +723,14 @@ export function InvitationScreen(): React.ReactElement {
{outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`} {outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`}
{/* Output description */} {/* Output description */}
{outputTemplate?.description && ' - ' + compileCashAssemblyString(outputTemplate?.description ?? '', variables.reduce((acc, variable) => { {outputTemplate?.description && ' - ' + compileCashAssemblyString({
acc[variable.variableIdentifier] = variable.value as XOInvitationVariableValue; cashAssemblyText: outputTemplate?.description,
return acc; variables: variables.reduce((acc, variable) => {
}, {} as Record<string, XOInvitationVariableValue>))} acc[variable.variableIdentifier] = variable.value as XOInvitationVariableValue;
return acc;
}, {} as Record<string, XOInvitationVariableValue>)
})}
{/* Output value */} {/* Output value */}
{outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)}${getFiatSuffix(outputSatoshis)})`} {outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)}${getFiatSuffix(outputSatoshis)})`}

View File

@@ -30,7 +30,7 @@ export const isInvitationRequirementsComplete = async (
invitation: Invitation, invitation: Invitation,
): Promise<boolean> => { ): Promise<boolean> => {
const missingRequirements = await invitation.getMissingRequirements(); const missingRequirements = await invitation.getMissingRequirements();
return !hasMissingRequirements(missingRequirements); return !hasMissingRequirements(missingRequirements.templateRequirements);
}; };
// TODO: Move to engine in templates.ts // TODO: Move to engine in templates.ts