1
+ /* eslint-disable jest/no-conditional-in-test */
1
2
import { Contract } from '@ethersproject/contracts' ;
2
- import { SolScope } from '@metamask/keyring-api' ;
3
- import type { Hex } from '@metamask/utils' ;
4
- import { bigIntToHex } from '@metamask/utils' ;
3
+ import {
4
+ EthAccountType ,
5
+ EthScope ,
6
+ SolAccountType ,
7
+ SolScope ,
8
+ } from '@metamask/keyring-api' ;
5
9
import nock from 'nock' ;
6
10
7
11
import { BridgeController } from './bridge-controller' ;
@@ -201,6 +205,10 @@ describe('BridgeController', function () {
201
205
} ;
202
206
203
207
it ( 'updateBridgeQuoteRequestParams should update the quoteRequest state' , async function ( ) {
208
+ messengerMock . call . mockReturnValue ( {
209
+ currentCurrency : 'usd' ,
210
+ } as never ) ;
211
+
204
212
await bridgeController . updateBridgeQuoteRequestParams (
205
213
{ srcChainId : 1 } ,
206
214
metricsContext ,
@@ -659,23 +667,31 @@ describe('BridgeController', function () {
659
667
) : ReturnType < BridgeControllerMessenger [ 'call' ] > => {
660
668
const actionType = args [ 0 ] ;
661
669
662
- // eslint-disable-next-line jest/no-conditional-in-test
663
670
if ( actionType === 'AccountsController:getSelectedMultichainAccount' ) {
664
671
return {
672
+ type : SolAccountType . DataAccount ,
673
+ id : 'account1' ,
674
+ scopes : [ SolScope . Mainnet ] ,
675
+ methods : [ ] ,
665
676
address : '0x123' ,
666
677
metadata : {
667
678
snap : {
668
679
id : 'npm:@metamask/solana-snap' ,
669
680
name : 'Solana Snap' ,
670
681
enabled : true ,
671
682
} ,
672
- } as never ,
683
+ name : 'Account 1' ,
684
+ importTime : 1717334400 ,
685
+ keyring : {
686
+ type : 'Keyring' ,
687
+ } ,
688
+ } ,
673
689
options : {
674
690
scope : 'mainnet' ,
675
691
} ,
676
- } as never ;
692
+ } ;
677
693
}
678
- // eslint-disable-next-line jest/no-conditional-in-test
694
+
679
695
if ( actionType === 'NetworkController:getNetworkClientById' ) {
680
696
return {
681
697
configuration : { rpcUrl : 'https://rpc.tenderly.co' } ,
@@ -820,11 +836,9 @@ describe('BridgeController', function () {
820
836
// Setup
821
837
const mockMessenger = {
822
838
call : jest . fn ( ) . mockImplementation ( ( methodName ) => {
823
- // eslint-disable-next-line jest/no-conditional-in-test
824
839
if ( methodName === 'NetworkController:getNetworkClientById' ) {
825
840
return { provider : null } ;
826
841
}
827
- // eslint-disable-next-line jest/no-conditional-in-test
828
842
if ( methodName === 'NetworkController:getState' ) {
829
843
return { selectedNetworkClientId : 'testNetworkClientId' } ;
830
844
}
@@ -854,29 +868,47 @@ describe('BridgeController', function () {
854
868
[
855
869
'should append l1GasFees if srcChain is 10 and srcToken is erc20' ,
856
870
mockBridgeQuotesErc20Native as QuoteResponse [ ] ,
857
- bigIntToHex ( BigInt ( '2608710388388' ) * 2n ) ,
858
- 12 ,
871
+ [ '0x2' , '0x1' ] ,
872
+ [ 6 , 12 ] ,
859
873
] ,
860
874
[
861
875
'should append l1GasFees if srcChain is 10 and srcToken is native' ,
862
876
mockBridgeQuotesNativeErc20 as unknown as QuoteResponse [ ] ,
863
- bigIntToHex ( BigInt ( '2608710388388' ) ) ,
864
- 2 ,
877
+ [ '0x1' , '0x1' ] ,
878
+ [ 2 , 2 ] ,
865
879
] ,
866
880
[
867
881
'should not append l1GasFees if srcChain is not 10' ,
868
882
mockBridgeQuotesNativeErc20Eth as unknown as QuoteResponse [ ] ,
869
- undefined ,
870
- 0 ,
883
+ [ ] ,
884
+ [ 2 , 0 ] ,
885
+ ] ,
886
+ [
887
+ 'should filter out quote if getL1Fees returns undefined' ,
888
+ mockBridgeQuotesErc20Native as unknown as QuoteResponse [ ] ,
889
+ [ '0x2' , undefined ] ,
890
+ [ 5 , 12 ] ,
891
+ ] ,
892
+ [
893
+ 'should filter out quote if L1 fee calculation fails' ,
894
+ mockBridgeQuotesErc20Native as unknown as QuoteResponse [ ] ,
895
+ [ '0x2' , '0x1' , 'L1 gas fee calculation failed' ] ,
896
+ [ 5 , 11 ] ,
871
897
] ,
872
898
] ) (
873
899
'updateBridgeQuoteRequestParams: %s' ,
874
900
async (
875
901
_testTitle : string ,
876
902
quoteResponse : QuoteResponse [ ] ,
877
- l1GasFeesInHexWei : Hex | undefined ,
878
- getLayer1GasFeeMockCallCount : number ,
903
+ [ totalL1GasFeesInHexWei , tradeL1GasFeesInHexWei , tradeL1GasFeeError ] : (
904
+ | string
905
+ | undefined
906
+ ) [ ] ,
907
+ [ expectedQuotesLength , expectedGetLayer1GasFeeMockCallCount ] : number [ ] ,
879
908
) => {
909
+ const errorSpy = jest
910
+ . spyOn ( console , 'error' )
911
+ . mockImplementation ( jest . fn ( ) ) ;
880
912
jest . useFakeTimers ( ) ;
881
913
const stopAllPollingSpy = jest . spyOn ( bridgeController , 'stopAllPolling' ) ;
882
914
const startPollingSpy = jest . spyOn ( bridgeController , 'startPolling' ) ;
@@ -888,7 +920,27 @@ describe('BridgeController', function () {
888
920
provider : jest . fn ( ) ,
889
921
selectedNetworkClientId : 'selectedNetworkClientId' ,
890
922
} as never ) ;
891
- getLayer1GasFeeMock . mockResolvedValue ( '0x25F63418AA4' ) ;
923
+
924
+ for ( const [ index , quote ] of quoteResponse . entries ( ) ) {
925
+ if ( tradeL1GasFeeError && index === 0 ) {
926
+ getLayer1GasFeeMock . mockRejectedValueOnce (
927
+ new Error ( tradeL1GasFeeError ) ,
928
+ ) ;
929
+ continue ;
930
+ }
931
+
932
+ if ( quote . approval ) {
933
+ getLayer1GasFeeMock . mockResolvedValueOnce ( '0x1' ) ;
934
+ }
935
+
936
+ if ( tradeL1GasFeesInHexWei === undefined && index === 0 ) {
937
+ getLayer1GasFeeMock . mockResolvedValueOnce ( undefined ) ;
938
+ continue ;
939
+ }
940
+ getLayer1GasFeeMock . mockResolvedValueOnce (
941
+ tradeL1GasFeesInHexWei ?? '0x1' ,
942
+ ) ;
943
+ }
892
944
893
945
const fetchBridgeQuotesSpy = jest
894
946
. spyOn ( fetchUtils , 'fetchBridgeQuotes' )
@@ -967,6 +1019,7 @@ describe('BridgeController', function () {
967
1019
jest . advanceTimersByTime ( 1500 ) ;
968
1020
await flushPromises ( ) ;
969
1021
const { quotes } = bridgeController . state ;
1022
+ expect ( quotes ) . toHaveLength ( expectedQuotesLength ) ;
970
1023
expect ( bridgeController . state ) . toStrictEqual (
971
1024
expect . objectContaining ( {
972
1025
quoteRequest : { ...quoteRequest , insufficientBal : true } ,
@@ -975,7 +1028,10 @@ describe('BridgeController', function () {
975
1028
} ) ,
976
1029
) ;
977
1030
quotes . forEach ( ( quote ) => {
978
- const expectedQuote = { ...quote , l1GasFeesInHexWei } ;
1031
+ const expectedQuote = {
1032
+ ...quote ,
1033
+ l1GasFeesInHexWei : totalL1GasFeesInHexWei ,
1034
+ } ;
979
1035
// eslint-disable-next-line jest/prefer-strict-equal
980
1036
expect ( quote ) . toEqual ( expectedQuote ) ;
981
1037
} ) ;
@@ -984,8 +1040,10 @@ describe('BridgeController', function () {
984
1040
expect ( firstFetchTime ) . toBeGreaterThan ( 0 ) ;
985
1041
986
1042
expect ( getLayer1GasFeeMock ) . toHaveBeenCalledTimes (
987
- getLayer1GasFeeMockCallCount ,
1043
+ expectedGetLayer1GasFeeMockCallCount ,
988
1044
) ;
1045
+
1046
+ expect ( errorSpy ) . toHaveBeenCalledTimes ( tradeL1GasFeeError ? 1 : 0 ) ;
989
1047
} ,
990
1048
) ;
991
1049
@@ -1201,12 +1259,14 @@ describe('BridgeController', function () {
1201
1259
[
1202
1260
'should append solanaFees for Solana quotes' ,
1203
1261
mockBridgeQuotesSolErc20 as unknown as QuoteResponse [ ] ,
1262
+ 2 ,
1204
1263
'5000' ,
1205
1264
solanaSnapCalls ,
1206
1265
] ,
1207
1266
[
1208
1267
'should not append solanaFees if selected account is not a snap' ,
1209
1268
mockBridgeQuotesSolErc20 as unknown as QuoteResponse [ ] ,
1269
+ 2 ,
1210
1270
undefined ,
1211
1271
[ ] ,
1212
1272
false ,
@@ -1217,6 +1277,7 @@ describe('BridgeController', function () {
1217
1277
...mockBridgeQuotesSolErc20 ,
1218
1278
...mockBridgeQuotesErc20Native ,
1219
1279
] as unknown as QuoteResponse [ ] ,
1280
+ 8 ,
1220
1281
undefined ,
1221
1282
mixedQuotesSnapCalls ,
1222
1283
] ,
@@ -1225,6 +1286,7 @@ describe('BridgeController', function () {
1225
1286
async (
1226
1287
_testTitle : string ,
1227
1288
quoteResponse : QuoteResponse [ ] ,
1289
+ expectedQuotesLength : number ,
1228
1290
expectedFees : string | undefined ,
1229
1291
expectedSnapCalls : typeof solanaSnapCalls ,
1230
1292
isSnapAccount = true ,
@@ -1242,27 +1304,54 @@ describe('BridgeController', function () {
1242
1304
) : ReturnType < BridgeControllerMessenger [ 'call' ] > => {
1243
1305
const actionType = args [ 0 ] ;
1244
1306
1245
- // eslint-disable-next-line jest/no-conditional-in-test
1246
1307
if (
1247
- // eslint-disable-next-line jest/no-conditional-in-test
1248
1308
actionType === 'AccountsController:getSelectedMultichainAccount' &&
1249
1309
isSnapAccount
1250
1310
) {
1251
1311
return {
1312
+ type : SolAccountType . DataAccount ,
1313
+ id : 'account1' ,
1314
+ scopes : [ SolScope . Mainnet ] ,
1315
+ methods : [ ] ,
1252
1316
address : '0x123' ,
1253
1317
metadata : {
1318
+ name : 'Account 1' ,
1319
+ importTime : 1717334400 ,
1320
+ keyring : {
1321
+ type : 'Keyring' ,
1322
+ } ,
1254
1323
snap : {
1255
1324
id : 'npm:@metamask/solana-snap' ,
1256
1325
name : 'Solana Snap' ,
1257
1326
enabled : true ,
1258
1327
} ,
1259
- } as never ,
1328
+ } ,
1260
1329
options : {
1261
1330
scope : 'mainnet' ,
1262
1331
} ,
1263
- } as never ;
1332
+ } ;
1333
+ }
1334
+ if (
1335
+ actionType === 'AccountsController:getSelectedMultichainAccount'
1336
+ ) {
1337
+ return {
1338
+ type : EthAccountType . Eoa ,
1339
+ id : 'account1' ,
1340
+ scopes : [ EthScope . Eoa ] ,
1341
+ methods : [ ] ,
1342
+ address : '0x123' ,
1343
+ metadata : {
1344
+ name : 'Account 1' ,
1345
+ importTime : 1717334400 ,
1346
+ keyring : {
1347
+ type : 'Keyring' ,
1348
+ } ,
1349
+ } ,
1350
+ options : {
1351
+ scope : 'mainnet' ,
1352
+ } ,
1353
+ } ;
1264
1354
}
1265
- // eslint-disable-next-line jest/no-conditional-in-test
1266
1355
if ( actionType === 'SnapController:handleRequest' ) {
1267
1356
return { value : '5000' } as never ;
1268
1357
}
@@ -1330,6 +1419,8 @@ describe('BridgeController', function () {
1330
1419
) ;
1331
1420
1332
1421
expect ( snapCalls ) . toMatchObject ( expectedSnapCalls ) ;
1422
+
1423
+ expect ( quotes ) . toHaveLength ( expectedQuotesLength ) ;
1333
1424
} ,
1334
1425
) ;
1335
1426
@@ -1605,4 +1696,73 @@ describe('BridgeController', function () {
1605
1696
expect ( trackMetaMetricsFn . mock . calls ) . toMatchSnapshot ( ) ;
1606
1697
} ) ;
1607
1698
} ) ;
1699
+
1700
+ describe ( 'trackUnifiedSwapBridgeEvent client-side call exceptions' , ( ) => {
1701
+ beforeEach ( ( ) => {
1702
+ jest . clearAllMocks ( ) ;
1703
+ messengerMock . call . mockImplementation (
1704
+ (
1705
+ ...args : Parameters < BridgeControllerMessenger [ 'call' ] >
1706
+ ) : ReturnType < BridgeControllerMessenger [ 'call' ] > => {
1707
+ const actionType = args [ 0 ] ;
1708
+ if (
1709
+ actionType === 'AccountsController:getSelectedMultichainAccount'
1710
+ ) {
1711
+ return {
1712
+ type : SolAccountType . DataAccount ,
1713
+ id : 'account1' ,
1714
+ scopes : [ SolScope . Mainnet ] ,
1715
+ methods : [ ] ,
1716
+ address : '0x123' ,
1717
+ metadata : {
1718
+ snap : {
1719
+ id : 'npm:@metamask/solana-snap' ,
1720
+ name : 'Solana Snap' ,
1721
+ enabled : true ,
1722
+ } ,
1723
+ name : 'Account 1' ,
1724
+ importTime : 1717334400 ,
1725
+ } as never ,
1726
+ options : {
1727
+ scope : 'mainnet' ,
1728
+ } ,
1729
+ } ;
1730
+ }
1731
+ return {
1732
+ provider : jest . fn ( ) as never ,
1733
+ selectedNetworkClientId : 'selectedNetworkClientId' ,
1734
+ rpcUrl : 'https://mainnet.infura.io/v3/123' ,
1735
+ configuration : {
1736
+ chainId : 'eip155:1' ,
1737
+ } ,
1738
+ } as never ;
1739
+ } ,
1740
+ ) ;
1741
+ } ) ;
1742
+
1743
+ it ( 'should not track the event if the account keyring type is not set' , ( ) => {
1744
+ const errorSpy = jest
1745
+ . spyOn ( console , 'error' )
1746
+ . mockImplementation ( jest . fn ( ) ) ;
1747
+ bridgeController . trackUnifiedSwapBridgeEvent (
1748
+ UnifiedSwapBridgeEventName . QuotesReceived ,
1749
+ {
1750
+ warnings : [ 'warning1' ] ,
1751
+ usd_quoted_gas : 0 ,
1752
+ gas_included : false ,
1753
+ quoted_time_minutes : 10 ,
1754
+ usd_quoted_return : 100 ,
1755
+ price_impact : 0 ,
1756
+ provider : 'provider_bridge' ,
1757
+ best_quote_provider : 'provider_bridge2' ,
1758
+ } ,
1759
+ ) ;
1760
+ expect ( trackMetaMetricsFn ) . toHaveBeenCalledTimes ( 0 ) ;
1761
+ expect ( errorSpy ) . toHaveBeenCalledTimes ( 1 ) ;
1762
+ expect ( errorSpy ) . toHaveBeenCalledWith (
1763
+ 'Error tracking cross-chain swaps MetaMetrics event' ,
1764
+ new TypeError ( "Cannot read properties of undefined (reading 'type')" ) ,
1765
+ ) ;
1766
+ } ) ;
1767
+ } ) ;
1608
1768
} ) ;
0 commit comments