diff --git a/craco.config.cjs b/craco.config.cjs index 824149dedb..ca3e18f3f3 100644 --- a/craco.config.cjs +++ b/craco.config.cjs @@ -56,10 +56,10 @@ module.exports = { '\\.css\\.ts$': '@vanilla-extract/jest-transform', '\\.(t|j)sx?$': '@swc/jest', }, - // Use @uniswap/conedison's build directly, as jest does not support its exports. - transformIgnorePatterns: ['@uniswap/conedison/provider'], + // Use d3-arrays's build directly, as jest does not support its exports. + transformIgnorePatterns: ['d3-array'], moduleNameMapper: { - '@uniswap/conedison/provider': '@uniswap/conedison/dist/provider', + 'd3-array': 'd3-array/dist/d3-array.min.js', }, }) }, diff --git a/package.json b/package.json index cc24a06232..be235d244f 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@types/array.prototype.flat": "^1.2.1", "@types/array.prototype.flatmap": "^1.2.2", "@types/d3": "^6.7.1", - "@types/jest": "^25.2.1", + "@types/jest": "^27.0.1", "@types/lingui__core": "^2.7.1", "@types/lingui__macro": "^2.7.4", "@types/lingui__react": "^2.8.3", @@ -192,7 +192,6 @@ "@types/react-window-infinite-loader": "^1.0.6", "@uniswap/analytics": "^1.4.0", "@uniswap/analytics-events": "^2.17.0", - "@uniswap/conedison": "^1.8.0", "@uniswap/governance": "^1.0.2", "@uniswap/liquidity-staker": "^1.0.2", "@uniswap/merkle-distributor": "^1.0.1", diff --git a/src/components/Web3Provider/index.tsx b/src/components/Web3Provider/index.tsx index 1e29cb286a..e5ff0a6c90 100644 --- a/src/components/Web3Provider/index.tsx +++ b/src/components/Web3Provider/index.tsx @@ -1,5 +1,4 @@ import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events' -import { getWalletMeta } from '@uniswap/conedison/provider/meta' import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core' import { Connector } from '@web3-react/types' import { sendAnalyticsEvent, user } from 'analytics' @@ -13,6 +12,7 @@ import { ReactNode, useEffect } from 'react' import { useLocation } from 'react-router-dom' import { useConnectedWallets } from 'state/wallets/hooks' import { getCurrentPageFromLocation } from 'utils/urlRoutes' +import { getWalletMeta } from 'utils/walletMeta' export default function Web3Provider({ children }: { children: ReactNode }) { useEagerlyConnect() diff --git a/src/hooks/usePermitAllowance.ts b/src/hooks/usePermitAllowance.ts index 996e7cfe33..40a776078c 100644 --- a/src/hooks/usePermitAllowance.ts +++ b/src/hooks/usePermitAllowance.ts @@ -1,4 +1,3 @@ -import { signTypedData } from '@uniswap/conedison/provider/signing' import { AllowanceTransfer, MaxAllowanceTransferAmount, PERMIT2_ADDRESS, PermitSingle } from '@uniswap/permit2-sdk' import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' @@ -9,6 +8,7 @@ import { useSingleCallResult } from 'lib/hooks/multicall' import ms from 'ms' import { useCallback, useEffect, useMemo, useState } from 'react' import { toReadableError, UserRejectedRequestError } from 'utils/errors' +import { signTypedData } from 'utils/signing' import { didUserReject } from 'utils/swapErrorToUserReadableMessage' const PERMIT_EXPIRATION = ms(`30d`) @@ -77,7 +77,6 @@ export function useUpdatePermitAllowance( } const { domain, types, values } = AllowanceTransfer.getPermitData(permit, PERMIT2_ADDRESS, chainId) - // Use conedison's signTypedData for better x-wallet compatibility. const signature = await signTypedData(provider.getSigner(account), domain, types, values) onPermitSignature?.({ ...permit, signature }) return diff --git a/src/hooks/useUniswapXSwapCallback.ts b/src/hooks/useUniswapXSwapCallback.ts index 94e66cb711..c898b7b9ab 100644 --- a/src/hooks/useUniswapXSwapCallback.ts +++ b/src/hooks/useUniswapXSwapCallback.ts @@ -1,7 +1,6 @@ import { BigNumber } from '@ethersproject/bignumber' import * as Sentry from '@sentry/react' import { SwapEventName } from '@uniswap/analytics-events' -import { signTypedData } from '@uniswap/conedison/provider/signing' import { Percent } from '@uniswap/sdk-core' import { DutchOrder, DutchOrderBuilder } from '@uniswap/uniswapx-sdk' import { useWeb3React } from '@web3-react/core' @@ -11,6 +10,7 @@ import { useCallback } from 'react' import { DutchOrderTrade, TradeFillType } from 'state/routing/types' import { trace } from 'tracing/trace' import { UserRejectedRequestError } from 'utils/errors' +import { signTypedData } from 'utils/signing' import { didUserReject, swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMessage' const DEFAULT_START_TIME_PADDING_SECONDS = 30 diff --git a/src/utils/signing.test.ts b/src/utils/signing.test.ts new file mode 100644 index 0000000000..3a0a050846 --- /dev/null +++ b/src/utils/signing.test.ts @@ -0,0 +1,154 @@ +import { ExternalProvider, JsonRpcProvider, JsonRpcSigner, Web3Provider } from '@ethersproject/providers' +import { signTypedData } from 'utils/signing' + +type Mutable = { -readonly [P in keyof T]: T[P] } + +describe('signing', () => { + describe('signTypedData', () => { + const wallet = '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826' + const domain = { + name: 'Ether Mail', + version: '1', + chainId: '1', + verifyingContract: '0xcccccccccccccccccccccccccccccccccccccccc', + } + + const types = { + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + } + + const value = { + from: { + name: 'Cow', + wallet, + }, + to: { + name: 'Bob', + wallet: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + }, + contents: 'Hello, Bob!', + } + + let signer: JsonRpcSigner + beforeEach(() => { + signer = new JsonRpcProvider().getSigner() + jest.spyOn(signer, 'getAddress').mockResolvedValue(wallet) + }) + + function itFallsBackToEthSignIfUnimplemented(signingMethod: string) { + it.each(['not found', 'not implemented'])(`falls back to eth_sign if ${signingMethod} is %s`, async (message) => { + const send = jest + .spyOn(signer.provider, 'send') + .mockImplementationOnce((method) => { + if (method === signingMethod) return Promise.reject({ message: `method ${message}` }) + throw new Error('Unimplemented') + }) + .mockImplementationOnce((method) => { + if (method === 'eth_sign') return Promise.resolve() + throw new Error('Unimplemented') + }) + jest.spyOn(console, 'warn').mockImplementation(() => undefined) + + await signTypedData(signer, domain, types, value) + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('signTypedData: wallet does not implement EIP-712, falling back to eth_sign'), + expect.anything() + ) + expect(send).toHaveBeenCalledTimes(2) + expect(send).toHaveBeenCalledWith(signingMethod, [wallet, expect.anything()]) + expect(send).toHaveBeenCalledWith('eth_sign', [wallet, expect.anything()]) + const hash = send.mock.lastCall[1]?.[1] + expect(hash).toBe('0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2') + }) + } + + function itFailsIfRejected(signingMethod: string) { + it('fails if rejected', async () => { + const send = jest.spyOn(signer.provider, 'send').mockImplementationOnce((method) => { + if (method === signingMethod) return Promise.reject(new Error('User rejected')) + throw new Error('Unimplemented') + }) + + await expect(async () => await signTypedData(signer, domain, types, value)).rejects.toThrow('User rejected') + expect(send).toHaveBeenCalledTimes(1) + expect(send).toHaveBeenCalledWith(signingMethod, [wallet, expect.anything()]) + const data = send.mock.lastCall[1]?.[1] + expect(JSON.parse(data)).toEqual(expect.objectContaining({ domain, message: value })) + }) + } + + it('signs using eth_signTypedData_v4', async () => { + const send = jest.spyOn(signer.provider, 'send').mockImplementationOnce((method) => { + if (method === 'eth_signTypedData_v4') return Promise.resolve() + throw new Error('Unimplemented') + }) + + await signTypedData(signer, domain, types, value) + expect(send).toHaveBeenCalledTimes(1) + expect(send).toHaveBeenCalledWith('eth_signTypedData_v4', [wallet, expect.anything()]) + const data = send.mock.lastCall[1]?.[1] + expect(JSON.parse(data)).toEqual(expect.objectContaining({ domain, message: value })) + }) + + itFallsBackToEthSignIfUnimplemented('eth_signTypedData_v4') + itFailsIfRejected('eth_signTypedData_v4') + + describe('wallets which do not support eth_signTypedData_v4', () => { + describe.each(['SafePal Wallet', 'Ledger Wallet Connect'])('%s', (name) => { + beforeEach(() => { + const web3Provider = signer.provider as Mutable + web3Provider.provider = { + isWalletConnect: true, + session: { peer: { metadata: { name } } }, + } as ExternalProvider + }) + + it('signs using eth_signTypedData', async () => { + const send = jest.spyOn(signer.provider, 'send').mockImplementationOnce((method) => { + if (method === 'eth_signTypedData') return Promise.resolve() + throw new Error('Unimplemented') + }) + + await signTypedData(signer, domain, types, value) + expect(send).toHaveBeenCalledTimes(1) + expect(send).toHaveBeenCalledWith('eth_signTypedData', [wallet, expect.anything()]) + const data = send.mock.lastCall[1]?.[1] + expect(JSON.parse(data)).toEqual(expect.objectContaining({ domain, message: value })) + }) + + itFallsBackToEthSignIfUnimplemented('eth_signTypedData') + itFailsIfRejected('eth_signTypedData') + }) + }) + + describe('TrustWallet fallback for eth_signTypedData_v4', () => { + beforeEach(() => { + const web3Provider = signer.provider as Mutable + web3Provider.provider = { + isWalletConnect: true, + session: { peer: { metadata: { name: 'Trust Wallet' } } }, + } as ExternalProvider + }) + + it('signs using eth_sign', async () => { + jest.spyOn(console, 'warn').mockReturnValue() + const send = jest.spyOn(signer.provider, 'send').mockImplementation((method) => { + if (method === 'eth_sign') return Promise.resolve() + throw new Error('TrustWalletConnect.WCError error 1') + }) + + await signTypedData(signer, domain, types, value) + expect(send).toHaveBeenCalledTimes(2) + expect(send).toHaveBeenCalledWith('eth_sign', [wallet, expect.anything()]) + }) + }) + }) +}) diff --git a/src/utils/signing.ts b/src/utils/signing.ts new file mode 100644 index 0000000000..1d56af1f1a --- /dev/null +++ b/src/utils/signing.ts @@ -0,0 +1,64 @@ +import type { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer' +import { _TypedDataEncoder } from '@ethersproject/hash' +import type { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers' +import { getWalletMeta, WalletType } from 'utils/walletMeta' + +// These are WalletConnect peers which do not implement eth_signTypedData_v4, but *do* implement eth_signTypedData. +// They are special-cased so that signing will still use EIP-712 (which is safer for the user). +const WC_PEERS_LACKING_V4_SUPPORT = ['SafePal Wallet', 'Ledger Wallet Connect'] + +// Assumes v4 support by default, except for known wallets. +function supportsV4(provider: JsonRpcProvider): boolean { + const meta = getWalletMeta(provider) + if (meta) { + const { type, name } = meta + if (name) { + if (type === WalletType.WALLET_CONNECT && name && WC_PEERS_LACKING_V4_SUPPORT.includes(name)) { + return false + } + } + } + + return true +} + +/** + * Signs TypedData with EIP-712, if available, or else by falling back to eth_sign. + * Calls eth_signTypedData_v4, or eth_signTypedData for wallets with incomplete EIP-712 support. + * + * @see https://github.com/ethers-io/ethers.js/blob/c80fcddf50a9023486e9f9acb1848aba4c19f7b6/packages/providers/src.ts/json-rpc-provider.ts#L334 + */ +export async function signTypedData( + signer: JsonRpcSigner, + domain: TypedDataDomain, + types: Record, + // Use Record for the value to match the JsonRpcSigner._signTypedData signature. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: Record +) { + // Populate any ENS names (in-place) + const populated = await _TypedDataEncoder.resolveNames(domain, types, value, (name: string) => { + return signer.provider.resolveName(name) as Promise + }) + + const method = supportsV4(signer.provider) ? 'eth_signTypedData_v4' : 'eth_signTypedData' + const address = (await signer.getAddress()).toLowerCase() + const message = JSON.stringify(_TypedDataEncoder.getPayload(populated.domain, types, populated.value)) + + try { + return await signer.provider.send(method, [address, message]) + } catch (error) { + // If eth_signTypedData is unimplemented, fall back to eth_sign. + if ( + typeof error.message === 'string' && + (error.message.match(/not (found|implemented)/i) || + error.message.match(/TrustWalletConnect.WCError error 1/) || + error.message.match(/Missing or invalid/)) + ) { + console.warn('signTypedData: wallet does not implement EIP-712, falling back to eth_sign', error.message) + const hash = _TypedDataEncoder.hash(populated.domain, types, populated.value) + return await signer.provider.send('eth_sign', [address, hash]) + } + throw error + } +} diff --git a/src/utils/walletMeta.test.ts b/src/utils/walletMeta.test.ts new file mode 100644 index 0000000000..114d92638e --- /dev/null +++ b/src/utils/walletMeta.test.ts @@ -0,0 +1,87 @@ +import type { ExternalProvider } from '@ethersproject/providers' +import { JsonRpcProvider } from '@ethersproject/providers' +import type WalletConnectProvider from '@walletconnect/ethereum-provider' +import { getWalletMeta, WalletMeta, WalletType } from 'utils/walletMeta' + +class MockJsonRpcProvider extends JsonRpcProvider { + name = 'JsonRpcProvider' + arg: string + + constructor(arg?: unknown) { + super() + this.arg = JSON.stringify(arg) + } +} + +const WC_META = { name: 'name', description: 'description', url: 'url', icons: [] } + +class MockWalletConnectProviderV2 extends MockJsonRpcProvider { + name = WalletType.WALLET_CONNECT + provider: WalletConnectProvider + + constructor(metadata: typeof WC_META | null) { + super(metadata) + this.provider = { isWalletConnect: true, session: { peer: { metadata } } } as unknown as WalletConnectProvider + } +} + +class MockInjectedProvider extends MockJsonRpcProvider { + name = WalletType.INJECTED + provider: ExternalProvider + + constructor(provider: Record) { + super(provider) + this.provider = { + isConnected() { + return true + }, + ...provider, + } as ExternalProvider + } +} + +const testCases: [MockJsonRpcProvider, WalletMeta | undefined][] = [ + [new MockJsonRpcProvider(), undefined], + [new MockWalletConnectProviderV2(null), { type: WalletType.WALLET_CONNECT, agent: '(WalletConnect)' }], + [ + new MockWalletConnectProviderV2(WC_META), + { type: WalletType.WALLET_CONNECT, agent: 'name (WalletConnect)', ...WC_META }, + ], + [new MockInjectedProvider({}), { type: WalletType.INJECTED, agent: '(Injected)', name: undefined }], + [ + new MockInjectedProvider({ isMetaMask: false }), + { type: WalletType.INJECTED, agent: '(Injected)', name: undefined }, + ], + [ + new MockInjectedProvider({ isMetaMask: true }), + { type: WalletType.INJECTED, agent: 'MetaMask (Injected)', name: 'MetaMask' }, + ], + [ + new MockInjectedProvider({ isTest: true, isMetaMask: true }), + { type: WalletType.INJECTED, agent: 'Test MetaMask (Injected)', name: 'Test' }, + ], + [ + new MockInjectedProvider({ isCoinbaseWallet: true, qrUrl: undefined }), + { type: WalletType.INJECTED, agent: 'CoinbaseWallet (Injected)', name: 'CoinbaseWallet' }, + ], + [ + new MockInjectedProvider({ isCoinbaseWallet: true, qrUrl: true }), + { type: WalletType.INJECTED, agent: 'CoinbaseWallet qrUrl (Injected)', name: 'CoinbaseWallet' }, + ], + [ + new MockInjectedProvider({ isA: true, isB: false }), + { type: WalletType.INJECTED, agent: 'A (Injected)', name: 'A' }, + ], + [ + new MockInjectedProvider({ isA: true, isB: true }), + { type: WalletType.INJECTED, agent: 'A B (Injected)', name: 'A' }, + ], +] + +describe('meta', () => { + describe.each(testCases)('getWalletMeta/getWalletName returns the project meta/name', (provider, meta) => { + it(`${provider?.name} ${provider.arg}`, () => { + expect(getWalletMeta(provider)).toEqual(meta) + }) + }) +}) diff --git a/src/utils/walletMeta.ts b/src/utils/walletMeta.ts new file mode 100644 index 0000000000..7242f681bb --- /dev/null +++ b/src/utils/walletMeta.ts @@ -0,0 +1,91 @@ +import type { ExternalProvider, JsonRpcProvider, Web3Provider } from '@ethersproject/providers' +import type WalletConnectProvider from '@walletconnect/ethereum-provider' + +function isWeb3Provider(provider: JsonRpcProvider): provider is Web3Provider { + return 'provider' in provider +} + +function isWalletConnectProvider(provider: ExternalProvider): provider is WalletConnectProvider { + return (provider as WalletConnectProvider).isWalletConnect +} + +export enum WalletType { + WALLET_CONNECT = 'WalletConnect', + INJECTED = 'Injected', +} + +/** + * WalletMeta for WalletConnect or Injected wallets. + * + * For WalletConnect wallets, name, description, url, and icons are taken from WalletConnect's peerMeta + * v1: @see https://docs.walletconnect.com/1.0/specs#session-request + * v2: @see https://docs.walletconnect.com/2.0/specs/clients/core/pairing/data-structures#metadata + * + * For Injected wallets, the name is derived from the `is*` properties on the provider (eg `isCoinbaseWallet`). + */ +export interface WalletMeta { + type: WalletType + /** + * The agent string of the wallet, for use with analytics/debugging. + * Denotes the wallet's provenance - analagous to a User String - including all `is*` properties and the type. + * + * Some injected wallets are used different ways (eg with/without spoofing MetaMask). + * The agent will capture these differences, while the name will not. + * + * @example 'CoinbaseWallet qUrl (Injected)' + */ + agent: string + /** + * The name of the wallet, for use with UI. + * + * @example 'CoinbaseWallet' + */ + name?: string + description?: string + url?: string + icons?: string[] +} + +function getWalletConnectMeta(provider: WalletConnectProvider): WalletMeta { + const metadata = provider.session?.peer.metadata + return { + type: WalletType.WALLET_CONNECT, + agent: metadata ? `${metadata.name} (WalletConnect)` : '(WalletConnect)', + ...metadata, + } +} + +function getInjectedMeta(provider: ExternalProvider & Record): WalletMeta { + const properties = Object.getOwnPropertyNames(provider) + const names = + properties + .filter((name) => name.match(/^is.*$/) && (provider as Record)[name] === true) + .map((name) => name.slice(2)) ?? [] + + // Many wallets spoof MetaMask by setting `isMetaMask` along with their own identifier, + // so we sort MetaMask last so that these wallets' names come first. + names.sort((a, b) => (a === 'MetaMask' ? 1 : b === 'MetaMask' ? -1 : 0)) + + // Coinbase Wallet can be connected through an extension or a QR code, with `qrUrl` as the only differentiator, + // so we capture `qrUrl` in the agent string. + if (properties.includes('qrUrl') && provider['qrUrl']) { + names.push('qrUrl') + } + + return { + type: WalletType.INJECTED, + agent: [...names, '(Injected)'].join(' '), + name: names[0], + // TODO(WEB-2914): Populate description, url, and icons for known wallets. + } +} + +export function getWalletMeta(provider: JsonRpcProvider): WalletMeta | undefined { + if (!isWeb3Provider(provider)) return undefined + + if (isWalletConnectProvider(provider.provider)) { + return getWalletConnectMeta(provider.provider) + } else { + return getInjectedMeta(provider.provider) + } +} diff --git a/yarn.lock b/yarn.lock index ebd07db900..be9808b35b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3408,16 +3408,6 @@ slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@^25.5.0": - version "25.5.0" - resolved "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz" - integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" - "@types/yargs" "^15.0.0" - chalk "^3.0.0" - "@jest/types@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" @@ -5672,14 +5662,6 @@ dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-reports@^1.1.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz" - integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== - dependencies: - "@types/istanbul-lib-coverage" "*" - "@types/istanbul-lib-report" "*" - "@types/istanbul-reports@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" @@ -5687,13 +5669,13 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^25.2.1": - version "25.2.3" - resolved "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz" - integrity sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw== +"@types/jest@*", "@types/jest@^27.0.1": + version "27.5.2" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.2.tgz#ec49d29d926500ffb9fd22b84262e862049c026c" + integrity sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA== dependencies: - jest-diff "^25.2.1" - pretty-format "^25.2.1" + jest-matcher-utils "^27.0.0" + pretty-format "^27.0.0" "@types/js-yaml@^4.0.0": version "4.0.5" @@ -6209,11 +6191,6 @@ react "^18.2.0" react-dom "^18.2.0" -"@uniswap/conedison@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.8.0.tgz#60fcfa1475780350a719510ccee1f6a49f8c5cf8" - integrity sha512-EeC37bbrd4sJnqW3TxNhExdWUBvzyJDhbTXcbDzFdBh/bg5sOCu1NbYJEnrjwBHEfewc9nF2OjEzejJFyJUx9A== - "@uniswap/default-token-list@^11.2.0": version "11.2.0" resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-11.2.0.tgz#f7babd37b8f2f1d5c724bd4689be93c4aedd9205" @@ -10391,11 +10368,6 @@ didyoumean@^1.2.2: resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== -diff-sequences@^25.2.6: - version "25.2.6" - resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz" - integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== - diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -13905,16 +13877,6 @@ jest-dev-server@^9.0.0: tree-kill "^1.2.2" wait-on "^7.0.1" -jest-diff@^25.2.1: - version "25.5.0" - resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz" - integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== - dependencies: - chalk "^3.0.0" - diff-sequences "^25.2.6" - jest-get-type "^25.2.6" - pretty-format "^25.5.0" - jest-diff@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" @@ -14031,11 +13993,6 @@ jest-fetch-mock@^3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" -jest-get-type@^25.2.6: - version "25.2.6" - resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz" - integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== - jest-get-type@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" @@ -14124,7 +14081,7 @@ jest-leak-detector@^29.6.1: jest-get-type "^29.4.3" pretty-format "^29.6.1" -jest-matcher-utils@^27.5.1: +jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== @@ -17333,16 +17290,6 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" -pretty-format@^25.2.1, pretty-format@^25.5.0: - version "25.5.0" - resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz" - integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== - dependencies: - "@jest/types" "^25.5.0" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" - react-is "^16.12.0" - pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" @@ -17353,7 +17300,7 @@ pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" -pretty-format@^27.0.2, pretty-format@^27.5.1: +pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==