import { BlockchainMonitor, Engine } from "@xo-cash/engine"; import { createStorageAdapter, State, StorageType, type UnspentOutputData, } from "@xo-cash/state"; import { InMemoryBlockchainProvider } from "@xo-cash/engine"; import { convertMnemonicToSeedBytes } from "@xo-cash/crypto"; import { binToHex, sha256 } from "@bitauth/libauth"; import { AppService } from "../../../src/services/app"; import { InMemoryStorage } from "../../../src/services/storage"; import { MockElectrumService } from "./electrum-service"; export const DEFAULT_SEED = "page pencil stock planet limb cluster assault speak off joke private pioneer"; /** * Options for creating a fake resource (UTXO) in tests. */ export type FakeResourceOptions = { /** Transaction hash of the outpoint. Auto-generated if not provided. */ outpointTransactionHash?: string; /** Index of the outpoint in the transaction. Defaults to 0. */ outpointIndex?: number; /** Value in satoshis. Defaults to 10000. */ valueSatoshis?: number; /** Template identifier. Defaults to "test-template". */ templateIdentifier?: string; /** Output identifier from the template. Defaults to "receiveOutput". */ outputIdentifier?: string; /** Locking bytecode for this output. Defaults to a placeholder. */ lockingBytecode?: string; /** Block height where the UTXO was mined. Defaults to 800000. */ minedAtHeight?: number; /** Invitation identifier that reserves this output. Undefined means unreserved. */ reservedBy?: string; }; /** * Generates a random 64-character hex string representing a transaction hash. */ export const randomTxHash = (): string => { const bytes = new Uint8Array(32); crypto.getRandomValues(bytes); return Array.from(bytes) .map((b) => b.toString(16).padStart(2, "0")) .join(""); }; /** * Adds a fake resource (UTXO) to the engine's state for testing purposes. * @param engine - The engine instance to add the resource to. * @param options - Options for the fake resource. All fields have sensible defaults. * @returns The created UnspentOutputData object. */ export const addFakeResource = async ( engine: Engine, options: FakeResourceOptions = {}, ): Promise => { const resource: UnspentOutputData = { status: "confirmed", selectable: true, privacy: false, templateIdentifier: options.templateIdentifier ?? "test-template", outputIdentifier: options.outputIdentifier ?? "receiveOutput", outpointIndex: options.outpointIndex ?? 0, outpointTransactionHash: options.outpointTransactionHash ?? randomTxHash(), minedAtHeight: options.minedAtHeight ?? 800000, valueSatoshis: options.valueSatoshis ?? 10000, lockingBytecode: options.lockingBytecode ?? "76a914000000000000000000000000000000000000000088ac", reservedBy: options.reservedBy, }; await engine.state.storeUnspentOutputData(resource); return resource; }; /** * Reserves a resource for a specific invitation. * @param engine - The engine instance. * @param outpointTransactionHash - The transaction hash of the UTXO to reserve. * @param outpointIndex - The output index of the UTXO to reserve. * @param invitationIdentifier - The invitation identifier to reserve for. */ export const reserveResource = async ( engine: Engine, outpointTransactionHash: string, outpointIndex: number, invitationIdentifier: string, ): Promise => { await engine.state.executeBulkUnspentOutputReservation( [{ outpointTransactionHash, outpointIndex }], true, invitationIdentifier, ); }; /** * Unreserves a resource from a specific invitation. * @param engine - The engine instance. * @param outpointTransactionHash - The transaction hash of the UTXO to unreserve. * @param outpointIndex - The output index of the UTXO to unreserve. * @param invitationIdentifier - The invitation identifier to unreserve from. */ export const unreserveResource = async ( engine: Engine, outpointTransactionHash: string, outpointIndex: number, invitationIdentifier: string, ): Promise => { await engine.state.executeBulkUnspentOutputReservation( [{ outpointTransactionHash, outpointIndex }], false, invitationIdentifier, ); }; /** * Create a mock engine instance with a given seed. Uses the in-memory storage and blockchain provider. * @param seed - The seed to use for the engine. * @returns A mock engine instance. */ export const createMockEngine = async (seed: string) => { // Create the in-memory storage adapter. const storage = await createStorageAdapter({ storageType: StorageType.INMEMORY, accountHash: binToHex(sha256.hash(convertMnemonicToSeedBytes(seed))), }); // Initialize the storage adapter. await storage.initialize(); // Create the state instance. const state = new State(storage); // Create the in-memory blockchain provider. const blockchainProvider = new InMemoryBlockchainProvider(); await blockchainProvider.initialize({ applicationIdentifier: "xo-cli-tests", electrumOptions: {}, }); // Create the blockchain monitor instance. const blockchainMonitor = new BlockchainMonitor(state, blockchainProvider); await blockchainMonitor.initializeEventListeners(); // Create the engine instance. const engine = new Engine(seed, state, blockchainMonitor, blockchainProvider); await engine.initializeStateSync(); return engine; }; export const createMockAppService = async (engine: Engine) => { const storage = await InMemoryStorage.create(); const electrum = new MockElectrumService(); const config = { syncServerUrl: "http://localhost:3000", engineConfig: { databasePath: "test-data", databaseFilename: "xo-wallet.db", }, invitationStoragePath: "test-invitations.db", }; return new AppService(engine, storage, config, electrum); };