import { Engine, type XOEngineOptions, // 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'; import type { XOInvitation } from '@xo-cash/types'; import { Invitation } from './invitation.js'; import { Storage } from './storage.js'; import { SyncServer } from '../utils/sync-server.js'; import { HistoryService } from './history.js'; import { EventEmitter } from '../utils/event-emitter.js'; // TODO: Remove this. Exists to hash the seed for database namespace. import { createHash } from 'crypto'; import { p2pkhTemplate } from '@xo-cash/templates'; export type AppEventMap = { 'invitation-added': Invitation; 'invitation-removed': Invitation; } export interface AppConfig { syncServerUrl: string; engineConfig: XOEngineOptions; invitationStoragePath: string; } export class AppService extends EventEmitter { public engine: Engine; public storage: Storage; public config: AppConfig; public history: HistoryService; public invitations: Invitation[] = []; static async create(seed: string, config: AppConfig): Promise { // 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'); // 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, }); // 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); // Set default locking parameters for P2PKH await engine.setDefaultLockingParameters( generateTemplateIdentifier(p2pkhTemplate), 'receiveOutput', 'receiver', ); // Create our own storage for the invitations const storage = await Storage.create(config.invitationStoragePath); // Create the app service return new AppService(engine, storage, config); } constructor(engine: Engine, storage: Storage, config: AppConfig) { super(); this.engine = engine; this.storage = storage; this.config = config; this.history = new HistoryService(engine, this.invitations); } async createInvitation(invitation: XOInvitation | string): Promise { // Make sure the engine has the template imported const invitationStorage = this.storage.child('invitations') const invitationSyncServer = new SyncServer(this.config.syncServerUrl, typeof invitation === 'string' ? invitation : invitation.invitationIdentifier); const deps = { engine: this.engine, syncServer: invitationSyncServer, storage: invitationStorage, }; // Create the invitation const invitationInstance = await Invitation.create(invitation, deps); // Add the invitation to the invitations array await this.addInvitation(invitationInstance); return invitationInstance; } async addInvitation(invitation: Invitation): Promise { // Add the invitation to the invitations array this.invitations.push(invitation); // Emit the invitation-added event this.emit('invitation-added', invitation); } async removeInvitation(invitation: Invitation): Promise { // Remove the invitation from the invitations array this.invitations = this.invitations.filter(i => i !== invitation); // Emit the invitation-removed event this.emit('invitation-removed', invitation); } async start(): Promise { // Get the invitations db const invitationsDb = this.storage.child('invitations'); // Load invitations from storage console.time('loadInvitations'); const invitations = await invitationsDb.all() as { key: string; value: XOInvitation }[]; console.timeEnd('loadInvitations'); console.time('createInvitations'); await Promise.all(invitations.map(async ({ key }) => { await this.createInvitation(key); })); console.timeEnd('createInvitations'); } }