Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mock provider for unit tests #371

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 296 additions & 0 deletions src/_tests/unit/mockProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
Provider,
Network,
TransactionResponse,
Block,
BlockTag,
ProviderEvent,
TransactionReceipt,
Log,
FeeData,
} from '../../providers/index.js';
import { AddressLike } from '../../address/index.js';
import { Shard, Zone } from '../../constants/index.js';
import { Listener, EventPayload, EventEmitterable, getBytes } from '../../utils/index.js';
import { Outpoint, OutpointDeltas } from '../../transaction/utxo.js';
import { AccessList, QiTransaction, QuaiTransaction } from '../../transaction/index.js';
import { WorkObjectLike } from '../../transaction/work-object.js';
import { QiPerformActionTransaction } from '../../providers/abstract-provider.js';
import { txpoolContentResponse, txpoolInspectResponse } from '../../providers/txpool.js';
import { QiTransactionResponse } from '../../providers/provider.js';
import { decodeProtoTransaction } from '../../encoding/index.js';
import { QiTransactionResponseParams } from '../../providers/formatting.js';

export class MockProvider implements Provider {
private _network: Network = Network.from(BigInt(1));
private _blockNumber: number = 1000;
private _transactions: Map<string, TransactionResponse> = new Map();
private _blocks: Map<string, Block> = new Map();
private _balances: Map<string, bigint> = new Map();
private _eventHandlers: Map<
string,
Array<{
listener: Listener;
once: boolean;
zone?: Zone;
}>
> = new Map();

// Required by Provider interface but not used in QiHDWallet tests
provider = this;

constructor(config?: { network?: bigint; blockNumber?: number }) {
if (config?.network) this._network = Network.from(config.network);
if (config?.blockNumber) this._blockNumber = config.blockNumber;
}

// Helper methods to set up test state
public setBalance(address: string, balance: bigint): void {
this._balances.set(address.toLowerCase(), balance);
}

public setTransaction(hash: string, tx: TransactionResponse): void {
this._transactions.set(hash.toLowerCase(), tx);
}

// Implementation of Provider interface methods
async getNetwork(): Promise<Network> {
return this._network;
}

async getBlockNumber(shard: Shard): Promise<number> {
return this._blockNumber;
}

async getBalance(address: AddressLike, blockTag?: BlockTag): Promise<bigint> {
return this._balances.get(address.toString().toLowerCase()) ?? BigInt(0);
}

async broadcastTransaction(zone: Zone, signedTx: string, from?: AddressLike): Promise<TransactionResponse> {
// Simulate transaction broadcast
const type = decodeProtoTransaction(getBytes(signedTx)).type;

const tx = type == 2 ? QiTransaction.from(signedTx) : QuaiTransaction.from(signedTx);
const txObj = tx.toJSON();

if (type == 2) {
return new QiTransactionResponse(txObj as QiTransactionResponseParams, this);
}

// TODO: Implement _wrapTransactionResponse for QuaiTransactionResponse
throw new Error('Broadcast transaction not implemented for Quai');
}

async getOutpointDeltas(): Promise<OutpointDeltas> {
// TODO: Implement
throw new Error('Method not implemented.');
}

async getOutpointsByAddress(): Promise<Array<Outpoint>> {
// TODO: Implement
throw new Error('Method not implemented.');
}

async getBlock(shard: Shard, blockHashOrBlockTag: BlockTag | string, prefetchTxs?: boolean): Promise<null | Block> {
if (typeof blockHashOrBlockTag === 'string') {
return this._blocks.get(blockHashOrBlockTag) ?? null;
}
// Handle block tag (latest, pending, etc.)
throw new Error('Method not implemented.');
}

async estimateFeeForQi(tx: QiPerformActionTransaction): Promise<bigint> {
// Return a mock fee for testing
return BigInt(1000);
}

async on(event: ProviderEvent, listener: Listener, zone?: Zone): Promise<this> {
const eventKey = this._getEventKey(event, zone);
if (!this._eventHandlers.has(eventKey)) {
this._eventHandlers.set(eventKey, []);
}
this._eventHandlers.get(eventKey)!.push({ listener, once: false, zone });
return this;
}

async once(event: ProviderEvent, listener: Listener, zone?: Zone): Promise<this> {
const eventKey = this._getEventKey(event, zone);
if (!this._eventHandlers.has(eventKey)) {
this._eventHandlers.set(eventKey, []);
}
this._eventHandlers.get(eventKey)!.push({ listener, once: true, zone });
return this;
}

async emit(event: ProviderEvent, zone?: Zone, ...args: Array<any>): Promise<boolean> {
const eventKey = this._getEventKey(event, zone);
const handlers = this._eventHandlers.get(eventKey);
if (!handlers || handlers.length === 0) {
return false;
}

const payload = new EventPayload<ProviderEvent>(this as EventEmitterable<ProviderEvent>, null, event);

const remainingHandlers = handlers.filter(({ listener, once }) => {
try {
listener.call(this, ...args, payload);
return !once;
} catch (error) {
console.error('Error in event handler:', error);
return true;
}
});

this._eventHandlers.set(eventKey, remainingHandlers);
return true;
}

async listenerCount(event?: ProviderEvent): Promise<number> {
if (!event) {
// Sum up all listeners across all events
return Array.from(this._eventHandlers.values()).reduce((sum, handlers) => sum + handlers.length, 0);
}
const handlers = this._eventHandlers.get(this._getEventKey(event));
return handlers?.length ?? 0;
}

async listeners(event?: ProviderEvent): Promise<Array<Listener>> {
if (!event) {
// Get all listeners across all events
return Array.from(this._eventHandlers.values())
.flat()
.map(({ listener }) => listener);
}
const handlers = this._eventHandlers.get(this._getEventKey(event));
return handlers?.map(({ listener }) => listener) ?? [];
}

async off(event: ProviderEvent, listener?: Listener, zone?: Zone): Promise<this> {
const eventKey = this._getEventKey(event, zone);
if (!listener) {
// Remove all listeners for this event
this._eventHandlers.delete(eventKey);
} else {
const handlers = this._eventHandlers.get(eventKey);
if (handlers) {
const remainingHandlers = handlers.filter((h) => h.listener !== listener);
this._eventHandlers.set(eventKey, remainingHandlers);
}
}
return this;
}

async removeAllListeners(event?: ProviderEvent): Promise<this> {
if (!event) {
this._eventHandlers.clear();
} else {
this._eventHandlers.delete(this._getEventKey(event));
}
return this;
}

async addListener(event: ProviderEvent, listener: Listener, zone?: Zone): Promise<this> {
return this.on(event, listener, zone);
}

async removeListener(event: ProviderEvent, listener: Listener, zone?: Zone): Promise<this> {
return this.off(event, listener, zone);
}

// Helper method to create consistent event keys
private _getEventKey(event: ProviderEvent, zone?: Zone): string {
if (typeof event === 'string') {
return zone ? `${event}:${zone}` : event;
}

if ('orphan' in event) {
return zone ? `orphan:${zone}` : 'orphan';
}
if ('transaction' in event) {
return zone ? `transaction:${event.transaction}:${zone}` : `transaction:${event.transaction}`;
}
if ('qiTransaction' in event) {
return zone ? `qiTransaction:${event.qiTransaction}:${zone}` : `qiTransaction:${event.qiTransaction}`;
}
if ('filter' in event) {
return zone ? `filter:${JSON.stringify(event.filter)}:${zone}` : `filter:${JSON.stringify(event.filter)}`;
}

return JSON.stringify(event);
}

// Stub implementations for other required methods
destroy(): void {
// No-op for mock
}

async getFeeData(_zone: Zone, _txType: boolean): Promise<FeeData> {
throw new Error('Method not implemented.');
}

async getPendingHeader(): Promise<WorkObjectLike> {
throw new Error('Method not implemented.');
}

async getLockedBalance(): Promise<bigint> {
throw new Error('Method not implemented.');
}
async getTransactionCount(): Promise<number> {
throw new Error('Method not implemented.');
}
async getCode(): Promise<string> {
throw new Error('Method not implemented.');
}
async getStorage(): Promise<string> {
throw new Error('Method not implemented.');
}
async estimateGas(): Promise<bigint> {
throw new Error('Method not implemented.');
}
async createAccessList(): Promise<AccessList> {
throw new Error('Method not implemented.');
}
async call(): Promise<string> {
throw new Error('Method not implemented.');
}
async getTransaction(): Promise<null | TransactionResponse> {
throw new Error('Method not implemented.');
}
async getTransactionReceipt(): Promise<null | TransactionReceipt> {
throw new Error('Method not implemented.');
}
async getTransactionResult(): Promise<null | string> {
throw new Error('Method not implemented.');
}
async getLogs(): Promise<Array<Log>> {
throw new Error('Method not implemented.');
}
async waitForTransaction(): Promise<null | TransactionReceipt> {
throw new Error('Method not implemented.');
}
async waitForBlock(): Promise<Block> {
throw new Error('Method not implemented.');
}
async getProtocolExpansionNumber(): Promise<number> {
throw new Error('Method not implemented.');
}
async getTxPoolContent(zone: Zone): Promise<txpoolContentResponse> {
throw new Error('Method not implemented.');
}
async txPoolInspect(zone: Zone): Promise<txpoolInspectResponse> {
throw new Error('Method not implemented.');
}
async getQiRateAtBlock(): Promise<bigint> {
throw new Error('Method not implemented.');
}
async getLatestQiRate(): Promise<bigint> {
throw new Error('Method not implemented.');
}
async getQuaiRateAtBlock(): Promise<bigint> {
throw new Error('Method not implemented.');
}
async getLatestQuaiRate(): Promise<bigint> {
throw new Error('Method not implemented.');
}
}
Loading
Loading