@@ -7,7 +7,7 @@ import { dropTables, initDb } from "@gonative-cc/lib/test-helpers/init_db";
77
88import { fetchNbtcAddresses , fetchPackageConfigs } from "./storage" ;
99import { CFStorage as CFStorageImpl } from "./cf-storage" ;
10- import { MintTxStatus , InsertBlockStatus } from "./models" ;
10+ import { MintTxStatus , InsertBlockStatus , type NbtcBroadcastedDeposit } from "./models" ;
1111
1212let 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 ( [ ] ) ) . toBeUndefined ( ) ;
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 only return 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