Breaking-Change: Extremely rough update to work with Kioks wallet

This commit is contained in:
2026-05-22 14:11:07 +02:00
parent 3d6518e465
commit def261b568
17 changed files with 422 additions and 107 deletions
+7 -22
View File
@@ -81,22 +81,7 @@ export class AppService extends EventEmitter<AppEventMap> {
// TODO: We *technically* dont want this here, but we also need some initial templates for the wallet, so im doing it here
// Import the default P2PKH template
const { templateIdentifier } = await engine.importTemplate(p2pkhTemplate);
// engine
// .subscribeToLockingBytecodesForTemplate(templateIdentifier)
// .catch((err) =>
// console.error(
// `Error subscribing to locking bytecodes for template ${templateIdentifier}: ${err}`,
// ),
// );
// engine
// .updateUnspentOutputsForTemplate(templateIdentifier)
// .catch((err) =>
// console.error(
// `Error updating unspent outputs for template ${templateIdentifier}: ${err}`,
// ),
// );
await engine.importTemplate(p2pkhTemplate);
// Update all the unspents for every template, and subscribe to the locking bytecodes for changes
// TODO: Remove the above lines that do the same thing. Minimising changes for BLISS.
@@ -104,7 +89,7 @@ export class AppService extends EventEmitter<AppEventMap> {
const templates = await engine.listImportedTemplates();
templates.forEach(async (template) => {
// engine.updateUnspentOutputsForTemplate(generateTemplateIdentifier(template));
engine.updateUnspentOutputsForTemplate(generateTemplateIdentifier(template));
engine.subscribeToScriptHashForTemplate(generateTemplateIdentifier(template));
});
};
@@ -114,11 +99,11 @@ export class AppService extends EventEmitter<AppEventMap> {
// 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(parseTemplate(p2pkhTemplate)),
// "receiveOutput",
// "receiver",
// );
await engine.setDefaultLockingParameters(
generateTemplateIdentifier(parseTemplate(p2pkhTemplate)),
"receiveOutput",
"receiver",
);
// Create our own storage for the invitations
const storage = await Storage.create(config.invitationStoragePath);
+25 -9
View File
@@ -1,6 +1,6 @@
import { binToHex, hexToBin, sha256 } from "@bitauth/libauth";
import { compileCashAssemblyString, type Engine } from "@xo-cash/engine";
import type { ScriptHashData, UnspentOutputData } from "@xo-cash/state";
import type { ScriptHashData, State, UnspentOutputData } from "@xo-cash/state";
import type {
XOInvitation,
XOInvitationCommit,
@@ -146,8 +146,10 @@ export class HistoryService {
const contexts = new Map<string, InvitationContext>();
for (const invitation of this.invitations) {
const template =
(await this.engine.getTemplate(invitation.data.templateIdentifier)) ?? null;
const templateIdentifier = invitation.data.templateIdentifier;
const template = templateIdentifier
? (await this.engine.getTemplate(templateIdentifier)) ?? null
: null;
contexts.set(invitation.data.invitationIdentifier, {
invitation,
template,
@@ -164,11 +166,19 @@ export class HistoryService {
const scriptHashDataByScriptHash = new Map<string, ScriptHashData>();
const templateIdentifiers = new Set<string>();
for (const utxo of allUtxos) {
templateIdentifiers.add(utxo.scriptHash);
}
for (const invitation of this.invitations) {
templateIdentifiers.add(invitation.data.templateIdentifier);
if (invitation.data.templateIdentifier) {
templateIdentifiers.add(invitation.data.templateIdentifier);
}
}
const uniqueScriptHashes = new Set(allUtxos.map((utxo) => utxo.scriptHash));
for (const scriptHash of uniqueScriptHashes) {
const scriptHashData = await this.getScriptHashData(scriptHash);
if (scriptHashData === undefined) continue;
scriptHashDataByScriptHash.set(scriptHash, scriptHashData);
templateIdentifiers.add(scriptHashData.templateIdentifier);
}
for (const templateIdentifier of templateIdentifiers) {
@@ -186,8 +196,10 @@ export class HistoryService {
metadataIndex: WalletMetadataIndex,
): Promise<UtxoContext> {
const scriptHashData = metadataIndex.scriptHashDataByScriptHash.get(utxo.scriptHash);
const templateIdentifier = scriptHashData?.templateIdentifier!;
const template = (await this.engine.getTemplate(templateIdentifier)) ?? null;
const templateIdentifier = scriptHashData?.templateIdentifier;
const template = templateIdentifier
? (await this.engine.getTemplate(templateIdentifier)) ?? null
: null;
return {
utxo,
@@ -592,6 +604,10 @@ export class HistoryService {
: binToHex(output.lockingBytecode);
}
private async getScriptHashData(scriptHash: string): Promise<ScriptHashData | undefined> {
return (this.engine as unknown as { state: State }).state.getScriptHashData(scriptHash);
}
private getOutpointKey(txid: string, index: number): string {
return `${txid}:${index}`;
}
+5 -1
View File
@@ -32,7 +32,7 @@ import type { BaseStorage } from "./storage.js";
import type { BlockchainService } from "./electrum.js";
import { EventEmitter } from "../utils/event-emitter.js";
import { decodeExtendedJson, decodeExtendedJsonObject, encodeExtendedJson } from "../utils/ext-json.js";
import { decodeExtendedJsonObject } from "../utils/ext-json.js";
import { compileCashAssemblyString } from "@xo-cash/engine";
export type InvitationEventMap = {
@@ -154,6 +154,10 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
* Start the invitation - Connect sync server and download latest invitation data.
*/
async start(): Promise<void> {
// Persist immediately so imports survive sync-server outages and appear in the TUI
// after a CLI import or app restart.
await this.storage.set(this.data.invitationIdentifier, this.data);
try {
// Connect to the sync server and get the invitation (in parallel)
const [_, invitation] = await Promise.all([
+11 -11
View File
@@ -1,5 +1,6 @@
import Database from "better-sqlite3";
import { decodeExtendedJson, encodeExtendedJson } from "../utils/ext-json.js";
import { deserializeInvitation, serializeInvitation } from "@xo-cash/engine";
import type { XOInvitation } from "@xo-cash/types";
/**
* This is not an actual storage adapter that we want to make use of. This storage adapter is a stop-gap while the engine is under development.
@@ -56,9 +57,8 @@ export class Storage extends BaseStorage {
return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey;
}
async set(key: string, value: any): Promise<void> {
// Encode the extended json object
const encodedValue = encodeExtendedJson(value);
async set(key: string, value: XOInvitation): Promise<void> {
const encodedValue = serializeInvitation(value);
// Insert or replace the value into the database with full key (including basePath)
const fullKey = this.getFullKey(key);
@@ -93,10 +93,10 @@ export class Storage extends BaseStorage {
return !strippedKey.includes(".");
});
// Decode the extended json objects and strip basePath from keys
// Deserialize invitations and strip basePath from keys
return filteredRows.map((row) => ({
key: this.stripBasePath(row.key),
value: decodeExtendedJson(row.value),
value: deserializeInvitation(row.value),
}));
}
@@ -111,7 +111,7 @@ export class Storage extends BaseStorage {
if (!row) return null;
// Decode the extended json object
return decodeExtendedJson(row.value);
return deserializeInvitation(row.value);
}
async remove(key: string): Promise<void> {
@@ -174,9 +174,9 @@ export class InMemoryStorage extends BaseStorage {
return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey;
}
async set(key: string, value: any): Promise<void> {
async set(key: string, value: XOInvitation): Promise<void> {
const fullKey = this.getFullKey(key);
const encodedValue = encodeExtendedJson(value);
const encodedValue = serializeInvitation(value);
this.store.set(fullKey, encodedValue);
}
@@ -199,7 +199,7 @@ export class InMemoryStorage extends BaseStorage {
return filteredRows.map((row) => ({
key: this.stripBasePath(row.key),
value: decodeExtendedJson(row.value),
value: deserializeInvitation(row.value),
}));
}
@@ -208,7 +208,7 @@ export class InMemoryStorage extends BaseStorage {
const encodedValue = this.store.get(fullKey);
if (encodedValue === undefined) return null;
return decodeExtendedJson(encodedValue);
return deserializeInvitation(encodedValue);
}
async remove(key: string): Promise<void> {