Update to latest engine. Add sync-v2. Various fixes.

This commit is contained in:
2026-09-07 03:33:44 +00:00
parent 051fc0c9ac
commit f01f9bc56b
31 changed files with 5977 additions and 1392 deletions
+70 -26
View File
@@ -1,6 +1,8 @@
import {
ElectrumBlockchainProvider,
Engine,
type XOEngineOptions,
computeAccountHash,
// This is temporary. Will likely be moved to where we import templates in the cli. I think that makes more sense as this is a library thing
generateTemplateIdentifier,
} from "@xo-cash/engine";
@@ -19,11 +21,12 @@ import { EventEmitter } from "../utils/event-emitter.js";
// TODO: Remove this. Exists to hash the seed for database namespace.
import { createHash } from "crypto";
import { hexToBin } from "@bitauth/libauth";
import { parseTemplate } from "@xo-cash/engine";
import { parseTemplate, BlockchainMonitor } from "@xo-cash/engine";
import { p2pkhTemplate } from "@xo-cash/templates";
import { vendingMachineTemplate } from "../templates/vending-machine.js";
import { wrapBCHTemplate } from "../templates/wrap-template.js";
import { createStorageAdapter, State, StorageType } from "@xo-cash/state";
export type AppEventMap = {
"invitation-added": Invitation;
@@ -54,6 +57,7 @@ export class AppService extends EventEmitter<AppEventMap> {
public electrum: BlockchainService;
public rates: RatesService;
public settings: SettingsService;
public state: State;
public invitations: Invitation[] = [];
/**
@@ -79,16 +83,45 @@ export class AppService extends EventEmitter<AppEventMap> {
// Because of a bug that lets wallets read the unspents of other wallets, we are going to manually namespace the storage paths for the app.
// We are going to do this by computing a hash of the seed and prefixing the storage paths with it.
const seedHash = createHash("sha256").update(seed).digest("hex");
const accountHash = computeAccountHash(seed);
// The v2 client derives an isolated signing key from this key for each invitation resource.
const syncPrivateKey = hexToBin(seedHash);
// We want to only prefix the file name
const prefixedStoragePath = `${seedHash.slice(0, 8)}-${config.engineConfig.databaseFilename}`;
// Create the engine
const engine = await Engine.create(seed, {
...config.engineConfig,
databaseFilename: prefixedStoragePath,
// Initialize the blockchain provider using Electrum
const blockchainProvider = new ElectrumBlockchainProvider({
applicationIdentifier: config.electrumApplicationIdentifier ?? 'XO-CLI',
electrumOptions: config.engineConfig,
});
// Start the blockchain provider (do this async, otherwise we block startup - the data is event-based so we dont need to await it)
blockchainProvider.initialize();
// Create the storage adapter
const storageAdapter = await createStorageAdapter({
storageType: config.engineConfig.storageType ?? StorageType.INDEXEDDB,
databasePath: config.engineConfig.databasePath,
databaseFilename: prefixedStoragePath,
accountHash,
});
// Create the state instance
const state = new State(storageAdapter);
// Initialize the blockchain monitor, event listeners for electrum application events
const blockchainMonitor = new BlockchainMonitor(state, blockchainProvider);
blockchainMonitor.initializeEventListeners();
// Create the engine
// TODO: Remove this type assertion. Make Engine constructor public OR allow full pre-defined dependency injection to `.create()`
const engine = new Engine(seed, state, blockchainProvider, blockchainMonitor);
// Initialize the state sync server.
await engine.initializeStateSync();
// 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
await engine.importTemplate(p2pkhTemplate);
@@ -100,14 +133,15 @@ export class AppService extends EventEmitter<AppEventMap> {
const updateTemplates = async () => {
const templates = await engine.listImportedTemplates();
templates.forEach(async (template) => {
engine.updateUnspentOutputsForTemplate(
generateTemplateIdentifier(template),
);
engine.subscribeToScriptHashForTemplate(
generateTemplateIdentifier(template),
);
});
// maaan, I have no clue if this is required still
// templates.forEach(async (template) => {
// engine.updateUnspentOutputsForTemplate(
// generateTemplateIdentifier(template),
// );
// engine.subscribeToScriptHashForTemplate(
// generateTemplateIdentifier(template),
// );
// });
};
updateTemplates();
@@ -115,11 +149,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.updateFallbackLockingParameters({
templateIdentifier: generateTemplateIdentifier(parseTemplate(p2pkhTemplate)),
outputIdentifier: "receiveOutput",
roleIdentifier: "receiver",
});
// Create our own storage for the invitations
const storage = await Storage.create(config.invitationStoragePath);
@@ -139,6 +173,8 @@ export class AppService extends EventEmitter<AppEventMap> {
electrum,
rates,
settings,
state,
syncPrivateKey,
);
}
@@ -149,6 +185,8 @@ export class AppService extends EventEmitter<AppEventMap> {
electrum: BlockchainService,
rates: RatesService,
settings: SettingsService,
state: State,
private readonly syncPrivateKey: Uint8Array,
) {
super();
@@ -158,7 +196,8 @@ export class AppService extends EventEmitter<AppEventMap> {
this.electrum = electrum;
this.rates = rates;
this.settings = settings;
this.history = new HistoryService(engine, this.invitations);
this.state = state;
this.history = new HistoryService(engine, this.invitations, state);
}
async createInvitation(
@@ -171,6 +210,7 @@ export class AppService extends EventEmitter<AppEventMap> {
typeof invitation === "string"
? invitation
: invitation.invitationIdentifier,
this.syncPrivateKey,
);
const deps = {
@@ -307,14 +347,18 @@ export class AppService extends EventEmitter<AppEventMap> {
byInvitation.set(output.reservedBy!, existing);
}
// console.error('Unreserving resources is not currently supported by the engine')
for (const [invitationIdentifier, outputs] of byInvitation) {
await this.engine.unreserveResources(
outputs.map((o) => ({
outpointTransactionHash: hexToBin(o.outpointTransactionHash),
outpointIndex: o.outpointIndex,
})),
invitationIdentifier,
);
// Remove them directly from state
this.state.archiveInvitation(invitationIdentifier);
// await this.engine.unreserveResources(
// outputs.map((o) => ({
// outpointTransactionHash: hexToBin(o.outpointTransactionHash),
// outpointIndex: o.outpointIndex,
// })),
// invitationIdentifier,
// );
}
return reserved.length;