|
1 | 1 | import { deriveStateFromMetadata } from '@metamask/base-controller'; |
2 | 2 | import type { |
3 | 3 | AccountAssetListUpdatedEventPayload, |
| 4 | + CaipAssetType, |
4 | 5 | CaipAssetTypeOrId, |
5 | 6 | } from '@metamask/keyring-api'; |
6 | 7 | import { |
@@ -921,6 +922,317 @@ describe('MultichainAssetsController', () => { |
921 | 922 | }); |
922 | 923 | }); |
923 | 924 |
|
| 925 | + describe('addAssets', () => { |
| 926 | + it('should add a single asset to account assets list', async () => { |
| 927 | + const { controller } = setupController({ |
| 928 | + state: { |
| 929 | + accountsAssets: { |
| 930 | + [mockSolanaAccount.id]: [ |
| 931 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501', |
| 932 | + ], |
| 933 | + }, |
| 934 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 935 | + allIgnoredAssets: {}, |
| 936 | + } as MultichainAssetsControllerState, |
| 937 | + }); |
| 938 | + |
| 939 | + const assetToAdd = |
| 940 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr'; |
| 941 | + |
| 942 | + const result = await controller.addAssets( |
| 943 | + [assetToAdd], |
| 944 | + mockSolanaAccount.id, |
| 945 | + ); |
| 946 | + |
| 947 | + expect(result).toStrictEqual([ |
| 948 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501', |
| 949 | + assetToAdd, |
| 950 | + ]); |
| 951 | + expect( |
| 952 | + controller.state.accountsAssets[mockSolanaAccount.id], |
| 953 | + ).toStrictEqual([ |
| 954 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501', |
| 955 | + assetToAdd, |
| 956 | + ]); |
| 957 | + }); |
| 958 | + |
| 959 | + it('should not add duplicate assets', async () => { |
| 960 | + const existingAsset = |
| 961 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
| 962 | + const { controller } = setupController({ |
| 963 | + state: { |
| 964 | + accountsAssets: { |
| 965 | + [mockSolanaAccount.id]: [existingAsset], |
| 966 | + }, |
| 967 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 968 | + allIgnoredAssets: {}, |
| 969 | + } as MultichainAssetsControllerState, |
| 970 | + }); |
| 971 | + |
| 972 | + const result = await controller.addAssets( |
| 973 | + [existingAsset], |
| 974 | + mockSolanaAccount.id, |
| 975 | + ); |
| 976 | + |
| 977 | + expect(result).toStrictEqual([existingAsset]); |
| 978 | + expect( |
| 979 | + controller.state.accountsAssets[mockSolanaAccount.id], |
| 980 | + ).toStrictEqual([existingAsset]); |
| 981 | + }); |
| 982 | + |
| 983 | + it('should remove asset from ignored list when added', async () => { |
| 984 | + const assetToAdd = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
| 985 | + const { controller } = setupController({ |
| 986 | + state: { |
| 987 | + accountsAssets: {}, |
| 988 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 989 | + allIgnoredAssets: { |
| 990 | + [mockSolanaAccount.id]: [assetToAdd], |
| 991 | + }, |
| 992 | + } as MultichainAssetsControllerState, |
| 993 | + }); |
| 994 | + |
| 995 | + const result = await controller.addAssets( |
| 996 | + [assetToAdd], |
| 997 | + mockSolanaAccount.id, |
| 998 | + ); |
| 999 | + |
| 1000 | + expect(result).toStrictEqual([assetToAdd]); |
| 1001 | + expect( |
| 1002 | + controller.state.accountsAssets[mockSolanaAccount.id], |
| 1003 | + ).toStrictEqual([assetToAdd]); |
| 1004 | + expect( |
| 1005 | + controller.state.allIgnoredAssets[mockSolanaAccount.id], |
| 1006 | + ).toBeUndefined(); |
| 1007 | + }); |
| 1008 | + |
| 1009 | + it('should handle adding asset to account with no existing assets', async () => { |
| 1010 | + const assetToAdd = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
| 1011 | + const { controller } = setupController({ |
| 1012 | + state: { |
| 1013 | + accountsAssets: {}, |
| 1014 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 1015 | + allIgnoredAssets: {}, |
| 1016 | + } as MultichainAssetsControllerState, |
| 1017 | + }); |
| 1018 | + |
| 1019 | + const result = await controller.addAssets( |
| 1020 | + [assetToAdd], |
| 1021 | + mockSolanaAccount.id, |
| 1022 | + ); |
| 1023 | + |
| 1024 | + expect(result).toStrictEqual([assetToAdd]); |
| 1025 | + expect( |
| 1026 | + controller.state.accountsAssets[mockSolanaAccount.id], |
| 1027 | + ).toStrictEqual([assetToAdd]); |
| 1028 | + }); |
| 1029 | + |
| 1030 | + it('should publish accountAssetListUpdated event when asset is added', async () => { |
| 1031 | + const { controller, messenger } = setupController({ |
| 1032 | + state: { |
| 1033 | + accountsAssets: {}, |
| 1034 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 1035 | + allIgnoredAssets: {}, |
| 1036 | + } as MultichainAssetsControllerState, |
| 1037 | + }); |
| 1038 | + |
| 1039 | + const assetToAdd = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
| 1040 | + |
| 1041 | + // Set up event listener to capture the published event |
| 1042 | + const eventListener = jest.fn(); |
| 1043 | + messenger.subscribe( |
| 1044 | + 'MultichainAssetsController:accountAssetListUpdated', |
| 1045 | + eventListener, |
| 1046 | + ); |
| 1047 | + |
| 1048 | + await controller.addAssets([assetToAdd], mockSolanaAccount.id); |
| 1049 | + |
| 1050 | + expect(eventListener).toHaveBeenCalledWith({ |
| 1051 | + assets: { |
| 1052 | + [mockSolanaAccount.id]: { |
| 1053 | + added: [assetToAdd], |
| 1054 | + removed: [], |
| 1055 | + }, |
| 1056 | + }, |
| 1057 | + }); |
| 1058 | + }); |
| 1059 | + |
| 1060 | + it('should add multiple assets from the same chain', async () => { |
| 1061 | + const { controller } = setupController({ |
| 1062 | + state: { |
| 1063 | + accountsAssets: { |
| 1064 | + [mockSolanaAccount.id]: [ |
| 1065 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501', |
| 1066 | + ], |
| 1067 | + }, |
| 1068 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 1069 | + allIgnoredAssets: {}, |
| 1070 | + } as MultichainAssetsControllerState, |
| 1071 | + }); |
| 1072 | + |
| 1073 | + const assetsToAdd: CaipAssetType[] = [ |
| 1074 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', |
| 1075 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:AnotherTokenAddress', |
| 1076 | + ]; |
| 1077 | + |
| 1078 | + const result = await controller.addAssets( |
| 1079 | + assetsToAdd, |
| 1080 | + mockSolanaAccount.id, |
| 1081 | + ); |
| 1082 | + |
| 1083 | + expect(result).toStrictEqual([ |
| 1084 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501', |
| 1085 | + ...assetsToAdd, |
| 1086 | + ]); |
| 1087 | + expect( |
| 1088 | + controller.state.accountsAssets[mockSolanaAccount.id], |
| 1089 | + ).toStrictEqual([ |
| 1090 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501', |
| 1091 | + ...assetsToAdd, |
| 1092 | + ]); |
| 1093 | + }); |
| 1094 | + |
| 1095 | + it('should throw error when assets are from different chains', async () => { |
| 1096 | + const { controller } = setupController({ |
| 1097 | + state: { |
| 1098 | + accountsAssets: {}, |
| 1099 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 1100 | + allIgnoredAssets: {}, |
| 1101 | + } as MultichainAssetsControllerState, |
| 1102 | + }); |
| 1103 | + |
| 1104 | + const assetsFromDifferentChains: CaipAssetType[] = [ |
| 1105 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501', |
| 1106 | + 'eip155:1/slip44:60', // Ethereum asset |
| 1107 | + ]; |
| 1108 | + |
| 1109 | + await expect( |
| 1110 | + controller.addAssets(assetsFromDifferentChains, mockSolanaAccount.id), |
| 1111 | + ).rejects.toThrow( |
| 1112 | + 'All assets must belong to the same chain. Found assets from chains: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1, eip155:1', |
| 1113 | + ); |
| 1114 | + }); |
| 1115 | + |
| 1116 | + it('should return existing assets when empty array is provided', async () => { |
| 1117 | + const existingAsset = |
| 1118 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
| 1119 | + const { controller } = setupController({ |
| 1120 | + state: { |
| 1121 | + accountsAssets: { |
| 1122 | + [mockSolanaAccount.id]: [existingAsset], |
| 1123 | + }, |
| 1124 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 1125 | + allIgnoredAssets: {}, |
| 1126 | + } as MultichainAssetsControllerState, |
| 1127 | + }); |
| 1128 | + |
| 1129 | + const result = await controller.addAssets([], mockSolanaAccount.id); |
| 1130 | + |
| 1131 | + expect(result).toStrictEqual([existingAsset]); |
| 1132 | + }); |
| 1133 | + |
| 1134 | + it('should only publish event for newly added assets', async () => { |
| 1135 | + const existingAsset = |
| 1136 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
| 1137 | + const newAsset = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:NewToken'; |
| 1138 | + |
| 1139 | + const { controller, messenger } = setupController({ |
| 1140 | + state: { |
| 1141 | + accountsAssets: { |
| 1142 | + [mockSolanaAccount.id]: [existingAsset], |
| 1143 | + }, |
| 1144 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 1145 | + allIgnoredAssets: {}, |
| 1146 | + } as MultichainAssetsControllerState, |
| 1147 | + }); |
| 1148 | + |
| 1149 | + const eventListener = jest.fn(); |
| 1150 | + messenger.subscribe( |
| 1151 | + 'MultichainAssetsController:accountAssetListUpdated', |
| 1152 | + eventListener, |
| 1153 | + ); |
| 1154 | + |
| 1155 | + await controller.addAssets( |
| 1156 | + [existingAsset, newAsset], |
| 1157 | + mockSolanaAccount.id, |
| 1158 | + ); |
| 1159 | + |
| 1160 | + expect(eventListener).toHaveBeenCalledWith({ |
| 1161 | + assets: { |
| 1162 | + [mockSolanaAccount.id]: { |
| 1163 | + added: [newAsset], // Only the new asset should be in the event |
| 1164 | + removed: [], |
| 1165 | + }, |
| 1166 | + }, |
| 1167 | + }); |
| 1168 | + }); |
| 1169 | + |
| 1170 | + it('should not publish event when no new assets are added', async () => { |
| 1171 | + const existingAsset = |
| 1172 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
| 1173 | + |
| 1174 | + const { controller, messenger } = setupController({ |
| 1175 | + state: { |
| 1176 | + accountsAssets: { |
| 1177 | + [mockSolanaAccount.id]: [existingAsset], |
| 1178 | + }, |
| 1179 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 1180 | + allIgnoredAssets: {}, |
| 1181 | + } as MultichainAssetsControllerState, |
| 1182 | + }); |
| 1183 | + |
| 1184 | + const eventListener = jest.fn(); |
| 1185 | + messenger.subscribe( |
| 1186 | + 'MultichainAssetsController:accountAssetListUpdated', |
| 1187 | + eventListener, |
| 1188 | + ); |
| 1189 | + |
| 1190 | + await controller.addAssets([existingAsset], mockSolanaAccount.id); |
| 1191 | + |
| 1192 | + // Event should not be published since no new assets were added |
| 1193 | + expect(eventListener).not.toHaveBeenCalled(); |
| 1194 | + }); |
| 1195 | + |
| 1196 | + it('should partially remove assets from ignored list when only some are added', async () => { |
| 1197 | + const ignoredAsset1 = |
| 1198 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
| 1199 | + const ignoredAsset2 = |
| 1200 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Token1'; |
| 1201 | + const ignoredAsset3 = |
| 1202 | + 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/token:Token2'; |
| 1203 | + |
| 1204 | + const { controller } = setupController({ |
| 1205 | + state: { |
| 1206 | + accountsAssets: {}, |
| 1207 | + assetsMetadata: mockGetMetadataReturnValue.assets, |
| 1208 | + allIgnoredAssets: { |
| 1209 | + [mockSolanaAccount.id]: [ |
| 1210 | + ignoredAsset1, |
| 1211 | + ignoredAsset2, |
| 1212 | + ignoredAsset3, |
| 1213 | + ], |
| 1214 | + }, |
| 1215 | + } as MultichainAssetsControllerState, |
| 1216 | + }); |
| 1217 | + |
| 1218 | + // Only add two of the three ignored assets |
| 1219 | + await controller.addAssets( |
| 1220 | + [ignoredAsset1, ignoredAsset2], |
| 1221 | + mockSolanaAccount.id, |
| 1222 | + ); |
| 1223 | + |
| 1224 | + // Should have added the two assets |
| 1225 | + expect( |
| 1226 | + controller.state.accountsAssets[mockSolanaAccount.id], |
| 1227 | + ).toStrictEqual([ignoredAsset1, ignoredAsset2]); |
| 1228 | + |
| 1229 | + // Should have only the third asset remaining in ignored list |
| 1230 | + expect( |
| 1231 | + controller.state.allIgnoredAssets[mockSolanaAccount.id], |
| 1232 | + ).toStrictEqual([ignoredAsset3]); |
| 1233 | + }); |
| 1234 | + }); |
| 1235 | + |
924 | 1236 | describe('asset detection with ignored assets', () => { |
925 | 1237 | it('should filter out ignored assets when account assets are updated', async () => { |
926 | 1238 | const ignoredAsset = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1/slip44:501'; |
|
0 commit comments