@@ -853,6 +853,241 @@ mod tests {
853853 cleanup ( & db_path) ;
854854 }
855855
856+ fn json_has_wallet_id < T : serde:: Serialize > ( value : & T ) -> bool {
857+ serde_json:: to_value ( value)
858+ . unwrap ( )
859+ . get ( "wallet_id" )
860+ . is_some ( )
861+ }
862+
863+ #[ test]
864+ fn test_default_wallet_activity_always_serializes_wallet_id ( ) {
865+ // The current/canonical payload always includes `wallet_id`, even for the default
866+ // wallet. A freshly serialized default-wallet record is therefore distinguishable
867+ // from legacy data, and round-trips back to the default wallet.
868+ let onchain = create_test_onchain_activity ( ) ;
869+ assert_eq ! ( onchain. wallet_id, DEFAULT_WALLET_ID ) ;
870+ assert ! (
871+ json_has_wallet_id( & onchain) ,
872+ "default-wallet onchain activity must serialize wallet_id (current payload)"
873+ ) ;
874+ let onchain_json = serde_json:: to_string ( & onchain) . unwrap ( ) ;
875+ let decoded: OnchainActivity = serde_json:: from_str ( & onchain_json) . unwrap ( ) ;
876+ assert_eq ! ( decoded. wallet_id, DEFAULT_WALLET_ID ) ;
877+
878+ let lightning = create_test_lightning_activity ( ) ;
879+ assert_eq ! ( lightning. wallet_id, DEFAULT_WALLET_ID ) ;
880+ assert ! (
881+ json_has_wallet_id( & lightning) ,
882+ "default-wallet lightning activity must serialize wallet_id (current payload)"
883+ ) ;
884+ let lightning_json = serde_json:: to_string ( & lightning) . unwrap ( ) ;
885+ let decoded: LightningActivity = serde_json:: from_str ( & lightning_json) . unwrap ( ) ;
886+ assert_eq ! ( decoded. wallet_id, DEFAULT_WALLET_ID ) ;
887+ }
888+
889+ #[ test]
890+ fn test_old_v1_activity_json_without_wallet_id_decodes ( ) {
891+ // Old JSON authored before wallet_id existed (no wallet_id key) must still decode,
892+ // defaulting to the built-in Bitkit wallet.
893+ let onchain_v1 = r#"{
894+ "id": "legacy_onchain",
895+ "tx_type": "Sent",
896+ "tx_id": "legacy_txid",
897+ "value": 5000,
898+ "fee": 50,
899+ "fee_rate": 1,
900+ "address": "bc1qlegacy",
901+ "confirmed": true,
902+ "timestamp": 1234567890,
903+ "is_boosted": false,
904+ "boost_tx_ids": [],
905+ "is_transfer": false,
906+ "does_exist": true,
907+ "confirm_timestamp": null,
908+ "channel_id": null,
909+ "transfer_tx_id": null
910+ }"# ;
911+ let decoded: OnchainActivity = serde_json:: from_str ( onchain_v1) . unwrap ( ) ;
912+ assert_eq ! ( decoded. wallet_id, DEFAULT_WALLET_ID ) ;
913+ assert_eq ! ( decoded. tx_id, "legacy_txid" ) ;
914+
915+ let lightning_v1 = r#"{
916+ "id": "legacy_lightning",
917+ "tx_type": "Received",
918+ "status": "Succeeded",
919+ "value": 7000,
920+ "fee": 3,
921+ "invoice": "lightning:legacy",
922+ "message": "legacy message",
923+ "timestamp": 1234567990,
924+ "preimage": null
925+ }"# ;
926+ let decoded: LightningActivity = serde_json:: from_str ( lightning_v1) . unwrap ( ) ;
927+ assert_eq ! ( decoded. wallet_id, DEFAULT_WALLET_ID ) ;
928+ assert_eq ! ( decoded. invoice, "lightning:legacy" ) ;
929+ }
930+
931+ #[ test]
932+ fn test_hardware_wallet_activity_serializes_wallet_id ( ) {
933+ // Wallet-scoped (non-default) records keep wallet_id in the JSON and round-trip it
934+ // back unchanged.
935+ let wallet_id = crate :: activity:: derive_wallet_id (
936+ "trezor" . to_string ( ) ,
937+ vec ! [ "xpubA" . to_string( ) , "xpubB" . to_string( ) ] ,
938+ )
939+ . unwrap ( ) ;
940+
941+ let mut onchain = create_test_onchain_activity ( ) ;
942+ onchain. wallet_id = wallet_id. clone ( ) ;
943+ assert ! ( json_has_wallet_id( & onchain) ) ;
944+ let decoded: OnchainActivity =
945+ serde_json:: from_str ( & serde_json:: to_string ( & onchain) . unwrap ( ) ) . unwrap ( ) ;
946+ assert_eq ! ( decoded. wallet_id, wallet_id) ;
947+
948+ let mut lightning = create_test_lightning_activity ( ) ;
949+ lightning. wallet_id = wallet_id. clone ( ) ;
950+ assert ! ( json_has_wallet_id( & lightning) ) ;
951+ let decoded: LightningActivity =
952+ serde_json:: from_str ( & serde_json:: to_string ( & lightning) . unwrap ( ) ) . unwrap ( ) ;
953+ assert_eq ! ( decoded. wallet_id, wallet_id) ;
954+ }
955+
956+ #[ test]
957+ fn test_wallet_scoped_metadata_models_always_serialize_wallet_id ( ) {
958+ // Tags, pre-activity metadata and transaction details gained wallet_id in the same
959+ // change; the current/canonical payload always carries wallet_id, for the default
960+ // wallet and for any other scope alike.
961+ let scoped = "hardware-wallet-1" ;
962+
963+ let default_tags = ActivityTags {
964+ wallet_id : DEFAULT_WALLET_ID . to_string ( ) ,
965+ activity_id : "act1" . to_string ( ) ,
966+ tags : vec ! [ "tag" . to_string( ) ] ,
967+ } ;
968+ assert ! ( json_has_wallet_id( & default_tags) ) ;
969+ let scoped_tags = ActivityTags {
970+ wallet_id : scoped. to_string ( ) ,
971+ ..default_tags. clone ( )
972+ } ;
973+ assert ! ( json_has_wallet_id( & scoped_tags) ) ;
974+
975+ let default_meta = create_test_pre_activity_metadata (
976+ "pay1" . to_string ( ) ,
977+ ActivityType :: Onchain ,
978+ vec ! [ "tag" . to_string( ) ] ,
979+ ) ;
980+ assert ! ( json_has_wallet_id( & default_meta) ) ;
981+ let scoped_meta = PreActivityMetadata {
982+ wallet_id : scoped. to_string ( ) ,
983+ ..default_meta. clone ( )
984+ } ;
985+ assert ! ( json_has_wallet_id( & scoped_meta) ) ;
986+
987+ let default_details = TransactionDetails {
988+ wallet_id : DEFAULT_WALLET_ID . to_string ( ) ,
989+ tx_id : "txid" . to_string ( ) ,
990+ amount_sats : 1000 ,
991+ inputs : vec ! [ ] ,
992+ outputs : vec ! [ ] ,
993+ } ;
994+ assert ! ( json_has_wallet_id( & default_details) ) ;
995+ let scoped_details = TransactionDetails {
996+ wallet_id : scoped. to_string ( ) ,
997+ ..default_details. clone ( )
998+ } ;
999+ assert ! ( json_has_wallet_id( & scoped_details) ) ;
1000+ }
1001+
1002+ #[ test]
1003+ fn test_mixed_v1_v2_lookup_and_search_is_wallet_scoped ( ) {
1004+ // A v1 (default-wallet) and v2 (hardware-wallet) activity sharing the same raw id
1005+ // must remain distinct: wallet-scoped lookup, list and search each return only the
1006+ // matching record, never a mixed or duplicated v1/v2 pair.
1007+ let ( mut db, db_path) = setup ( ) ;
1008+ let wallet_id = "hardware-wallet-1" ;
1009+ let shared_id = "shared_raw_id" ;
1010+
1011+ let mut v1 = create_test_onchain_activity ( ) ;
1012+ v1. id = shared_id. to_string ( ) ;
1013+ v1. tx_id = "v1_txid" . to_string ( ) ;
1014+ v1. address = "bc1qv1default" . to_string ( ) ;
1015+ v1. value = 10_000 ;
1016+
1017+ let mut v2 = create_test_onchain_activity ( ) ;
1018+ v2. wallet_id = wallet_id. to_string ( ) ;
1019+ v2. id = shared_id. to_string ( ) ;
1020+ v2. tx_id = "v2_txid" . to_string ( ) ;
1021+ v2. address = "bc1qv2hardware" . to_string ( ) ;
1022+ v2. value = 20_000 ;
1023+
1024+ db. insert_onchain_activity ( & v1) . unwrap ( ) ;
1025+ db. insert_onchain_activity ( & v2) . unwrap ( ) ;
1026+
1027+ let default_activity = db
1028+ . get_activity_by_id ( DEFAULT_WALLET_ID , shared_id)
1029+ . unwrap ( )
1030+ . unwrap ( ) ;
1031+ assert_eq ! ( default_activity. get_wallet_id( ) , DEFAULT_WALLET_ID ) ;
1032+
1033+ let scoped_activity = db
1034+ . get_activity_by_id ( wallet_id, shared_id)
1035+ . unwrap ( )
1036+ . unwrap ( ) ;
1037+ assert_eq ! ( scoped_activity. get_wallet_id( ) , wallet_id) ;
1038+
1039+ // Wallet-scoped list returns exactly one record for each wallet, not the mixed pair.
1040+ let default_list = db
1041+ . get_activities (
1042+ Some ( DEFAULT_WALLET_ID ) ,
1043+ Some ( ActivityFilter :: Onchain ) ,
1044+ None ,
1045+ None ,
1046+ None ,
1047+ None ,
1048+ None ,
1049+ None ,
1050+ None ,
1051+ )
1052+ . unwrap ( ) ;
1053+ assert_eq ! ( default_list. len( ) , 1 ) ;
1054+ assert_eq ! ( default_list[ 0 ] . get_wallet_id( ) , DEFAULT_WALLET_ID ) ;
1055+
1056+ let scoped_list = db
1057+ . get_activities (
1058+ Some ( wallet_id) ,
1059+ Some ( ActivityFilter :: Onchain ) ,
1060+ None ,
1061+ None ,
1062+ None ,
1063+ None ,
1064+ None ,
1065+ None ,
1066+ None ,
1067+ )
1068+ . unwrap ( ) ;
1069+ assert_eq ! ( scoped_list. len( ) , 1 ) ;
1070+ assert_eq ! ( scoped_list[ 0 ] . get_wallet_id( ) , wallet_id) ;
1071+
1072+ // Search stays wallet-scoped: the hardware address is invisible to the default wallet.
1073+ let scoped_search = db
1074+ . get_activities (
1075+ Some ( DEFAULT_WALLET_ID ) ,
1076+ None ,
1077+ None ,
1078+ None ,
1079+ Some ( "bc1qv2hardware" . to_string ( ) ) ,
1080+ None ,
1081+ None ,
1082+ None ,
1083+ None ,
1084+ )
1085+ . unwrap ( ) ;
1086+ assert ! ( scoped_search. is_empty( ) ) ;
1087+
1088+ cleanup ( & db_path) ;
1089+ }
1090+
8561091 #[ test]
8571092 fn test_delete_activities_by_wallet_id_cleans_scoped_data ( ) {
8581093 let ( mut db, db_path) = setup ( ) ;
0 commit comments