Skip to content
Closed
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
265 changes: 108 additions & 157 deletions packages/btcindexer/src/btcindexer.helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Miniflare } from "miniflare";
import { Block, type Transaction } from "bitcoinjs-lib";
import { Block } from "bitcoinjs-lib";
import { expect } from "bun:test";
import type { D1Database, KVNamespace } from "@cloudflare/workers-types";

import { Indexer } from "./btcindexer";
import { CFStorage } from "./cf-storage";
Expand Down Expand Up @@ -54,142 +53,118 @@ interface SetupOptions {
testData?: TestBlocks;
}

export interface TestIndexerHelper {
interface MfBindings {
DB: D1Database;
BtcBlocks: KVNamespace;
nbtc_txs: KVNamespace;
}

export class TesSuiteHelper {
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name "TesSuiteHelper" contains a typo. It should be "TestSuiteHelper" instead of "TesSuiteHelper" (missing 't' in "Test").

Suggested change
export class TesSuiteHelper {
export class TestSuiteHelper {

Copilot uses AI. Check for mistakes.
indexer: Indexer;
db: D1Database;
blocksKV: KVNamespace;
txsKV: KVNamespace;
storage: CFStorage;
mockSuiClient: MockSuiClient;
mockElectrs: Electrs;
private testData: TestBlocks;
private options: SetupOptions;

constructor(options: SetupOptions = {}) {
this.testData = options.testData || {};
this.options = options;
this.db = null!;
this.blocksKV = null!;
this.txsKV = null!;
this.storage = null!;
this.mockSuiClient = null!;
this.mockElectrs = null!;
this.indexer = null!;
Comment on lines +76 to +82
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initializing properties with null! (non-null assertion) is unsafe. These properties are not actually initialized until init() is called, which could lead to runtime errors if any class methods are called before init(). Consider making init() required in the constructor, or use optional types (e.g., indexer?: Indexer) to better reflect the actual state.

Copilot uses AI. Check for mistakes.
}

setupBlock: (height: number) => Promise<void>;
getBlock: (height: number) => Block;
getTx: (
height: number,
txIndex: number,
) => {
blockData: TestBlock;
block: Block;
targetTx: Transaction;
txInfo: TxInfo;
};
createBlockQueueRecord: (
height: number,
options?: Partial<BlockQueueRecord>,
) => BlockQueueRecord;

mockElectrsSender: (address: string) => void;
mockElectrsError: (error: Error) => void;
mockSuiMintBatch: (result: [boolean, string] | null) => void;
async init(mf: Miniflare): Promise<void> {
this.db = await mf.getD1Database("DB");
await initDb(this.db);

insertTx: (options: {
txId: string;
status: MintTxStatus | string;
retryCount?: number;
blockHeight?: number;
blockHash?: string;
suiRecipient?: string;
amountSats?: number;
depositAddress?: string;
sender?: string;
vout?: number;
}) => Promise<void>;
const env = (await mf.getBindings()) as MfBindings;
this.storage = new CFStorage(env.DB, env.BtcBlocks, env.nbtc_txs);
this.blocksKV = env.BtcBlocks;
this.txsKV = env.nbtc_txs;

expectMintingCount: (count: number) => Promise<void>;
expectSenderCount: (count: number, expectedAddress?: string) => Promise<void>;
expectTxStatus: (txId: string, expectedStatus: MintTxStatus | string) => Promise<void>;
}
const packageConfig: NbtcPkgCfg = this.options.packageConfig || TEST_PACKAGE_CONFIG;

// test suite helper functions constructor.
export async function setupTestIndexerSuite(
mf: Miniflare,
options: SetupOptions = {},
): Promise<TestIndexerHelper> {
const testData = options.testData || {};

const db = await mf.getD1Database("DB");
await initDb(db);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const env = (await mf.getBindings()) as any;
const storage = new CFStorage(env.DB, env.BtcBlocks, env.nbtc_txs);
const blocksKV = env.BtcBlocks as KVNamespace;
const txsKV = env.nbtc_txs as KVNamespace;

const packageConfig: NbtcPkgCfg = options.packageConfig || TEST_PACKAGE_CONFIG;

await db
.prepare(
`INSERT INTO setups (
id, btc_network, sui_network, nbtc_pkg, nbtc_contract,
lc_pkg, lc_contract,
sui_fallback_address, is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.bind(
packageConfig.id,
packageConfig.btc_network,
packageConfig.sui_network,
packageConfig.nbtc_pkg,
packageConfig.nbtc_contract,
packageConfig.lc_pkg,
packageConfig.lc_contract,
packageConfig.sui_fallback_address,
packageConfig.is_active,
)
.run();

const nbtcAddressesMap: NbtcDepositAddrsMap = new Map();
const depositAddresses = options.depositAddresses || [];

for (const addr of depositAddresses) {
await db
await this.db
.prepare(
`INSERT INTO nbtc_deposit_addresses (setup_id, deposit_address, is_active)
VALUES (?, ?, 1)`,
`INSERT INTO setups (
id, btc_network, sui_network, nbtc_pkg, nbtc_contract,
lc_pkg, lc_contract,
sui_fallback_address, is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.bind(
packageConfig.id,
packageConfig.btc_network,
packageConfig.sui_network,
packageConfig.nbtc_pkg,
packageConfig.nbtc_contract,
packageConfig.lc_pkg,
packageConfig.lc_contract,
packageConfig.sui_fallback_address,
packageConfig.is_active,
)
.bind(packageConfig.id, addr)
.run();

nbtcAddressesMap.set(addr, {
setup_id: packageConfig.id,
is_active: true,
});
const nbtcAddressesMap: NbtcDepositAddrsMap = new Map();
const depositAddresses = this.options.depositAddresses || [];

for (const addr of depositAddresses) {
await this.db
.prepare(
`INSERT INTO nbtc_deposit_addresses (setup_id, deposit_address, is_active)
VALUES (?, ?, 1)`,
)
.bind(packageConfig.id, addr)
.run();

nbtcAddressesMap.set(addr, {
setup_id: packageConfig.id,
is_active: true,
});
}

const suiClients = new Map<SuiNet, SuiClientI>();
this.mockSuiClient = this.options.customSuiClient || new MockSuiClient();
suiClients.set(toSuiNet(packageConfig.sui_network), this.mockSuiClient);

const electrsClients = new Map<BtcNet, Electrs>();
this.mockElectrs = mkElectrsServiceMock();
electrsClients.set(BtcNet.REGTEST, this.mockElectrs);

this.indexer = new Indexer(
this.storage,
[packageConfig],
suiClients,
nbtcAddressesMap,
this.options.confirmationDepth || 8,
this.options.maxRetries || 2,
electrsClients,
);
}

const suiClients = new Map<SuiNet, SuiClientI>();
const mockSuiClient = options.customSuiClient || new MockSuiClient();
suiClients.set(toSuiNet(packageConfig.sui_network), mockSuiClient);

const electrsClients = new Map<BtcNet, Electrs>();
const mockElectrs = mkElectrsServiceMock();
electrsClients.set(BtcNet.REGTEST, mockElectrs);

const indexer = new Indexer(
storage,
[packageConfig],
suiClients,
nbtcAddressesMap,
options.confirmationDepth || 8,
options.maxRetries || 2,
electrsClients,
);

const setupBlock = async (height: number): Promise<void> => {
const blockData = testData[height];
setupBlock = async (height: number): Promise<void> => {
const blockData = this.testData[height];
if (!blockData) throw new Error(`Block ${height} not found in test data`);
await blocksKV.put(blockData.hash, Buffer.from(blockData.rawBlockHex, "hex").buffer);
await this.blocksKV.put(blockData.hash, Buffer.from(blockData.rawBlockHex, "hex").buffer);
};

const getBlock = (height: number): Block => {
const blockData = testData[height];
getBlock = (height: number): Block => {
const blockData = this.testData[height];
if (!blockData) throw new Error(`Block ${height} not found in test data`);
return Block.fromHex(blockData.rawBlockHex);
};

const getTx = (height: number, txIndex: number) => {
const blockData = testData[height];
getTx = (height: number, txIndex: number) => {
const blockData = this.testData[height];
if (!blockData) throw new Error(`Block ${height} not found in test data`);

const block = Block.fromHex(blockData.rawBlockHex);
Expand All @@ -202,11 +177,11 @@ export async function setupTestIndexerSuite(
return { blockData, block, targetTx, txInfo };
};

const createBlockQueueRecord = (
createBlockQueueRecord = (
height: number,
options?: Partial<BlockQueueRecord>,
): BlockQueueRecord => {
const blockData = testData[height];
const blockData = this.testData[height];
if (!blockData) throw new Error(`Block ${height} not found in test data`);

return {
Expand All @@ -217,9 +192,9 @@ export async function setupTestIndexerSuite(
};
};

const mockElectrsSender = (address: string): void => {
mockElectrsSender = (address: string): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockElectrs.getTx as any).mockResolvedValue(
(this.mockElectrs.getTx as any).mockResolvedValue(
new Response(
JSON.stringify({
vout: [{ scriptpubkey_address: address }],
Expand All @@ -228,16 +203,16 @@ export async function setupTestIndexerSuite(
);
};

const mockElectrsError = (error: Error): void => {
mockElectrsError = (error: Error): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockElectrs.getTx as any).mockRejectedValue(error);
(this.mockElectrs.getTx as any).mockRejectedValue(error);
};

const mockSuiMintBatch = (result: [boolean, string] | null): void => {
mockSuiClient.tryMintNbtcBatch.mockResolvedValue(result);
mockSuiMintBatch = (result: [boolean, string] | null): void => {
this.mockSuiClient.tryMintNbtcBatch.mockResolvedValue(result);
};

const insertTx = async (options: {
insertTx = async (options: {
txId: string;
status: MintTxStatus | string;
retryCount?: number;
Expand All @@ -249,13 +224,13 @@ export async function setupTestIndexerSuite(
sender?: string;
vout?: number;
}): Promise<void> => {
const defaultBlock = testData[329] || testData[327] || Object.values(testData)[0];
const defaultBlock =
this.testData[329] || this.testData[327] || Object.values(this.testData)[0];
if (!defaultBlock) throw new Error("No test data available for default values");

const depositAddr = options.depositAddress || defaultBlock.depositAddr;

// Validate that the deposit address exists in the database
const addressResult = await db
const addressResult = await this.db
.prepare(`SELECT id FROM nbtc_deposit_addresses WHERE deposit_address = ?`)
.bind(depositAddr)
.first<{ id: number }>();
Expand All @@ -267,7 +242,7 @@ export async function setupTestIndexerSuite(
);
}

await db
await this.db
.prepare(
`INSERT INTO nbtc_minting (tx_id, address_id, sender, vout, block_hash, block_height, sui_recipient, amount_sats, status, created_at, updated_at, retry_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
Expand All @@ -289,51 +264,27 @@ export async function setupTestIndexerSuite(
.run();
};

const expectMintingCount = async (count: number): Promise<void> => {
const { results } = await db.prepare("SELECT * FROM nbtc_minting").all();
expectMintingCount = async (count: number): Promise<void> => {
const { results } = await this.db.prepare("SELECT * FROM nbtc_minting").all();
expect(results.length).toEqual(count);
};

const expectSenderCount = async (count: number, expectedAddress?: string): Promise<void> => {
const { results } = await db.prepare("SELECT * FROM nbtc_minting").all();
expectSenderCount = async (count: number, expectedAddress?: string): Promise<void> => {
const { results } = await this.db.prepare("SELECT * FROM nbtc_minting").all();
const recordsWithSender = results.filter((r) => r.sender && r.sender !== "");
expect(recordsWithSender.length).toEqual(count);
if (expectedAddress && recordsWithSender[0]) {
expect(recordsWithSender[0].sender).toEqual(expectedAddress);
}
};

const expectTxStatus = async (
txId: string,
expectedStatus: MintTxStatus | string,
): Promise<void> => {
const { results } = await db
expectTxStatus = async (txId: string, expectedStatus: MintTxStatus | string): Promise<void> => {
const { results } = await this.db
.prepare("SELECT status FROM nbtc_minting WHERE tx_id = ?")
.bind(txId)
.all();
expect(results.length).toEqual(1);
expect(results[0]).toBeDefined();
expect(results[0]!.status).toEqual(expectedStatus);
};

return {
indexer,
db,
blocksKV,
txsKV,
storage,
mockSuiClient,
mockElectrs,
setupBlock,
getBlock,
getTx,
createBlockQueueRecord,
mockElectrsSender,
mockElectrsError,
mockSuiMintBatch,
insertTx,
expectMintingCount,
expectSenderCount,
expectTxStatus,
};
}
Loading
Loading