The tests work

This commit is contained in:
2026-09-07 10:56:08 +00:00
parent f01f9bc56b
commit b7bedc5476
19 changed files with 1855 additions and 1559 deletions
+56 -39
View File
@@ -14,11 +14,11 @@
"@electrum-cash/protocol": "^2.3.1", "@electrum-cash/protocol": "^2.3.1",
"@generalprotocols/oracle-client": "^0.0.1-development.11945476152", "@generalprotocols/oracle-client": "^0.0.1-development.11945476152",
"@xo-cash/crypto": "^0.0.2", "@xo-cash/crypto": "^0.0.2",
"@xo-cash/engine": "file:../engine", "@xo-cash/engine": "^0.0.2-cli.20260907103534754",
"@xo-cash/state": "^0.0.3", "@xo-cash/state": "^0.0.3",
"@xo-cash/templates": "^0.0.3", "@xo-cash/templates": "^0.0.3",
"@xo-cash/types": "^0.0.5", "@xo-cash/types": "^0.0.5",
"@xo-cash/utils": "^0.0.5-test.20260831095544025", "@xo-cash/utils": "^0.0.6-cli.20260907041319224",
"better-sqlite3": "^12.6.2", "better-sqlite3": "^12.6.2",
"clipboardy": "^5.1.0", "clipboardy": "^5.1.0",
"ink": "^6.6.0", "ink": "^6.6.0",
@@ -43,38 +43,6 @@
"vitest": "^4.1.2" "vitest": "^4.1.2"
} }
}, },
"../engine": {
"name": "@xo-cash/engine",
"version": "0.0.2",
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.8",
"@electrum-cash/application": "^0.2.3-development.13447192992",
"@electrum-cash/network": "^4.2.2",
"@electrum-cash/protocol": "^2.3.1",
"@electrum-cash/servers": "^3.1.0",
"@xo-cash/crypto": "0.0.3",
"@xo-cash/primitives": "0.0.2",
"@xo-cash/state": "0.0.3",
"@xo-cash/templates": "0.0.3",
"@xo-cash/types": "0.0.5",
"@xo-cash/utils": "0.0.4",
"eventemitter3": "^5.0.1"
},
"devDependencies": {
"@generalprotocols/cspell-dictionary": "^1.0.1",
"@vitest/coverage-v8": "^4.0.17",
"@viz-kit/esbuild-analyzer": "^1.0.0",
"@xo-cash/eslint-config": "1.0.1",
"cspell": "^9.6.0",
"prettier": "^3.6.2",
"tsdown": "^0.20.0-beta.4",
"typedoc": "^0.28.16",
"typedoc-plugin-coverage": "^4.0.2",
"typescript": "^5.3.2",
"vitest": "^4.0.17"
}
},
"node_modules/@alcalzone/ansi-tokenize": { "node_modules/@alcalzone/ansi-tokenize": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.4.tgz", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.4.tgz",
@@ -1198,8 +1166,57 @@
} }
}, },
"node_modules/@xo-cash/engine": { "node_modules/@xo-cash/engine": {
"resolved": "../engine", "version": "0.0.2-cli.20260907103534754",
"link": true "resolved": "https://verdaccio.harvmaster.com/@xo-cash/engine/-/engine-0.0.2-cli.20260907103534754.tgz",
"integrity": "sha512-HcR5uECJ1SRLZaCfzCsSGTUMmeU4v7dMVgiLhxdbBLUFEUYKEax5bX1BuKrfaqp7+FlN772skK/rAt6TzuKFfQ==",
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.8",
"@electrum-cash/application": "^0.2.3-development.13447192992",
"@electrum-cash/network": "^4.2.2",
"@electrum-cash/protocol": "^2.3.1",
"@electrum-cash/servers": "^3.1.0",
"@xo-cash/crypto": "0.0.3",
"@xo-cash/primitives": "0.0.2",
"@xo-cash/state": "0.0.3",
"@xo-cash/templates": "0.0.3",
"@xo-cash/types": "0.0.5",
"@xo-cash/utils": "^0.0.6-cli.20260907035204932",
"eventemitter3": "^5.0.1"
}
},
"node_modules/@xo-cash/engine/node_modules/@xo-cash/crypto": {
"version": "0.0.3",
"resolved": "https://verdaccio.harvmaster.com/@xo-cash/crypto/-/crypto-0.0.3.tgz",
"integrity": "sha512-f4OXZQ5psIaDOgfAVRCtl8IeMXbdXvtLOft42ux+SUGRnrvZ40Yf//XaLshrzzrG0s/3Co1ijO7fC/nQYvNcjA==",
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.8",
"@xo-cash/primitives": "0.0.2",
"@xo-cash/types": "0.0.5",
"@xo-cash/utils": "0.0.4"
}
},
"node_modules/@xo-cash/engine/node_modules/@xo-cash/utils": {
"version": "0.0.4",
"resolved": "https://verdaccio.harvmaster.com/@xo-cash/utils/-/utils-0.0.4.tgz",
"integrity": "sha512-QsVXnf9/z5+5ywMx5BPVmREuE8OCWIKwmQMM6osu2kVJQJplqZdYy+hGRnOwPGlrrvmWr61BU9u7dBIrDwDDnw==",
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.8",
"@xo-cash/primitives": "0.0.2",
"@xo-cash/types": "0.0.4",
"zod": "^4.3.6"
}
},
"node_modules/@xo-cash/engine/node_modules/@xo-cash/utils/node_modules/@xo-cash/types": {
"version": "0.0.4",
"resolved": "https://verdaccio.harvmaster.com/@xo-cash/types/-/types-0.0.4.tgz",
"integrity": "sha512-XBB/jNC1orDcMUi5Q0g+fnPXdKdb+n7OyqmJ8pxFrfM0y9OAdLjfddMih/JWfOWQtBkvY0CtdaZapgBSrvLeSw==",
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.8"
}
}, },
"node_modules/@xo-cash/primitives": { "node_modules/@xo-cash/primitives": {
"version": "0.0.2", "version": "0.0.2",
@@ -1283,9 +1300,9 @@
} }
}, },
"node_modules/@xo-cash/utils": { "node_modules/@xo-cash/utils": {
"version": "0.0.5-test.20260831095544025", "version": "0.0.6-clio.20260907035202101",
"resolved": "https://verdaccio.harvmaster.com/@xo-cash/utils/-/utils-0.0.5-test.20260831095544025.tgz", "resolved": "https://verdaccio.harvmaster.com/@xo-cash/utils/-/utils-0.0.6-clio.20260907035202101.tgz",
"integrity": "sha512-XWiG8tPPmXy/zrDYUjM1fILL7STaV0THE4xoj6PosuSgh1mUQNdr71Wanbhkou/Pc4HbOcT+KO+DbLz0dGMYRw==", "integrity": "sha512-DBou0RivCxg4PvyC6eWpFWXsxh2gTSD7BPeYIToqkdjweVzszvsWIUJ8tb27MxL56fW6CbGhDXyElciXEqbQmg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@bitauth/libauth": "^3.1.0-next.8", "@bitauth/libauth": "^3.1.0-next.8",
+3 -6
View File
@@ -15,7 +15,7 @@
"build:unsafe": "tsc --nocheck --noEmitOnError false || true && npm run build:copy-scripts", "build:unsafe": "tsc --nocheck --noEmitOnError false || true && npm run build:copy-scripts",
"syntax": "tsc --noEmit", "syntax": "tsc --noEmit",
"start": "SYNC_SERVER_URL=https://v2.sync.xo.harvmaster.com node dist/index.js", "start": "SYNC_SERVER_URL=https://v2.sync.xo.harvmaster.com node dist/index.js",
"test": "vitest --run --passWithNoTests", "test": "vitest --run --passWithNoTests --test-timeout=15000",
"test:watch": "vitest", "test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests", "test:coverage": "vitest run --coverage --passWithNoTests",
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
@@ -31,9 +31,6 @@
"cli", "cli",
"tui" "tui"
], ],
"overrides": {
"@xo-cash/utils": "^0.0.5-test.20260831095544025"
},
"author": "General Protocols", "author": "General Protocols",
"license": "ISC", "license": "ISC",
"description": "XO Wallet CLI - Terminal User Interface for XO crypto wallet", "description": "XO Wallet CLI - Terminal User Interface for XO crypto wallet",
@@ -43,11 +40,11 @@
"@electrum-cash/protocol": "^2.3.1", "@electrum-cash/protocol": "^2.3.1",
"@generalprotocols/oracle-client": "^0.0.1-development.11945476152", "@generalprotocols/oracle-client": "^0.0.1-development.11945476152",
"@xo-cash/crypto": "^0.0.2", "@xo-cash/crypto": "^0.0.2",
"@xo-cash/engine": "file:../engine", "@xo-cash/engine": "^0.0.2-cli.20260907103534754",
"@xo-cash/state": "^0.0.3", "@xo-cash/state": "^0.0.3",
"@xo-cash/templates": "^0.0.3", "@xo-cash/templates": "^0.0.3",
"@xo-cash/types": "^0.0.5", "@xo-cash/types": "^0.0.5",
"@xo-cash/utils": "^0.0.5-test.20260831095544025", "@xo-cash/utils": "^0.0.6-cli.20260907041319224",
"better-sqlite3": "^12.6.2", "better-sqlite3": "^12.6.2",
"clipboardy": "^5.1.0", "clipboardy": "^5.1.0",
"ink": "^6.6.0", "ink": "^6.6.0",
+2 -1
View File
@@ -13,6 +13,7 @@ import { createStorageAdapter, State, StorageType } from "@xo-cash/state";
import { convertMnemonicToSeedBytes } from "@xo-cash/crypto"; import { convertMnemonicToSeedBytes } from "@xo-cash/crypto";
import { binToHex, hash256 } from "@bitauth/libauth"; import { binToHex, hash256 } from "@bitauth/libauth";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { InMemoryBlockchainProvider } from "../../utils/blockchain/in-memory-blockchain-provider.js";
/** /**
* Options for creating an offline engine. * Options for creating an offline engine.
@@ -64,7 +65,7 @@ export async function createOfflineEngine(
const state = new State(storageAdapter); const state = new State(storageAdapter);
// Create a minimal blockchain monitor (no electrum initialization) // Create a minimal blockchain monitor (no electrum initialization)
const blockchainMonitor = new BlockchainMonitor(state); const blockchainMonitor = new BlockchainMonitor(state, new InMemoryBlockchainProvider());
// Engine constructor is private; bypass for offline read-only completions. // Engine constructor is private; bypass for offline read-only completions.
type EngineConstructor = new ( type EngineConstructor = new (
+19 -7
View File
@@ -382,10 +382,19 @@ export const handleInvitationCommand = async (
const template = await resolveTemplate(deps, templateQuery); const template = await resolveTemplate(deps, templateQuery);
const templateIdentifier = generateTemplateIdentifier(template); const templateIdentifier = generateTemplateIdentifier(template);
// Read the variables that were passed in via `-var-<name> <value>`
// TODO: Move this to markers: InvVarHere
const variables = parseVariablesFromOptions(options);
deps.io.verbose(`Variables: ${formatObject(variables)}`);
// if (variables.length > 0) {
// // await invitationInstance.addVariables(variables);
// }
// Create an XOInvitation. We will convert this into our own invitation instance afterwards // Create an XOInvitation. We will convert this into our own invitation instance afterwards
const rawInvitation = await deps.app.engine.createInvitation({ const rawInvitation = await deps.app.engine.createInvitation({
templateIdentifier, templateIdentifier,
actionIdentifier, actionIdentifier,
variables,
}); });
deps.io.verbose(`XOInvitation created: ${formatObject(rawInvitation)}`); deps.io.verbose(`XOInvitation created: ${formatObject(rawInvitation)}`);
@@ -395,13 +404,6 @@ export const handleInvitationCommand = async (
`Invitation instance created: ${formatObject(invitationInstance.data)}`, `Invitation instance created: ${formatObject(invitationInstance.data)}`,
); );
// Read the variables that were passed in via `-var-<name> <value>`
const variables = parseVariablesFromOptions(options);
deps.io.verbose(`Variables: ${formatObject(variables)}`);
if (variables.length > 0) {
await invitationInstance.addVariables(variables);
}
// Build the parameters for the append call. This will resolve the inputs and outputs for the invitation. // Build the parameters for the append call. This will resolve the inputs and outputs for the invitation.
const params = await buildAppendParams(deps, invitationInstance, options); const params = await buildAppendParams(deps, invitationInstance, options);
if (!params) { if (!params) {
@@ -411,6 +413,8 @@ export const handleInvitationCommand = async (
); );
} }
// TOOD MARKER: InvVarHere
// Append the inputs and outputs to the invitation // Append the inputs and outputs to the invitation
const { inputs, outputs } = params; const { inputs, outputs } = params;
deps.io.verbose(`Inputs: ${formatObject(inputs)}`); deps.io.verbose(`Inputs: ${formatObject(inputs)}`);
@@ -419,10 +423,18 @@ export const handleInvitationCommand = async (
await invitationInstance.append({ inputs, outputs }); await invitationInstance.append({ inputs, outputs });
} }
if (inputs.length > 0) { if (inputs.length > 0) {
try {
const feeAwareChange = await invitationInstance.addFeeAwareChange(); const feeAwareChange = await invitationInstance.addFeeAwareChange();
deps.io.out( deps.io.out(
`Miner fee: ${feeAwareChange.feeSatoshis} satoshis; change: ${feeAwareChange.changeAmountSatoshis} satoshis`, `Miner fee: ${feeAwareChange.feeSatoshis} satoshis; change: ${feeAwareChange.changeAmountSatoshis} satoshis`,
); );
} catch(error) {
deps.io.err(`Failed to add fee-aware change: ${error instanceof Error ? error.message : "unknown error"}`);
throw new CommandError(
"invitation.create.add_fee_aware_change_failed",
`Failed to add fee-aware change: ${error instanceof Error ? error.message : "unknown error"}`,
);
}
} }
// Write the invitation to a file in the working directory // Write the invitation to a file in the working directory
+5 -5
View File
@@ -227,13 +227,13 @@ export const handleResourceCommand = async (
} }
// Unreserve the resources // Unreserve the resources
await deps.app.engine.unreserveResources( await deps.app.engine.archiveInvitation(target.reservedBy);
[{ outpointTransactionHash: hexToBin(txHash), outpointIndex: vout }],
target.reservedBy,
);
// TODO: This should ideally not archive the invitation, but instead just release the resource so the user can add a different resource to the same invitation.
// Maybe make this method only available on invitations that haven't been synced yet? That way the user has some time to undo it without scrapping the whole invitation,
// but is still safe from the other participants from spending the resource?
deps.io.out( deps.io.out(
`Unreserved ${bold(`${txHash}:${vout}`)} (was reserved for ${target.reservedBy})`, `Archived invitation ${bold(target.reservedBy)} and released its resources`,
); );
// TODO: What do I want to return here? // TODO: What do I want to return here?
+4 -4
View File
@@ -1,6 +1,6 @@
import { existsSync, writeFileSync } from "fs"; import { existsSync, writeFileSync } from "fs";
import path from "path"; import path from "path";
import { generateTemplateIdentifier } from "@xo-cash/engine"; import { generateTemplateIdentifier, parseTemplate } from "@xo-cash/engine";
import type { XOTemplate } from "@xo-cash/types"; import type { XOTemplate } from "@xo-cash/types";
import { bold, dim, formatObject } from "../utils.js"; import { bold, dim, formatObject } from "../utils.js";
@@ -529,11 +529,11 @@ export const handleTemplateCommand = async (
); );
// Set the default locking parameters // Set the default locking parameters
await deps.app.engine.setDefaultLockingParameters( await deps.app.engine.updateFallbackLockingParameters({
templateFile, templateIdentifier: generateTemplateIdentifier(parseTemplate(templateFile)),
outputIdentifier, outputIdentifier,
roleIdentifier, roleIdentifier,
); });
// Return an empty object // Return an empty object
return {}; return {};
+2 -1
View File
@@ -350,7 +350,8 @@ export class AppService extends EventEmitter<AppEventMap> {
// console.error('Unreserving resources is not currently supported by the engine') // console.error('Unreserving resources is not currently supported by the engine')
for (const [invitationIdentifier, outputs] of byInvitation) { for (const [invitationIdentifier, outputs] of byInvitation) {
// Remove them directly from state // Remove them directly from state
this.state.archiveInvitation(invitationIdentifier); // TODO: Make this parallel. CBF doing it now, because it likely breaks things.
await this.state.archiveInvitation(invitationIdentifier);
// await this.engine.unreserveResources( // await this.engine.unreserveResources(
// outputs.map((o) => ({ // outputs.map((o) => ({
+2 -1
View File
@@ -1,5 +1,6 @@
import { binToHex, hexToBin, sha256 } from "@bitauth/libauth"; import { binToHex, hexToBin, sha256 } from "@bitauth/libauth";
import { compileCashAssemblyString, type Engine } from "@xo-cash/engine"; import { type Engine } from "@xo-cash/engine";
import { compileCashAssemblyString } from "@xo-cash/utils";
import type { ScriptHashData, State, UnspentOutputData } from "@xo-cash/state"; import type { ScriptHashData, State, UnspentOutputData } from "@xo-cash/state";
import type { import type {
XOInvitation, XOInvitation,
+1 -1
View File
@@ -34,7 +34,7 @@ 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 { decodeExtendedJsonObject } from "../utils/ext-json.js";
import { compileCashAssemblyString } from "@xo-cash/engine"; import { compileCashAssemblyString } from "@xo-cash/utils";
import type { ResolvedInvitationData } from "../utils/resolve-invitation-data.js"; import type { ResolvedInvitationData } from "../utils/resolve-invitation-data.js";
import { resolveCommitReferences } from "../utils/resolve-invitation-data.js"; import { resolveCommitReferences } from "../utils/resolve-invitation-data.js";
@@ -0,0 +1,264 @@
import type { ChainStatus, ElectrumApplicationEvents, TransactionState } from '@electrum-cash/application';
import type { ScriptHash, ScriptHashListUnspentEntry, ScriptHashListUnspentResponse, ScriptHashStatus, TransactionHash, TransactionHex } from '@electrum-cash/protocol';
import type { XOBlockchainProvider } from '@xo-cash/engine';
import { binToHex, hash256, hexToBin } from '@bitauth/libauth';
import { EventEmitter } from '@xo-cash/utils';
/**
* Extremely primitive in-memory blockchain adapter for testing.
* We probably want to move to using the Mem-Cash from Mainnet-Pat
* https://github.com/mainnet-pat/mem-cash
* I haven't looked at how it works yet, though. - Harvey
*/
type TransactionReceivedEvent = {
transactionHash: string;
transactionState: TransactionState;
};
type ScriptHashUpdateEvent = {
scriptHash: string;
status: ScriptHashStatus;
};
type ChainStatusEvent = {
chainStatus: ChainStatus;
};
type InMemoryBlockchainEvents = {
TransactionReceived: TransactionReceivedEvent;
ScriptHashUpdate: ScriptHashUpdateEvent;
ChainStatus: ChainStatusEvent;
};
/**
* In-memory blockchain provider for deterministic tests and local simulations.
*/
export class InMemoryBlockchainProvider implements XOBlockchainProvider {
/**
* Event emitter compatible with electrum event shapes used by the engine.
*/
private readonly events = new EventEmitter<InMemoryBlockchainEvents>();
/**
* Flag indicating if the provider is initialized and available.
*/
private initialized = false;
/**
* Mutable synthetic chain status.
*/
private chainStatus: ChainStatus = {
currentHeight: 0,
verifiedHeight: 0,
verifiedPercent: '0',
};
/**
* Script-hash keyed in-memory UTXO view.
*/
private readonly scriptHashUnspentOutputs = new Map<ScriptHash, ScriptHashListUnspentResponse>();
/**
* Transaction hash -> transaction hex storage for inspection in tests.
*/
private readonly transactions = new Map<TransactionHash, TransactionHex>();
/**
* Initializes the in-memory provider.
*
* @param _options - Initialization options (unused for in-memory backend)
*/
async initialize(): Promise<void> {
this.initialized = true;
this.emitChainStatus();
}
/**
* Stops the in-memory provider and clears all in-memory data.
*/
async stop(): Promise<void> {
this.initialized = false;
this.scriptHashUnspentOutputs.clear();
this.transactions.clear();
}
/**
* Returns whether this provider is initialized.
*/
hasConnectedClient(): boolean {
return this.initialized;
}
/**
* Returns the current synthetic chain status.
*/
async getChainStatus(): Promise<ChainStatus> {
this.assertInitialized();
return structuredClone(this.chainStatus);
}
/**
* Stores the transaction in memory and emits a synthetic TransactionReceived event.
*
* @param transactionHex - Raw transaction hex
* @returns Deterministic transaction hash
*/
async broadcastTransaction(transactionHex: TransactionHex): Promise<TransactionHash> {
this.assertInitialized();
const transactionBytes = hexToBin(transactionHex);
if (typeof transactionBytes === 'string') {
throw new Error('Cannot broadcast invalid transaction hex in InMemoryBlockchainProvider.');
}
const transactionHash = binToHex(hash256(transactionBytes));
this.transactions.set(transactionHash, transactionHex);
this.events.emit('TransactionReceived', {
transactionHash,
transactionState: {
received: true,
verified: undefined,
contested: undefined,
finalized: false,
},
});
return transactionHash;
}
/**
* No-op subscription for in-memory backend.
*
* @param _scriptHash - Script hash to subscribe to
*/
async subscribeToScriptHash(_scriptHash: ScriptHash): Promise<void> {
this.assertInitialized();
}
/**
* Reads current in-memory unspent outputs for the provided script hash.
*
* @param scriptHash - Script hash to query
*/
async fetchScriptHashUnspentOutputs(scriptHash: ScriptHash): Promise<ScriptHashListUnspentResponse> {
this.assertInitialized();
const unspentOutputs = this.scriptHashUnspentOutputs.get(scriptHash) ?? [];
return structuredClone(unspentOutputs);
}
async fetchScriptHashUnspentTransactionOutputs(scriptHash: ScriptHash, includeLocked: boolean, includeReserved: boolean): Promise<ScriptHashListUnspentEntry[]> {
return [] as ScriptHashListUnspentEntry[];
}
async fetchTransaction(transactionHash: TransactionHash): Promise<TransactionHex> {
this.assertInitialized();
const transaction = this.transactions.get(transactionHash);
if (transaction === undefined) {
throw new Error(`Transaction ${transactionHash} not found in InMemoryBlockchainProvider.`);
}
return transaction;
}
/**
* Registers an event listener.
*
* @param eventName - Event name
* @param listener - Event listener
*/
on<EventName extends keyof ElectrumApplicationEvents>(
eventName: EventName,
listener: (...args: ElectrumApplicationEvents[EventName]) => void,
): void {
this.events.on(eventName as 'TransactionReceived' | 'ScriptHashUpdate' | 'ChainStatus', listener as (...args: any[]) => void);
}
/**
* Removes an event listener.
*
* @param eventName - Event name
* @param listener - Event listener
*/
off<EventName extends keyof ElectrumApplicationEvents>(
eventName: EventName,
listener: (...args: any[]) => void,
): void {
this.events.off(eventName as keyof InMemoryBlockchainEvents, listener);
}
/**
* Sets synthetic chain status and emits ChainStatus.
*
* @param chainStatus - Updated chain status
*/
setChainStatus(chainStatus: ChainStatus): void {
this.chainStatus = structuredClone(chainStatus);
this.emitChainStatus();
}
/**
* Advances chain height and emits ChainStatus.
*
* @param blocks - Number of blocks to advance
*/
advanceBlocks(blocks = 1): void {
this.chainStatus.currentHeight += blocks;
this.chainStatus.verifiedHeight += blocks;
const ratio = this.chainStatus.currentHeight === 0 ? 0 : (this.chainStatus.verifiedHeight / this.chainStatus.currentHeight) * 100;
this.chainStatus.verifiedPercent = ratio.toFixed(0);
this.emitChainStatus();
}
/**
* Replaces the in-memory unspent outputs for a script hash.
* Emits a ScriptHashUpdate notification.
*
* @param scriptHash - Target script hash
* @param unspentOutputs - New unspent outputs list
* @param status - Optional script hash status marker
*/
setScriptHashUnspentOutputs(scriptHash: ScriptHash, unspentOutputs: ScriptHashListUnspentResponse, status?: ScriptHashStatus): void {
this.scriptHashUnspentOutputs.set(scriptHash, structuredClone(unspentOutputs));
this.events.emit('ScriptHashUpdate', {
scriptHash,
status: status ?? ('' as ScriptHashStatus),
});
}
/**
* Gets a previously broadcast transaction hex if present.
*
* @param transactionHash - Transaction hash
* @returns Transaction hex if present
*/
getBroadcastTransaction(transactionHash: TransactionHash): TransactionHex | undefined {
const transaction = this.transactions.get(transactionHash);
return transaction === undefined ? undefined : structuredClone(transaction);
}
/**
* Emits the current chain status.
*/
private emitChainStatus(): void {
this.events.emit('ChainStatus', { chainStatus: structuredClone(this.chainStatus) });
}
/**
* Ensures the provider is initialized.
*
* @throws {Error} if provider is not initialized
*/
private assertInitialized(): void {
if (!this.initialized) {
throw new Error('InMemoryBlockchainProvider is not initialized.');
}
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ type InstanceChanged = ResourceInstance & {
}; };
function logSyncServer(message: string): void { function logSyncServer(message: string): void {
console.error(`[SyncServer] ${message}`); // console.error(`[SyncServer] ${message}`);
} }
function errorMessage(error: unknown): string { function errorMessage(error: unknown): string {
+38 -19
View File
@@ -160,7 +160,7 @@ describe("invitation command - error cases", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-errors-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-errors-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -201,7 +201,7 @@ describe("invitation command - receive flow", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-receive-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-receive-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -276,7 +276,7 @@ describe("invitation command - receive flow", () => {
await handleInvitationCommand( await handleInvitationCommand(
createCommandDeps(app, io, paths), createCommandDeps(app, io, paths),
["create", "Wallet (P2PKH)", "receive"], ["create", "Wallet (P2PKH)", "receive"],
{}, { varTransferredSatoshis: "10000", role: "receiver" },
); );
expectLogs(spies, [{ out: "All requirements satisfied" }]); expectLogs(spies, [{ out: "All requirements satisfied" }]);
@@ -314,7 +314,7 @@ describe("invitation command - request satoshis flow", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-request-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-request-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -410,7 +410,7 @@ describe("invitation command - send flow with resources", () => {
engine = mockEngine.engine; engine = mockEngine.engine;
state = mockEngine.state; state = mockEngine.state;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-send-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-send-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -489,7 +489,7 @@ describe("invitation command - send flow with resources", () => {
varRecipientLockingscript: varRecipientLockingscript:
"76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac", "76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac",
addInput: addInput:
"0000000000000000000000000000000000000000000000000000000000000000:0", "0000000000000000000000000000000000000000000000000000000000000001:0",
role: "sender", role: "sender",
}, },
); );
@@ -536,7 +536,7 @@ describe("invitation command - multi-step append", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-append-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-append-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -684,7 +684,7 @@ describe("invitation command - list and inspect", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-list-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-list-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -853,7 +853,7 @@ describe("invitation command - sign flow", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-sign-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-sign-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -930,7 +930,7 @@ describe("invitation command - sign flow", () => {
const result = await handleInvitationCommand( const result = await handleInvitationCommand(
createCommandDeps(app, io, paths), createCommandDeps(app, io, paths),
["create", "Wallet (P2PKH)", "receive"], ["create", "Wallet (P2PKH)", "receive"],
{ sign: "true" }, { sign: "true", varTransferredSatoshis: "10000", role: "receiver" },
); );
expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/); expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
@@ -940,6 +940,24 @@ describe("invitation command - sign flow", () => {
]); ]);
}); });
/**
* Tests the --sign flag on create will not auto-signs when requirements are not satisfied.
*/
test("--sign flag auto-signs on create", async () => {
const { io, spies } = createMockIO();
const result = await handleInvitationCommand(
createCommandDeps(app, io, paths),
["create", "Wallet (P2PKH)", "receive"],
{ sign: "true" },
);
expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
expectLogs(spies, [
{ out: "Remaining requirements" },
]);
});
/** /**
* Tests that signing fails when invitation doesn't exist. * Tests that signing fails when invitation doesn't exist.
*/ */
@@ -969,12 +987,13 @@ describe("invitation command - import flow", () => {
let app: AppService; let app: AppService;
let tempDir: string; let tempDir: string;
let paths: CommandPaths; let paths: CommandPaths;
let mockEngine: any;
beforeEach(async () => { beforeEach(async () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-import-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-import-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -1001,7 +1020,7 @@ describe("invitation command - import flow", () => {
`inv-${createResult.invitationIdentifier}.json`, `inv-${createResult.invitationIdentifier}.json`,
); );
const secondApp = await createMockAppService(engine); const secondApp = await createMockAppService(engine, mockEngine.state);
const { io: importIO } = createMockIO(); const { io: importIO } = createMockIO();
const importResult = await handleInvitationCommand( const importResult = await handleInvitationCommand(
@@ -1048,7 +1067,7 @@ describe("invitation command - import flow", () => {
`inv-${createResult.invitationIdentifier}.json`, `inv-${createResult.invitationIdentifier}.json`,
); );
const secondApp = await createMockAppService(engine); const secondApp = await createMockAppService(engine, mockEngine.state);
const { io: importIO } = createMockIO(); const { io: importIO } = createMockIO();
await handleInvitationCommand( await handleInvitationCommand(
@@ -1077,7 +1096,7 @@ describe("invitation command - import flow", () => {
`inv-${createResult.invitationIdentifier}.json`, `inv-${createResult.invitationIdentifier}.json`,
); );
const secondApp = await createMockAppService(engine); const secondApp = await createMockAppService(engine, mockEngine.state);
const { io: importIO } = createMockIO(); const { io: importIO } = createMockIO();
await handleInvitationCommand( await handleInvitationCommand(
@@ -1106,7 +1125,7 @@ describe("invitation command - auto-inputs flow", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-autoinputs-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-autoinputs-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -1182,7 +1201,7 @@ describe("invitation command - broadcast flow", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-broadcast-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-broadcast-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -1247,7 +1266,7 @@ describe("invitation command - full lifecycle", () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-lifecycle-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-invitation-lifecycle-"));
paths = createMockPaths(tempDir); paths = createMockPaths(tempDir);
}); });
@@ -1402,7 +1421,7 @@ describe("invitation command - full lifecycle", () => {
const result = await handleInvitationCommand( const result = await handleInvitationCommand(
createCommandDeps(app, io, paths), createCommandDeps(app, io, paths),
["create", "Wallet (P2PKH)", "receive"], ["create", "Wallet (P2PKH)", "receive"],
{ sign: "true" }, { sign: "true", varTransferredSatoshis: "10000", role: "receiver" },
); );
expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/); expect(result.invitationIdentifier).toMatch(/^[a-f0-9]{32}$/);
+2 -2
View File
@@ -85,7 +85,7 @@ describe("receive command", () => {
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-receive-tests-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-receive-tests-"));
}); });
@@ -139,4 +139,4 @@ describe("receive command", () => {
} }
}, },
); );
}); }, 1000);
+31 -14
View File
@@ -11,7 +11,7 @@ import {
reserveResource, reserveResource,
} from "../mocks/engine"; } from "../mocks/engine";
import { type Engine } from "@xo-cash/engine"; import { type Engine } from "@xo-cash/engine";
import { p2pkhTemplate } from "../mocks/template-p2pkh"; import { p2pkhTemplate, p2pkhTemplateIdentifier } from "../mocks/template-p2pkh";
import { AppService } from "../../../src/services/app"; import { AppService } from "../../../src/services/app";
import { handleResourceCommand } from "../../../src/cli/commands/resource"; import { handleResourceCommand } from "../../../src/cli/commands/resource";
@@ -125,7 +125,7 @@ describe("resource command", () => {
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-resource-tests-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-resource-tests-"));
}); });
@@ -192,7 +192,7 @@ describe("resource command with populated data", () => {
engine = mockEngine.engine; engine = mockEngine.engine;
state = mockEngine.state; state = mockEngine.state;
await engine.importTemplate(p2pkhTemplate); await engine.importTemplate(p2pkhTemplate);
app = await createMockAppService(engine); app = await createMockAppService(engine, state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-resource-tests-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-resource-tests-"));
}); });
@@ -283,9 +283,17 @@ describe("resource command with populated data", () => {
}); });
test("unreserve releases a reserved UTXO", async () => { test("unreserve releases a reserved UTXO", async () => {
// Create an invitation
const invitation = await engine.createInvitation({
templateIdentifier: p2pkhTemplateIdentifier,
actionIdentifier: "receive",
variables: [],
});
const invitationInstance = await app.createInvitation(invitation);
const resource = await addFakeResource(state, { const resource = await addFakeResource(state, {
valueSatoshis: 25000, valueSatoshis: 25000,
reservedBy: "inv-123", reservedBy: invitationInstance.data.invitationIdentifier,
}); });
const { io, spies } = createMockIO(); const { io, spies } = createMockIO();
@@ -299,8 +307,8 @@ describe("resource command with populated data", () => {
); );
expectLogs(spies, [ expectLogs(spies, [
{ out: "Unreserved" }, { out: "Archived invitation" },
{ out: "was reserved for inv-123" }, { out: invitationInstance.data.invitationIdentifier },
]); ]);
const resources = await engine.listUnspentOutputsData(); const resources = await engine.listUnspentOutputsData();
@@ -327,15 +335,24 @@ describe("resource command with populated data", () => {
}); });
test("unreserve-all releases all reserved UTXOs", async () => { test("unreserve-all releases all reserved UTXOs", async () => {
// Create 2 fake invitations
const invitation1 = await engine.createInvitation({
templateIdentifier: p2pkhTemplateIdentifier,
actionIdentifier: "receive",
variables: [],
});
const invitation2 = await engine.createInvitation({
templateIdentifier: p2pkhTemplateIdentifier,
actionIdentifier: "receive",
variables: [],
});
const invitationInstance1 = await app.createInvitation(invitation1);
const invitationInstance2 = await app.createInvitation(invitation2);
await addFakeResource(state, { valueSatoshis: 50000 }); await addFakeResource(state, { valueSatoshis: 50000 });
await addFakeResource(state, { await addFakeResource(state, { valueSatoshis: 25000, reservedBy: invitationInstance1.data.invitationIdentifier });
valueSatoshis: 25000, await addFakeResource(state, { valueSatoshis: 10000, reservedBy: invitationInstance2.data.invitationIdentifier });
reservedBy: "inv-123",
});
await addFakeResource(state, {
valueSatoshis: 10000,
reservedBy: "inv-456",
});
const { io, spies } = createMockIO(); const { io, spies } = createMockIO();
const result = await handleResourceCommand( const result = await handleResourceCommand(
+5 -2
View File
@@ -204,9 +204,12 @@ describe("template command", () => {
beforeEach(async () => { beforeEach(async () => {
const mockEngine = await createMockEngine(DEFAULT_SEED); const mockEngine = await createMockEngine(DEFAULT_SEED);
engine = mockEngine.engine; engine = mockEngine.engine;
await engine.importTemplate(p2pkhTemplate); const { templateIdentifier } = await engine.importTemplate(p2pkhTemplate);
if (templateIdentifier !== p2pkhTemplateIdentifier) {
throw new Error("Template identifier mismatch");
}
app = await createMockAppService(engine); app = await createMockAppService(engine, mockEngine.state);
tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-template-tests-")); tempDir = mkdtempSync(path.join(tmpdir(), "xo-cli-template-tests-"));
}); });
+17 -4
View File
@@ -10,7 +10,7 @@ import {
UnspentOutputStatus, UnspentOutputStatus,
type UnspentOutputData, type UnspentOutputData,
} from "@xo-cash/state"; } from "@xo-cash/state";
import { InMemoryBlockchainProvider } from "@xo-cash/engine"; import { InMemoryBlockchainProvider } from "../../../src/utils/blockchain/in-memory-blockchain-provider.js";
import { convertMnemonicToSeedBytes } from "@xo-cash/crypto"; import { convertMnemonicToSeedBytes } from "@xo-cash/crypto";
import { binToHex, sha256 } from "@bitauth/libauth"; import { binToHex, sha256 } from "@bitauth/libauth";
@@ -20,6 +20,7 @@ import { MockElectrumService } from "./electrum-service";
import { MockRatesService } from "./rates-service"; import { MockRatesService } from "./rates-service";
import { RatesService } from "../../../src/services/rates"; import { RatesService } from "../../../src/services/rates";
import { SettingsService } from "../../../src/services/settings"; import { SettingsService } from "../../../src/services/settings";
import { mkdirSync } from "node:fs";
export const DEFAULT_SEED = export const DEFAULT_SEED =
"oven crop same above under tower promote decrease vocal pretty require slow"; "oven crop same above under tower promote decrease vocal pretty require slow";
@@ -68,6 +69,7 @@ export const addFakeResource = async (
options: FakeResourceOptions = {}, options: FakeResourceOptions = {},
): Promise<UnspentOutputData> => { ): Promise<UnspentOutputData> => {
const resource: UnspentOutputData = { const resource: UnspentOutputData = {
templateIdentifier: options.templateIdentifier ?? "test-template",
status: UnspentOutputStatus.CONFIRMED, status: UnspentOutputStatus.CONFIRMED,
selectable: true, selectable: true,
privacy: false, privacy: false,
@@ -131,10 +133,16 @@ export const unreserveResource = async (
* @returns A mock engine instance. * @returns A mock engine instance.
*/ */
export const createMockEngine = async (seed: string) => { export const createMockEngine = async (seed: string) => {
const randomId = Math.random().toString(36).substring(2, 15);
const stateDir = `${tmpdir()}/test-data-${randomId}`;
mkdirSync(stateDir, { recursive: true });
// Create the in-memory storage adapter. // Create the in-memory storage adapter.
const storage = await createStorageAdapter({ const storage = await createStorageAdapter({
storageType: "inmemory", storageType: StorageType.INDEXEDDB,
accountHash: binToHex(sha256.hash(convertMnemonicToSeedBytes(seed))), accountHash: binToHex(sha256.hash(convertMnemonicToSeedBytes(seed))),
databasePath: stateDir,
databaseFilename: `xo-wallet-${randomId}.db`,
}); });
// Initialize the storage adapter. // Initialize the storage adapter.
@@ -152,13 +160,17 @@ export const createMockEngine = async (seed: string) => {
await blockchainMonitor.initializeEventListeners(); await blockchainMonitor.initializeEventListeners();
// Create the engine instance. // Create the engine instance.
const engine = new Engine(seed, state, blockchainMonitor, blockchainProvider); const engine = new Engine(seed, state, blockchainProvider, blockchainMonitor);
await engine.initializeStateSync(); await engine.initializeStateSync();
return { engine, state, blockchainMonitor, blockchainProvider }; return { engine, state, blockchainMonitor, blockchainProvider };
}; };
export const createMockAppService = async (engine: Engine) => { export const createMockAppService = async (engine: Engine, state: State) => {
if (!state) {
throw new Error("State is required");
}
const settings = new SettingsService( const settings = new SettingsService(
`${tmpdir()}/xo-cli-tests-settings.json`, `${tmpdir()}/xo-cli-tests-settings.json`,
); );
@@ -187,6 +199,7 @@ export const createMockAppService = async (engine: Engine) => {
mockElectrum, mockElectrum,
rates, rates,
settings, settings,
state,
new Uint8Array(32).fill(1), new Uint8Array(32).fill(1),
); );
}; };
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -37,7 +37,7 @@ describe("loadTemplateFromFile", () => {
test("loads TypeScript templates via child process", async () => { test("loads TypeScript templates via child process", async () => {
const tsTemplatePath = path.resolve( const tsTemplatePath = path.resolve(
process.cwd(), process.cwd(),
"../templates/source/p2pkh.ts", "./tests/cli/mocks/template-p2pkh.ts",
); );
expect(existsSync(tsTemplatePath)).toBe(true); expect(existsSync(tsTemplatePath)).toBe(true);
+3 -3
View File
@@ -1,5 +1,5 @@
import { binToHex } from "@bitauth/libauth"; import { binToHex } from "@bitauth/libauth";
import { serializeInvitation } from "@xo-cash/engine"; import { deserializeInvitation, serializeInvitation } from "@xo-cash/engine";
import type { XOInvitation, XOInvitationCommit } from "@xo-cash/types"; import type { XOInvitation, XOInvitationCommit } from "@xo-cash/types";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -135,7 +135,7 @@ describe("SyncServer v2 adapter", () => {
invitationIdentifier, invitationIdentifier,
new Uint8Array(32).fill(1), new Uint8Array(32).fill(1),
); );
const messages: Array<{ event?: string; data: string }> = []; const messages: Array<{ event?: string; data: XOInvitation }> = [];
sync.on("message", (message) => messages.push(message)); sync.on("message", (message) => messages.push(message));
client.onMessage?.( client.onMessage?.(
@@ -149,6 +149,6 @@ describe("SyncServer v2 adapter", () => {
expect(messages).toHaveLength(1); expect(messages).toHaveLength(1);
expect(messages[0]?.event).toBe("invitation-updated"); expect(messages[0]?.event).toBe("invitation-updated");
expect(messages[0]?.data).toBe(serializeInvitation(updated)); expect(messages[0]?.data).toEqual(deserializeInvitation(serializeInvitation(updated)));
}); });
}); });