diff --git a/app/components/UI/Swaps/components/TokenSelectModal.js b/app/components/UI/Swaps/components/TokenSelectModal.js index ac620379b1b..7165738699c 100644 --- a/app/components/UI/Swaps/components/TokenSelectModal.js +++ b/app/components/UI/Swaps/components/TokenSelectModal.js @@ -1,4 +1,10 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import PropTypes from 'prop-types'; import { StyleSheet, @@ -155,6 +161,7 @@ function TokenSelectModal({ providerConfig, networkConfigurations, balances, + initialSearchString = '', }) { const navigation = useNavigation(); const { trackEvent } = useMetrics(); @@ -301,6 +308,14 @@ function TokenSelectModal({ showTokenImportModal(); }, [showTokenImportModal]); + useEffect(() => { + !!initialSearchString && setSearchString(initialSearchString); + + if (tokenMetadata.valid) { + handleShowImportToken(); + } + }, [initialSearchString, handleShowImportToken, tokenMetadata.valid]); + const handlePressImportToken = useCallback( (item) => { const { address, symbol } = item; @@ -386,7 +401,6 @@ function TokenSelectModal({ setSearchString(''); searchInput?.current?.focus(); }, [setSearchString]); - return ( ({ diff --git a/app/components/UI/Swaps/index.js b/app/components/UI/Swaps/index.js index eeb33ec4e44..af1862cd0d3 100644 --- a/app/components/UI/Swaps/index.js +++ b/app/components/UI/Swaps/index.js @@ -205,6 +205,7 @@ function SwapsAmountView({ const explorer = useBlockExplorer(providerConfig, networkConfigurations); const initialSource = route.params?.sourceToken ?? SWAPS_NATIVE_ADDRESS; const initialDestination = route.params?.destinationToken; + const initialAmount = route.params?.amount; const [amount, setAmount] = useState('0'); const [slippage, setSlippage] = useState(AppConstants.SWAPS.DEFAULT_SLIPPAGE); @@ -229,6 +230,7 @@ function SwapsAmountView({ toLowerCaseEquals(token.address, initialDestination), ), ); + const [initialDestinationSearch, setInitialDestinationSearch] = useState(''); const [hasDismissedTokenAlert, setHasDismissedTokenAlert] = useState(true); const [contractBalance, setContractBalance] = useState(null); const [contractBalanceAsUnits, setContractBalanceAsUnits] = useState( @@ -361,13 +363,31 @@ function SwapsAmountView({ useEffect(() => { if (canSetAnInitialTokenDestination) { setIsDestinationSet(true); - setDestinationToken( - swapsTokens.find((token) => - toLowerCaseEquals(token.address, initialDestination), - ), + const destinationTokenOnList = swapsTokens.find((token) => + toLowerCaseEquals(token.address, initialDestination), ); + if (destinationTokenOnList) { + setDestinationToken(destinationTokenOnList); + } else { + toggleDestinationModal(); + setInitialDestinationSearch(initialDestination); + } + } + }, [ + canSetAnInitialTokenDestination, + initialDestination, + swapsTokens, + toggleDestinationModal, + ]); + + const canSetInitialAmount = + destinationToken && sourceToken && initialAmount && swapsControllerTokens; + + useEffect(() => { + if (canSetInitialAmount) { + setAmount(parseInt(initialAmount, 16).toString()); } - }, [canSetAnInitialTokenDestination, initialDestination, swapsTokens]); + }, [initialAmount, canSetInitialAmount]); useEffect(() => { setHasDismissedTokenAlert(false); @@ -775,6 +795,7 @@ function SwapsAmountView({ ]} onItemPress={handleDestinationTokenPress} excludeAddresses={[sourceToken?.address]} + initialSearchString={initialDestinationSearch} /> diff --git a/app/components/Views/confirmations/SendFlow/Amount/index.js b/app/components/Views/confirmations/SendFlow/Amount/index.js index f2bd09ddc3a..c0f6b50144d 100644 --- a/app/components/Views/confirmations/SendFlow/Amount/index.js +++ b/app/components/Views/confirmations/SendFlow/Amount/index.js @@ -94,14 +94,14 @@ import { PREFIX_HEX_STRING } from '../../../../../constants/transaction'; import Routes from '../../../../../constants/navigation/Routes'; import { getRampNetworks } from '../../../../../reducers/fiatOrders'; import { swapsLivenessSelector } from '../../../../../reducers/swaps'; -import { isSwapsAllowed } from '../../../../UI/Swaps/utils'; +import { isSwapsAllowed } from '../../../../../components/UI/Swaps/utils'; import { swapsUtils } from '@metamask/swaps-controller'; import { regex } from '../../../../../util/regex'; import { AmountViewSelectorsIDs } from '../../../../../../e2e/selectors/SendFlow/AmountView.selectors'; -import { isNetworkRampNativeTokenSupported } from '../../../../../components/UI/Ramp/utils'; import { withMetricsAwareness } from '../../../../../components/hooks/useMetrics'; import { selectGasFeeEstimates } from '../../../../../selectors/confirmTransaction'; import { selectGasFeeControllerEstimateType } from '../../../../../selectors/gasFeeController'; +import { isNetworkRampNativeTokenSupported } from '../../../../../components/UI/Ramp/utils'; import { createBuyNavigationDetails } from '../../../../UI/Ramp/routes/utils'; const KEYBOARD_OFFSET = Device.isSmallDevice() ? 80 : 120; diff --git a/app/core/Permissions/specifications.js b/app/core/Permissions/specifications.js index 6d3b0dca2f1..dde9c9fdb07 100644 --- a/app/core/Permissions/specifications.js +++ b/app/core/Permissions/specifications.js @@ -398,6 +398,7 @@ export const unrestrictedMethods = Object.freeze([ 'metamask_logWeb3ShimUsage', 'wallet_switchEthereumChain', 'wallet_addEthereumChain', + 'wallet_swapAsset', ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) 'wallet_getAllSnaps', 'wallet_getSnaps', diff --git a/app/core/RPCMethods/RPCMethodMiddleware.test.ts b/app/core/RPCMethods/RPCMethodMiddleware.test.ts index 4f46dc579f3..e216451b5aa 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.test.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.test.ts @@ -17,7 +17,9 @@ import { PermissionController, } from '@metamask/permission-controller'; import PPOMUtil from '../../lib/ppom/ppom-util'; -import { backgroundState } from '../../util/test/initial-root-state'; +import initialRootState, { + backgroundState, +} from '../../util/test/initial-root-state'; import { Store } from 'redux'; import { RootState } from 'app/reducers'; import { addTransaction } from '../../util/transaction-controller'; @@ -28,6 +30,7 @@ import { unrestrictedMethods, } from '../Permissions/specifications'; import { EthAccountType, EthMethod } from '@metamask/keyring-api'; +import { merge } from 'lodash'; import { processOriginThrottlingRejection, validateOriginThrottling, @@ -37,6 +40,8 @@ import { OriginThrottlingState, } from '../redux/slices/originThrottling'; import { ProviderConfig } from '../../selectors/networkController'; +import { mockNetworkState } from '../../util/test/network'; +import { createMockInternalAccount } from '../../util/test/accountsControllerTestUtils'; jest.mock('./spam'); @@ -61,6 +66,7 @@ jest.mock('../Engine', () => ({ }), }, }, + setSelectedAddress: jest.fn(), })); const MockEngine = jest.mocked(Engine); @@ -114,7 +120,10 @@ function assertIsJsonRpcSuccess( throw new Error(`Response is missing 'result' property`); } } - +// Mock the navigation object. +const navigation = { + navigate: jest.fn(), +}; /** * Return a minimal set of options for `getRpcMethodMiddleware`. These options * are complete enough to test at least some method handlers, and they are type- @@ -126,7 +135,7 @@ function getMinimalOptions() { return { hostname: '', getProviderState: jest.fn(), - navigation: jest.fn(), + navigation, url: { current: '' }, title: { current: '' }, icon: { current: undefined }, @@ -244,10 +253,23 @@ function setupGlobalState({ selectedNetworkClientId: string; networksMetadata?: Record; networkConfigurationsByChainId?: Record; - providerConfig?: ProviderConfig; selectedAddress?: string; originThrottling?: OriginThrottlingState; }) { + const mockState: RootState = merge({}, initialRootState, { + browser: activeTab + ? { + activeTab, + } + : {}, + engine: { + backgroundState: { + ...backgroundState, + PreferencesController: selectedAddress ? { selectedAddress } : {}, + }, + }, + }); + jest.spyOn(store, 'getState').mockImplementation(() => mockState); // TODO: Remove any cast once PermissionController type is fixed. Currently, the state shows never. jest // TODO: Replace "any" with type @@ -1382,6 +1404,326 @@ describe('getRpcMethodMiddleware', () => { expect(spy).toBeCalledTimes(1); }); }); + describe('wallet_swapAsset', () => { + it('should throw error if the account sent by the dapp is not the one connected', async () => { + const mockState: RootState = merge({}, initialRootState, { + swaps: { '0x1': { isLive: true }, hasOnboarded: false, isLive: true }, + fiatOrders: { + networks: [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + }, + ], + }, + engine: { + backgroundState: { + ...backgroundState, + NetworkController: { + ...mockNetworkState({ + chainId: '0x1', + id: 'mainnet', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', + blockExplorerUrl: 'https://goerli.lineascan.build', + }), + }, + }, + }, + }); + jest.spyOn(store, 'getState').mockImplementation(() => mockState); + const middleware = getRpcMethodMiddleware({ + ...getMinimalOptions(), + hostname: 'example.metamask.io', + }); + const request = { + jsonrpc, + id: 1, + method: 'wallet_swapAsset', + params: [ + { + fromToken: [ + { + // DAI address + address: 'eip155:1:0x6b175474e89094c44da98b954eedeac495271d0f', + value: '0xDE0B6B3A7640000', + }, + ], + toToken: { + // ETH address + address: 'eip155:1:0x0000000000000000000000000000000000000000', + }, + userAddress: 'eip155:1:0x0234', + }, + ], + }; + + const response = await callMiddleware({ middleware, request }); + //@ts-expect-error now the response can have an error property + await expect(response?.error?.message).toStrictEqual( + 'The swap could not be completed as requested', + ); + }); + + it('should throw error if it was sent more than one token to swap from', async () => { + const mockState: RootState = merge({}, initialRootState, { + swaps: { '0x1': { isLive: true }, hasOnboarded: false, isLive: true }, + fiatOrders: { + networks: [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + }, + ], + }, + engine: { + backgroundState: { + ...backgroundState, + NetworkController: { + ...mockNetworkState({ + chainId: '0x1', + id: 'mainnet', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', + blockExplorerUrl: 'https://goerli.lineascan.build', + }), + }, + }, + }, + }); + jest.spyOn(store, 'getState').mockImplementation(() => mockState); + const middleware = getRpcMethodMiddleware({ + ...getMinimalOptions(), + hostname: 'example.metamask.io', + }); + const request = { + jsonrpc, + id: 1, + method: 'wallet_swapAsset', + params: [ + { + fromToken: [ + { + // DAI address + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + value: '0xDE0B6B3A7640000', + }, + { + // ETH address + address: 'eip155:1:0x0000000000000000000000000000000000000000', + value: '0xDE0B6B3A7640000', + }, + ], + toToken: { + chainId: '0x1', + // ETH address + address: 'eip155:1:0x0000000000000000000000000000000000000000', + }, + userAddress: 'eip155:1:0x0', + }, + ], + }; + + const response = await callMiddleware({ middleware, request }); + //@ts-expect-error now the response can have an error property + await expect(response?.error?.message).toStrictEqual( + 'Currently we de not support multiple tokens swap', + ); + }); + + it('should throw error if address required param is not defined', async () => { + const mockState = merge({}, initialRootState, { + swaps: { '0x1': { isLive: true }, hasOnboarded: false, isLive: true }, + fiatOrders: { + networks: [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + }, + ], + }, + engine: { + backgroundState: { + ...backgroundState, + NetworkController: { + ...mockNetworkState({ + chainId: '0x1', + id: 'mainnet', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', + blockExplorerUrl: 'https://goerli.lineascan.build', + }), + }, + }, + }, + }); + jest.spyOn(store, 'getState').mockImplementation(() => mockState); + const middleware = getRpcMethodMiddleware({ + ...getMinimalOptions(), + hostname: 'example.metamask.io', + }); + const request = { + jsonrpc, + id: 1, + method: 'wallet_swapAsset', + params: [ + { + fromToken: [ + { + // DAI address + value: '0xDE0B6B3A7640000', + }, + ], + toToken: { + // ETH address + address: 'eip155:1:0x0000000000000000000000000000000000000000', + }, + userAddress: 'eip155:1:0x0', + }, + ], + }; + + const response = await callMiddleware({ middleware, request }); + //@ts-expect-error now the response can have an error property + await expect(response?.error?.message).toStrictEqual( + 'address property of fromToken is not defined', + ); + }); + + it('should throw error if swap is not live', async () => { + const mockState = merge({}, initialRootState, { + swaps: { '0x1': { isLive: false }, hasOnboarded: false, isLive: false }, + fiatOrders: { + networks: [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + }, + ], + }, + engine: { + backgroundState: { + ...backgroundState, + NetworkController: { + ...mockNetworkState({ + chainId: '0x1', + id: 'mainnet', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', + blockExplorerUrl: 'https://goerli.lineascan.build', + }), + }, + }, + }, + }); + jest.spyOn(store, 'getState').mockImplementation(() => mockState); + const middleware = getRpcMethodMiddleware({ + ...getMinimalOptions(), + hostname: 'example.metamask.io', + }); + const request = { + jsonrpc, + id: 1, + method: 'wallet_swapAsset', + params: [ + { + fromToken: [ + { + // DAI address + address: 'eip155:1:0x6b175474e89094c44da98b954eedeac495271d0f', + value: '0xDE0B6B3A7640000', + }, + ], + toToken: { + // ETH address + address: 'eip155:1:0x0000000000000000000000000000000000000000', + }, + userAddress: 'eip155:1:0x0', + }, + ], + }; + + const response = await callMiddleware({ middleware, request }); + //@ts-expect-error now the response can have an error property + await expect(response?.error?.message).toStrictEqual( + 'Swap is not available on this chain Ethereum Mainnet', + ); + }); + + it('should navigate to SwapsAmountView if all conditions are met', async () => { + const mockState = merge({}, initialRootState, { + swaps: { '0x1': { isLive: true }, hasOnboarded: false, isLive: true }, + fiatOrders: { + networks: [ + { + active: true, + chainId: 1, + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + }, + ], + }, + engine: { + backgroundState: { + ...backgroundState, + NetworkController: { + ...mockNetworkState({ + chainId: '0x1', + id: 'mainnet', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', + blockExplorerUrl: 'https://goerli.lineascan.build', + }), + }, + }, + }, + }); + jest.spyOn(store, 'getState').mockImplementation(() => mockState); + const middleware = getRpcMethodMiddleware({ + ...getMinimalOptions(), + hostname: 'example.metamask.io', + }); + const request = { + jsonrpc, + id: 1, + method: 'wallet_swapAsset', + params: [ + { + fromToken: [ + { + // DAI address + address: 'eip155:1:0x6b175474e89094c44da98b954eedeac495271d0f', + value: '0xDE0B6B3A7640000', + }, + ], + toToken: { + // ETH address + address: 'eip155:1:0x0000000000000000000000000000000000000000', + }, + userAddress: 'eip155:1:0x0', + }, + ], + }; + + await callMiddleware({ middleware, request }); + expect(navigation.navigate).toHaveBeenCalledWith('Swaps', { + screen: 'SwapsAmountView', + params: { + sourceToken: '0x6b175474e89094c44da98b954eedeac495271d0f', + destinationToken: '0x0000000000000000000000000000000000000000', + amount: '1', + }, + }); + }); + }); describe('originThrottling', () => { const assumedBlockableRPCMethod = 'eth_sendTransaction'; diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 033b42c9158..e1eacd3c8b4 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -10,13 +10,25 @@ import { import { recoverPersonalSignature } from '@metamask/eth-sig-util'; import RPCMethods from './index.js'; import { RPC } from '../../constants/network'; -import { ChainId, NetworkType } from '@metamask/controller-utils'; +import { ChainId, NetworkType, toHex } from '@metamask/controller-utils'; import { PermissionController, permissionRpcMethods, } from '@metamask/permission-controller'; -import { blockTagParamIndex, getAllNetworks } from '../../util/networks'; -import { polyfillGasPrice } from './utils'; +import { + CaipAccountAddress, + CaipAccountId, + CaipChainId, + CaipNamespace, + CaipReference, + parseCaipAccountId, +} from '@metamask/utils'; +import { + blockTagParamIndex, + getAllNetworks, + getNetworkNameFromProviderConfig, +} from '../../util/networks'; +import { polyfillGasPrice, validateParams } from './utils'; import { processOriginThrottlingRejection, validateOriginThrottling, @@ -31,13 +43,20 @@ import { v1 as random } from 'uuid'; import { getPermittedAccounts } from '../Permissions'; import AppConstants from '../AppConstants'; import PPOMUtil from '../../lib/ppom/ppom-util'; -import { selectProviderConfig } from '../../selectors/networkController'; +import { + selectChainId, + selectProviderConfig, +} from '../../selectors/networkController'; import { setEventStageError, setEventStage } from '../../actions/rpcEvents'; import { isWhitelistedRPC, RPCStageTypes } from '../../reducers/rpcEvents'; import { regex } from '../../../app/util/regex'; +import { swapsLivenessSelector } from '../../reducers/swaps/index.js'; +import { isSwapsAllowed } from '../../components/UI/Swaps/utils/index.js'; +import { fromWei } from '../../util/number/index.js'; import Logger from '../../../app/util/Logger'; import DevLogger from '../SDKConnect/utils/DevLogger'; import { addTransaction } from '../../util/transaction-controller'; +import { selectSelectedInternalAccountChecksummedAddress } from '../../selectors/accountsController'; import Routes from '../../constants/navigation/Routes'; import { endTrace, trace, TraceName } from '../../util/trace'; import { @@ -398,6 +417,140 @@ export const getRpcMethodMiddleware = ({ // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const rpcMethods: any = { + wallet_swapAsset: async () => { + const accounts = await getPermittedAccounts(origin); + const permittedAccounts = accounts.map(safeToChecksumAddress); + const { fromToken, toToken, userAddress } = req.params[0]; + const selectedAddress = selectSelectedInternalAccountChecksummedAddress( + store.getState(), + ); + // Implement on utils repo a interface for this + let parsedCaip10UserAddress: { + address: CaipAccountAddress; + chainId: CaipChainId; + chain: { namespace: CaipNamespace; reference: CaipReference }; + }; + try { + parsedCaip10UserAddress = parseCaipAccountId( + userAddress as CaipAccountId, + ); + } catch (error) { + throw rpcErrors.invalidParams('Invalid caip-10 user address'); + } + if (parsedCaip10UserAddress.chain.namespace !== 'eip155') { + throw new Error('Only support Ethereum addresses at the moment'); + } + + const dappConnectedAccount = permittedAccounts.find((address) => { + if (!address) return undefined; + return ( + safeToChecksumAddress(address) === + safeToChecksumAddress(parsedCaip10UserAddress.address) + ); + }); + + if (!dappConnectedAccount) { + throw rpcErrors.invalidParams( + 'The swap could not be completed as requested', + ); + } + + // This condition is only needed until we support multiple source tokens swap + if (fromToken.length > 1) { + throw rpcErrors.methodNotSupported( + 'Currently we de not support multiple tokens swap', + ); + } + + validateParams(fromToken[0], ['address'], 'fromToken'); + validateParams(toToken, ['address'], 'toToken'); + + // Implement on utils repo a interface for this + let parsedCaip10FromTokenAddress: { + address: CaipAccountAddress; + chainId: CaipChainId; + chain: { namespace: CaipNamespace; reference: CaipReference }; + }; + try { + parsedCaip10FromTokenAddress = parseCaipAccountId( + fromToken[0].address, + ); + } catch (error) { + throw rpcErrors.invalidParams('Invalid caip-10 fromToken address'); + } + + if (parsedCaip10FromTokenAddress.chain.namespace !== 'eip155') { + throw new Error('Only support Ethereum addresses at the moment'); + } + + // Implement on utils repo a interface for this + let parsedCaip10ToTokenAddress: { + address: CaipAccountAddress; + chainId: CaipChainId; + chain: { namespace: CaipNamespace; reference: CaipReference }; + }; + try { + parsedCaip10ToTokenAddress = parseCaipAccountId(toToken.address); + } catch (error) { + throw rpcErrors.invalidParams('Invalid caip-10 toToken address'); + } + + if (parsedCaip10ToTokenAddress.chain.namespace !== 'eip155') { + throw new Error('Only support Ethereum addresses at the moment'); + } + + if ( + parsedCaip10FromTokenAddress.chainId !== + parsedCaip10ToTokenAddress.chainId + ) { + throw rpcErrors.methodNotSupported( + 'Cross-chain swaps are currently not supported. Both fromToken and toToken must be on the same blockchain.', + ); + } + + const chainId = selectChainId(store.getState()); + + if (chainId !== toHex(parsedCaip10FromTokenAddress.chain.reference)) { + throw rpcErrors.invalidParams( + `Invalid parameters: active chainId is different than the one provided.`, + ); + } + + const checksummedDappConnectedAccount = + safeToChecksumAddress(dappConnectedAccount); + + if (selectedAddress !== checksummedDappConnectedAccount) { + Engine.setSelectedAddress(checksummedDappConnectedAccount); + } + + // switch to the chain id asked from the dapp + // validate if swaps is enable on that network + const swapsIsLive = swapsLivenessSelector(store.getState()); + const isSwappable = isSwapsAllowed(chainId) && swapsIsLive; + + if (!isSwappable) { + const providerConfig = selectProviderConfig(store.getState()); + + const networkName = getNetworkNameFromProviderConfig(providerConfig); + + Alert.alert(`Swap is not available on this chain ${networkName}`); + throw rpcErrors.methodNotSupported( + `Swap is not available on this chain ${networkName}`, + ); + } + //If value is not defined by the dapp it defaults to 0 + const decimalWei = parseInt(fromToken[0].value ?? 0, 16); + const tokenAmount = fromWei(decimalWei); + + navigation.navigate('Swaps', { + screen: 'SwapsAmountView', + params: { + sourceToken: parsedCaip10FromTokenAddress.address, + destinationToken: parsedCaip10ToTokenAddress.address, + amount: tokenAmount, + }, + }); + }, wallet_getPermissions: async () => // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/app/core/RPCMethods/utils.ts b/app/core/RPCMethods/utils.ts index d5b2c577611..67bbcdb7f2b 100644 --- a/app/core/RPCMethods/utils.ts +++ b/app/core/RPCMethods/utils.ts @@ -145,6 +145,24 @@ export const polyfillGasPrice = async ( return data; }; + +export const validateParams = ( + obj: Record, + properties: string[], + name: string, +): void => { + if (!obj) { + throw rpcErrors.invalidParams(`"${name}" is not defined`); + } + properties.forEach((property) => { + if (!obj[property]) { + throw rpcErrors.invalidParams( + `${property} property of ${name} is not defined`, + ); + } + }); +}; + export default { polyfillGasPrice, };