Skip to content

Commit 1715a5c

Browse files
committed
feat: add missing tests to btcindexer storage
Signed-off-by: Ravindra Meena <rmeena840@gmail.com>
1 parent 5e319dd commit 1715a5c

File tree

1 file changed

+306
-1
lines changed

1 file changed

+306
-1
lines changed

packages/btcindexer/src/storage.test.ts

Lines changed: 306 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { dropTables, initDb } from "@gonative-cc/lib/test-helpers/init_db";
77

88
import { fetchNbtcAddresses, fetchPackageConfigs } from "./storage";
99
import { CFStorage as CFStorageImpl } from "./cf-storage";
10-
import { MintTxStatus, InsertBlockStatus } from "./models";
10+
import { MintTxStatus, InsertBlockStatus, type NbtcBroadcastedDeposit } from "./models";
1111

1212
let mf: Miniflare;
1313

@@ -371,5 +371,310 @@ describe("CFStorage", () => {
371371
expect(txs.length).toBe(1);
372372
expect(txs[0]!.tx_id).toBe("tx1");
373373
});
374+
375+
it("getTxStatus should return status for existing tx", async () => {
376+
await storage.insertOrUpdateNbtcTxs([txBase]);
377+
const status = await storage.getTxStatus("tx1");
378+
expect(status).toBe(MintTxStatus.Confirming);
379+
});
380+
381+
it("getTxStatus should return null for non-existent tx", async () => {
382+
const status = await storage.getTxStatus("nonexistent");
383+
expect(status).toBeNull();
384+
});
385+
386+
it("insertOrUpdateNbtcTxs should handle empty array", async () => {
387+
expect(await storage.insertOrUpdateNbtcTxs([])).not.toBeNull();
388+
});
389+
390+
it("updateNbtcTxsStatus should handle empty array", async () => {
391+
expect(await storage.updateNbtcTxsStatus([], MintTxStatus.Minted)).not.toBeNull();
392+
});
393+
394+
it("finalizeNbtcTxs should handle empty array", async () => {
395+
expect(await storage.finalizeNbtcTxs([])).not.toBeNull();
396+
});
397+
398+
it("batchUpdateNbtcMintTxs should handle MintFailed status", async () => {
399+
await storage.insertOrUpdateNbtcTxs([txBase]);
400+
await storage.batchUpdateNbtcMintTxs([
401+
{
402+
txId: "tx1",
403+
vout: 0,
404+
status: MintTxStatus.MintFailed,
405+
suiTxDigest: "failedDigest",
406+
},
407+
]);
408+
const tx = await storage.getNbtcMintTx("tx1");
409+
expect(tx!.status).toBe(MintTxStatus.MintFailed);
410+
expect(tx!.retry_count).toBe(1);
411+
});
412+
413+
it("getNbtcMintCandidates should include failed txs within retry limit", async () => {
414+
await storage.insertOrUpdateNbtcTxs([txBase]);
415+
await storage.finalizeNbtcTxs(["tx1"]);
416+
await storage.batchUpdateNbtcMintTxs([
417+
{
418+
txId: "tx1",
419+
vout: 0,
420+
status: MintTxStatus.MintFailed,
421+
},
422+
]);
423+
424+
const candidates = await storage.getNbtcMintCandidates(3);
425+
expect(candidates.length).toBe(1);
426+
});
427+
428+
it("getNbtcMintCandidates should exclude failed txs exceeding retry limit", async () => {
429+
await storage.insertOrUpdateNbtcTxs([txBase]);
430+
await storage.finalizeNbtcTxs(["tx1"]);
431+
// Simulate multiple failures
432+
for (let i = 0; i < 4; i++) {
433+
await storage.batchUpdateNbtcMintTxs([
434+
{
435+
txId: "tx1",
436+
vout: 0,
437+
status: MintTxStatus.MintFailed,
438+
},
439+
]);
440+
}
441+
442+
const candidates = await storage.getNbtcMintCandidates(3);
443+
expect(candidates.length).toBe(0);
444+
});
445+
446+
it("insertOrUpdateNbtcTxs should update existing tx with new block info", async () => {
447+
await storage.insertOrUpdateNbtcTxs([txBase]);
448+
449+
const updatedTx = {
450+
...txBase,
451+
blockHash: "newBlockHash",
452+
blockHeight: 101,
453+
};
454+
await storage.insertOrUpdateNbtcTxs([updatedTx]);
455+
456+
const tx = await storage.getNbtcMintTx("tx1");
457+
expect(tx!.block_hash).toBe("newBlockHash");
458+
expect(tx!.block_height).toBe(101);
459+
expect(tx!.status).toBe(MintTxStatus.Confirming);
460+
});
461+
462+
it("registerBroadcastedNbtcTx should ignore duplicate broadcasts", async () => {
463+
const broadcast: NbtcBroadcastedDeposit = {
464+
txId: "txNoBlock",
465+
btcNetwork: BtcNet.REGTEST,
466+
suiNetwork: "devnet",
467+
nbtcPkg: "0xPkg1",
468+
depositAddress: "bcrt1qAddress1",
469+
sender: "sender",
470+
vout: 0,
471+
suiRecipient: "0xSui",
472+
amount: 1000,
473+
};
474+
475+
await storage.registerBroadcastedNbtcTx([broadcast]);
476+
await storage.registerBroadcastedNbtcTx([broadcast]); // duplicate
477+
478+
const tx = await storage.getNbtcMintTx("txNoBlock");
479+
expect(tx!.status).toBe(MintTxStatus.Broadcasting);
480+
});
481+
482+
it("updateNbtcTxsStatus should update multiple txs", async () => {
483+
const tx2 = { ...txBase, txId: "tx2", vout: 1 };
484+
await storage.insertOrUpdateNbtcTxs([txBase, tx2]);
485+
486+
await storage.updateNbtcTxsStatus(["tx1", "tx2"], MintTxStatus.Minted);
487+
488+
const tx1Result = await storage.getNbtcMintTx("tx1");
489+
const tx2Result = await storage.getNbtcMintTx("tx2");
490+
expect(tx1Result!.status).toBe(MintTxStatus.Minted);
491+
expect(tx2Result!.status).toBe(MintTxStatus.Minted);
492+
});
493+
494+
it("getConfirmingBlocks should not return blocks with null hash", async () => {
495+
await storage.registerBroadcastedNbtcTx([
496+
{
497+
txId: "txNoBlock",
498+
btcNetwork: BtcNet.REGTEST,
499+
suiNetwork: "devnet",
500+
nbtcPkg: "0xPkg1",
501+
depositAddress: "bcrt1qAddress1",
502+
sender: "sender",
503+
vout: 0,
504+
suiRecipient: "0xSui",
505+
amount: 1000,
506+
},
507+
]);
508+
509+
// Update to Confirming but no block hash
510+
await storage.updateNbtcTxsStatus(["txNoBlock"], MintTxStatus.Confirming);
511+
512+
const blocks = await storage.getConfirmingBlocks();
513+
expect(blocks.length).toBe(0);
514+
});
515+
516+
it("getNbtcMintTxsBySuiAddr should return empty for non-existent address", async () => {
517+
const txs = await storage.getNbtcMintTxsBySuiAddr("0xNonExistent");
518+
expect(txs.length).toBe(0);
519+
});
520+
521+
it("getNbtcMintTxsByBtcSender should return empty for non-existent sender", async () => {
522+
const txs = await storage.getNbtcMintTxsByBtcSender("nonexistent", BtcNet.REGTEST);
523+
expect(txs.length).toBe(0);
524+
});
525+
526+
it("getNbtcMintTx should return null for non-existent tx", async () => {
527+
const tx = await storage.getNbtcMintTx("nonexistent");
528+
expect(tx).toBeNull();
529+
});
530+
});
531+
532+
describe("Block Operations - Edge Cases", () => {
533+
it("getLatestBlockHeight should return null for empty database", async () => {
534+
const height = await storage.getLatestBlockHeight(BtcNet.REGTEST);
535+
expect(height).toBeNull();
536+
});
537+
538+
it("getChainTip should return null when not set", async () => {
539+
const tip = await storage.getChainTip(BtcNet.TESTNET);
540+
expect(tip).toBeNull();
541+
});
542+
543+
it("getBlock should return null for non-existent hash", async () => {
544+
const block = await storage.getBlock("nonexistent");
545+
expect(block).toBeNull();
546+
});
547+
548+
it("getBlockHash should return null for non-existent height", async () => {
549+
const hash = await storage.getBlockHash(999, BtcNet.REGTEST);
550+
expect(hash).toBeNull();
551+
});
552+
553+
it("getBlocksToProcess should return empty array when all processed", async () => {
554+
await storage.insertBlockInfo({
555+
hash: "hash1",
556+
height: 100,
557+
network: BtcNet.REGTEST,
558+
timestamp_ms: 1000,
559+
});
560+
await storage.markBlockAsProcessed("hash1", BtcNet.REGTEST);
561+
562+
const blocks = await storage.getBlocksToProcess(10);
563+
expect(blocks.length).toBe(0);
564+
});
565+
566+
it("getBlocksToProcess should respect batch size limit", async () => {
567+
for (let i = 0; i < 5; i++) {
568+
await storage.insertBlockInfo({
569+
hash: `hash${i}`,
570+
height: 100 + i,
571+
network: BtcNet.REGTEST,
572+
timestamp_ms: 1000 + i,
573+
});
574+
}
575+
576+
const blocks = await storage.getBlocksToProcess(3);
577+
expect(blocks.length).toBe(3);
578+
});
579+
580+
it("getBlocksToProcess should return blocks in ascending height order", async () => {
581+
await storage.insertBlockInfo({
582+
hash: "hash102",
583+
height: 102,
584+
network: BtcNet.REGTEST,
585+
timestamp_ms: 1002,
586+
});
587+
await storage.insertBlockInfo({
588+
hash: "hash100",
589+
height: 100,
590+
network: BtcNet.REGTEST,
591+
timestamp_ms: 1000,
592+
});
593+
await storage.insertBlockInfo({
594+
hash: "hash101",
595+
height: 101,
596+
network: BtcNet.REGTEST,
597+
timestamp_ms: 1001,
598+
});
599+
600+
const blocks = await storage.getBlocksToProcess(10);
601+
expect(blocks.length).toBe(3);
602+
expect(blocks[0]!.height).toBe(100);
603+
expect(blocks[1]!.height).toBe(101);
604+
expect(blocks[2]!.height).toBe(102);
605+
});
606+
607+
it("insertBlockInfo should handle same timestamp", async () => {
608+
await storage.insertBlockInfo({
609+
hash: "hash1",
610+
height: 100,
611+
network: BtcNet.REGTEST,
612+
timestamp_ms: 1000,
613+
});
614+
615+
const result = await storage.insertBlockInfo({
616+
hash: "hash2",
617+
height: 100,
618+
network: BtcNet.REGTEST,
619+
timestamp_ms: 1000,
620+
});
621+
622+
expect(result).toBe(InsertBlockStatus.Skipped);
623+
});
624+
625+
it("setChainTip and getChainTip should work for different networks", async () => {
626+
await storage.setChainTip(100, BtcNet.REGTEST);
627+
await storage.setChainTip(200, BtcNet.MAINNET);
628+
629+
expect(await storage.getChainTip(BtcNet.REGTEST)).toBe(100);
630+
expect(await storage.getChainTip(BtcNet.MAINNET)).toBe(200);
631+
});
632+
});
633+
634+
describe("Storage Helper Functions - Edge Cases", () => {
635+
it("fetchPackageConfigs should return empty array when no active packages", async () => {
636+
const db = await mf.getD1Database("DB");
637+
await db
638+
.prepare(
639+
`INSERT INTO setups (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, nbtc_fallback_addr, is_active)
640+
VALUES (2, 'testnet', 'testnet', '0xPkg2', '0xContract2', '0xLC2', '0xLCC2', '0xFallback2', 0)`,
641+
)
642+
.run();
643+
644+
const configs = await fetchPackageConfigs(db);
645+
expect(configs.length).toBe(1);
646+
});
647+
648+
it("fetchNbtcAddresses should return empty map when no active setups", async () => {
649+
const db = await mf.getD1Database("DB");
650+
await db
651+
.prepare(
652+
`INSERT INTO setups (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, nbtc_fallback_addr, is_active)
653+
VALUES (2, 'testnet', 'testnet', '0xPkg2', '0xContract2', '0xLC2', '0xLCC2', '0xFallback2', 0)`,
654+
)
655+
.run();
656+
await db
657+
.prepare(
658+
`INSERT INTO nbtc_deposit_addresses (setup_id, deposit_address, is_active)
659+
VALUES (2, 'bcrt1qAddress2', 1)`,
660+
)
661+
.run();
662+
663+
const addrMap = await fetchNbtcAddresses(db);
664+
expect(addrMap.size).toBe(1);
665+
});
666+
667+
it("fetchPackageConfigs should handle multiple active packages", async () => {
668+
const db = await mf.getD1Database("DB");
669+
await db
670+
.prepare(
671+
`INSERT INTO setups (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, nbtc_fallback_addr, is_active)
672+
VALUES (2, 'mainnet', 'mainnet', '0xPkg2', '0xContract2', '0xLC2', '0xLCC2', '0xFallback2', 1)`,
673+
)
674+
.run();
675+
676+
const configs = await fetchPackageConfigs(db);
677+
expect(configs.length).toBe(2);
678+
});
374679
});
375680
});

0 commit comments

Comments
 (0)