diff --git a/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch b/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch index 21be6c303395..2fe517135290 100644 --- a/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch +++ b/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch @@ -46,3 +46,16 @@ index f64d13f8de56631345a44e6ebb025e62e03f51bc..99aa7f27c574c94b26daa56091ac50d1 } } }); +diff --git a/dist/token-prices-service/codefi-v2.cjs b/dist/token-prices-service/codefi-v2.cjs +index 34f7bcf4dea1b8d6a1ea45051be09059d9d35353..6aa82360e63727852cda1719f5e893508b764e75 100644 +--- a/dist/token-prices-service/codefi-v2.cjs ++++ b/dist/token-prices-service/codefi-v2.cjs +@@ -98,6 +98,8 @@ exports.SUPPORTED_CURRENCIES = [ + 'mxn', + // Malaysian Ringgit + 'myr', ++ // Monad ++ 'mon', + // Nigerian Naira + 'ngn', + // Norwegian Krone diff --git a/app/components/UI/Bridge/hooks/useInitialSlippage/index.ts b/app/components/UI/Bridge/hooks/useInitialSlippage/index.ts index 269da2ea883c..0e4110c81612 100644 --- a/app/components/UI/Bridge/hooks/useInitialSlippage/index.ts +++ b/app/components/UI/Bridge/hooks/useInitialSlippage/index.ts @@ -8,9 +8,9 @@ import { selectSourceToken, setSlippage, } from '../../../../../core/redux/slices/bridge'; -import { getIsStablecoinPair } from '../../../Swaps/useStablecoinsDefaultSlippage'; import { isHex } from 'viem'; import AppConstants from '../../../../../core/AppConstants'; +import { getIsStablecoinPair } from '../useStablecoinsDefaultSlippage'; export const useInitialSlippage = () => { const dispatch = useDispatch(); diff --git a/app/components/UI/Swaps/useStablecoinsDefaultSlippage.test.tsx b/app/components/UI/Bridge/hooks/useStablecoinsDefaultSlippage/index.test.tsx similarity index 97% rename from app/components/UI/Swaps/useStablecoinsDefaultSlippage.test.tsx rename to app/components/UI/Bridge/hooks/useStablecoinsDefaultSlippage/index.test.tsx index 87e02062e80f..bd0527878d8c 100644 --- a/app/components/UI/Swaps/useStablecoinsDefaultSlippage.test.tsx +++ b/app/components/UI/Bridge/hooks/useStablecoinsDefaultSlippage/index.test.tsx @@ -1,11 +1,8 @@ -import { renderHookWithProvider } from '../../../util/test/renderWithProvider'; -import { - useStablecoinsDefaultSlippage, - handleEvmStablecoinSlippage, -} from './useStablecoinsDefaultSlippage'; +import { useStablecoinsDefaultSlippage, handleEvmStablecoinSlippage } from './'; import { Hex } from '@metamask/utils'; import { CHAIN_IDS } from '@metamask/transaction-controller'; -import AppConstants from '../../../core/AppConstants'; +import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; +import AppConstants from '../../../../../core/AppConstants'; describe('useStablecoinsDefaultSlippage', () => { const mockSetSlippage = jest.fn(); diff --git a/app/components/UI/Swaps/useStablecoinsDefaultSlippage.ts b/app/components/UI/Bridge/hooks/useStablecoinsDefaultSlippage/index.ts similarity index 96% rename from app/components/UI/Swaps/useStablecoinsDefaultSlippage.ts rename to app/components/UI/Bridge/hooks/useStablecoinsDefaultSlippage/index.ts index 5a6c121eb65c..b9412f4d9f9d 100644 --- a/app/components/UI/Swaps/useStablecoinsDefaultSlippage.ts +++ b/app/components/UI/Bridge/hooks/useStablecoinsDefaultSlippage/index.ts @@ -1,10 +1,10 @@ import { useEffect } from 'react'; -import AppConstants from '../../../core/AppConstants'; import { Hex } from '@metamask/utils'; import { toChecksumHexAddress } from '@metamask/controller-utils'; -import usePrevious from '../../hooks/usePrevious'; -import { NETWORKS_CHAIN_ID } from '../../../constants/network'; import { CHAIN_IDS } from '@metamask/transaction-controller'; +import AppConstants from '../../../../../core/AppConstants'; +import { NETWORKS_CHAIN_ID } from '../../../../../constants/network'; +import usePrevious from '../../../../hooks/usePrevious'; // USDC and USDT for now const StablecoinsByChainId: Partial>> = { diff --git a/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx b/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx index 23ed6a693471..635f4b2a9322 100644 --- a/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx +++ b/app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.tsx @@ -65,6 +65,7 @@ import ButtonHero from '../../../../../component-library/components-temp/Buttons import { usePredictRewards } from '../../hooks/usePredictRewards'; import { TraceName } from '../../../../../util/trace'; import { usePredictMeasurement } from '../../hooks/usePredictMeasurement'; +import { PredictBuyPreviewSelectorsIDs } from '../../../../../../e2e/selectors/Predict/Predict.selectors'; const PredictBuyPreview = () => { const tw = useTailwind(); @@ -473,6 +474,7 @@ const PredictBuyPreview = () => { return ( mockNetworks); -jest.mock('../../hooks/usePopularNetworks', () => ({ +jest.mock('../../hooks/usePopularNetworks/usePopularNetworks', () => ({ usePopularNetworks: () => mockUsePopularNetworks(), })); diff --git a/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.tsx b/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.tsx index d2cb9cece478..693f9e86b919 100644 --- a/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.tsx +++ b/app/components/UI/Trending/components/TrendingTokensBottomSheet/TrendingTokenNetworkBottomSheet.tsx @@ -19,7 +19,7 @@ import Avatar, { import { strings } from '../../../../../../locales/i18n'; import { ProcessedNetwork } from '../../../../hooks/useNetworksByNamespace/useNetworksByNamespace'; import { CaipChainId } from '@metamask/utils'; -import { usePopularNetworks } from '../../hooks/usePopularNetworks'; +import { usePopularNetworks } from '../../hooks/usePopularNetworks/usePopularNetworks'; export enum NetworkOption { AllNetworks = 'all', diff --git a/app/components/UI/Trending/hooks/usePopularNetworks/usePopularNetworks.test.ts b/app/components/UI/Trending/hooks/usePopularNetworks/usePopularNetworks.test.ts index f609370f6454..079d0e9d3dae 100644 --- a/app/components/UI/Trending/hooks/usePopularNetworks/usePopularNetworks.test.ts +++ b/app/components/UI/Trending/hooks/usePopularNetworks/usePopularNetworks.test.ts @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'; import { CaipChainId } from '@metamask/utils'; import { BtcScope, SolScope } from '@metamask/keyring-api'; import { isTestNet } from '../../../../../util/networks'; -import { usePopularNetworks } from '.'; +import { usePopularNetworks } from './usePopularNetworks'; jest.mock('react-redux', () => ({ useSelector: jest.fn(), diff --git a/app/components/UI/Trending/hooks/usePopularNetworks/index.ts b/app/components/UI/Trending/hooks/usePopularNetworks/usePopularNetworks.ts similarity index 100% rename from app/components/UI/Trending/hooks/usePopularNetworks/index.ts rename to app/components/UI/Trending/hooks/usePopularNetworks/usePopularNetworks.ts diff --git a/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.test.ts b/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.test.ts index 198999cb0d37..e480a112f640 100644 --- a/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.test.ts +++ b/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.test.ts @@ -1,6 +1,6 @@ -import { DEBOUNCE_WAIT, useSearchRequest } from '.'; +import { useSearchRequest } from './useSearchRequest'; import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; -import { act } from '@testing-library/react-native'; +import { act, waitFor } from '@testing-library/react-native'; import { CaipChainId } from '@metamask/utils'; // eslint-disable-next-line import/no-namespace import * as assetsControllers from '@metamask/assets-controllers'; @@ -8,7 +8,6 @@ import * as assetsControllers from '@metamask/assets-controllers'; describe('useSearchRequest', () => { beforeEach(() => { jest.clearAllMocks(); - jest.useFakeTimers(); }); it('returns search results when search succeeds', async () => { @@ -31,11 +30,10 @@ describe('useSearchRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); + await waitFor(() => { + expect(spySearchTokens).toHaveBeenCalledTimes(1); }); - expect(spySearchTokens).toHaveBeenCalledTimes(1); expect(result.current.results).toEqual(mockResults); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); @@ -57,32 +55,44 @@ describe('useSearchRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); + await waitFor(() => { + expect(result.current.error).toEqual(mockError); }); - expect(result.current.error).toEqual(mockError); expect(result.current.results).toEqual([]); expect(result.current.isLoading).toBe(false); - // Ensure all operations complete before cleanup - await act(async () => { - result.current.search.cancel(); - jest.runOnlyPendingTimers(); - jest.advanceTimersByTime(DEBOUNCE_WAIT); - // Flush all remaining promises - for (let i = 0; i < 20; i++) { - await Promise.resolve(); - } - }); - spySearchTokens.mockRestore(); unmount(); }); - it('coalesces multiple rapid calls into a single search', async () => { + it('handles stale results when multiple requests are triggered', async () => { const spySearchTokens = jest.spyOn(assetsControllers, 'searchTokens'); - spySearchTokens.mockResolvedValue({ data: [] } as never); + const mockResults1 = [ + { + assetId: 'eip155:1/erc20:0x123', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + }, + ]; + const mockResults2 = [ + { + assetId: 'eip155:1/erc20:0x456', + symbol: 'DAI', + name: 'Dai Stablecoin', + decimals: 18, + }, + ]; + + let resolveFirstRequest: ((value: unknown) => void) | undefined; + const firstRequestPromise = new Promise((resolve) => { + resolveFirstRequest = resolve; + }); + + spySearchTokens + .mockReturnValueOnce(firstRequestPromise as never) + .mockResolvedValueOnce({ data: mockResults2 } as never); const { result, unmount } = renderHookWithProvider(() => useSearchRequest({ @@ -92,31 +102,27 @@ describe('useSearchRequest', () => { }), ); + await waitFor(() => { + expect(result.current.isLoading).toBe(true); + }); + await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await result.current.search(); }); - spySearchTokens.mockClear(); + await waitFor(() => { + expect(result.current.results).toEqual(mockResults2); + }); await act(async () => { - result.current.search(); - result.current.search(); - result.current.search(); - - // Only test intermediate state if debounce wait is long enough - if (DEBOUNCE_WAIT > 100) { - jest.advanceTimersByTime(DEBOUNCE_WAIT - 100); - expect(spySearchTokens).not.toHaveBeenCalled(); - jest.advanceTimersByTime(200); - } else { - jest.advanceTimersByTime(DEBOUNCE_WAIT + 100); + if (resolveFirstRequest) { + resolveFirstRequest({ data: mockResults1 }); } - await Promise.resolve(); }); - expect(spySearchTokens).toHaveBeenCalledTimes(1); + expect(result.current.results).toEqual(mockResults2); + spySearchTokens.mockRestore(); unmount(); }); @@ -132,65 +138,72 @@ describe('useSearchRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); }); expect(spySearchTokens).not.toHaveBeenCalled(); expect(result.current.results).toEqual([]); - expect(result.current.isLoading).toBe(false); await act(async () => { await result.current.search(); - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); }); expect(spySearchTokens).not.toHaveBeenCalled(); + spySearchTokens.mockRestore(); unmount(); }); - it('maintains stable search function reference when chainIds array reference changes but values remain the same', async () => { + it('allows manual retry after error using search function', async () => { const spySearchTokens = jest.spyOn(assetsControllers, 'searchTokens'); - spySearchTokens.mockResolvedValue({ data: [] } as never); + const mockError = new Error('Failed to search tokens'); + const mockResults = [ + { + assetId: 'eip155:1/erc20:0x123', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + }, + ]; - let chainIds: CaipChainId[] = ['eip155:1', 'eip155:10']; - const { result, rerender, unmount } = renderHookWithProvider(() => + spySearchTokens.mockRejectedValueOnce(mockError); + + const { result, unmount } = renderHookWithProvider(() => useSearchRequest({ - chainIds, + chainIds: ['eip155:1'], query: 'ETH', limit: 10, }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(result.current.error).toEqual(mockError); }); - const firstSearchFunction = result.current.search; - - chainIds = ['eip155:1', 'eip155:10']; - rerender(undefined); + spySearchTokens.mockResolvedValue({ data: mockResults } as never); await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await result.current.search(); + }); + + await waitFor(() => { + expect(result.current.error).toBe(null); }); - expect(result.current.search).toBe(firstSearchFunction); + expect(result.current.results).toEqual(mockResults); + expect(result.current.isLoading).toBe(false); + spySearchTokens.mockRestore(); unmount(); }); - it('creates new search function when chainIds values change', async () => { + it('triggers new search when chainIds values change', async () => { const spySearchTokens = jest.spyOn(assetsControllers, 'searchTokens'); spySearchTokens.mockResolvedValue({ data: [] } as never); let chainIds: CaipChainId[] = ['eip155:1', 'eip155:10']; - const { result, rerender, unmount } = renderHookWithProvider(() => + const { rerender, unmount } = renderHookWithProvider(() => useSearchRequest({ chainIds, query: 'ETH', @@ -198,22 +211,18 @@ describe('useSearchRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(spySearchTokens).toHaveBeenCalledTimes(1); }); - const firstSearchFunction = result.current.search; - chainIds = ['eip155:1', 'eip155:137']; rerender(undefined); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(spySearchTokens).toHaveBeenCalledTimes(2); }); - expect(result.current.search).not.toBe(firstSearchFunction); + spySearchTokens.mockRestore(); unmount(); }); }); diff --git a/app/components/UI/Trending/hooks/useSearchRequest/index.ts b/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.ts similarity index 50% rename from app/components/UI/Trending/hooks/useSearchRequest/index.ts rename to app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.ts index 3b1e0217fa51..56bdc3845b5b 100644 --- a/app/components/UI/Trending/hooks/useSearchRequest/index.ts +++ b/app/components/UI/Trending/hooks/useSearchRequest/useSearchRequest.ts @@ -1,9 +1,7 @@ -import { useCallback, useMemo, useEffect, useState, useRef } from 'react'; -import { debounce } from 'lodash'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { CaipChainId } from '@metamask/utils'; import { searchTokens } from '@metamask/assets-controllers'; import { useStableArray } from '../../../Perps/hooks/useStableArray'; -export const DEBOUNCE_WAIT = 0; /** * Hook for handling search tokens request @@ -15,33 +13,19 @@ export const useSearchRequest = (options: { limit: number; }) => { const { chainIds, query, limit } = options; - const [results, setResults] = useState - > | null>(null); + const [results, setResults] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Track the current request ID to prevent stale results from overwriting current ones const requestIdRef = useRef(0); - // Stabilize the chainIds array reference to prevent unnecessary re-memoization + // Stabilize the chainIds array reference to prevent unnecessary re-fetching const stableChainIds = useStableArray(chainIds); - // Memoize the options object to ensure stable reference - const memoizedOptions = useMemo( - () => ({ - chainIds: stableChainIds, - query, - limit, - }), - [stableChainIds, query, limit], - ); - const searchTokensRequest = useCallback(async () => { - if (!memoizedOptions.query) { - // Increment request ID to invalidate any pending requests - ++requestIdRef.current; - setResults(null); + if (!query) { + setResults([]); setIsLoading(false); return; } @@ -52,22 +36,18 @@ export const useSearchRequest = (options: { setError(null); try { - const searchResults = await searchTokens( - memoizedOptions.chainIds, - memoizedOptions.query, - { - limit: memoizedOptions.limit, - }, - ); + const searchResults = await searchTokens(stableChainIds, query, { + limit, + }); // Only update state if this is still the current request if (currentRequestId === requestIdRef.current) { - setResults(searchResults || null); + setResults(searchResults?.data || []); } } catch (err) { // Only update state if this is still the current request if (currentRequestId === requestIdRef.current) { setError(err as Error); - setResults(null); + setResults([]); } } finally { // Only update loading state if this is still the current request @@ -75,40 +55,17 @@ export const useSearchRequest = (options: { setIsLoading(false); } } - }, [memoizedOptions]); - - const debouncedSearchTokensRequest = useMemo( - () => debounce(searchTokensRequest, DEBOUNCE_WAIT), - [searchTokensRequest], - ); + }, [stableChainIds, query, limit]); // Automatically trigger search when query changes - // Cancel previous debounced function BEFORE triggering new one to prevent race conditions useEffect(() => { - // Cancel any pending debounced calls from previous render - debouncedSearchTokensRequest.cancel(); - - setIsLoading(true); - - // If query is empty, don't trigger search - if (!memoizedOptions.query) { - setIsLoading(false); - return; - } - - // Trigger new search - debouncedSearchTokensRequest(); - - // Cleanup: cancel on unmount or when dependencies change - return () => { - debouncedSearchTokensRequest.cancel(); - }; - }, [debouncedSearchTokensRequest, memoizedOptions.query]); + searchTokensRequest(); + }, [searchTokensRequest]); return { - results: results?.data || [], + results, isLoading, error, - search: debouncedSearchTokensRequest, + search: searchTokensRequest, }; }; diff --git a/app/components/UI/Trending/hooks/useTrendingRequest/useTrendingRequest.test.ts b/app/components/UI/Trending/hooks/useTrendingRequest/useTrendingRequest.test.ts index 1a0974a43aa9..1cc7fd9dc011 100644 --- a/app/components/UI/Trending/hooks/useTrendingRequest/useTrendingRequest.test.ts +++ b/app/components/UI/Trending/hooks/useTrendingRequest/useTrendingRequest.test.ts @@ -1,6 +1,6 @@ -import { DEBOUNCE_WAIT, useTrendingRequest } from '.'; +import { useTrendingRequest } from './useTrendingRequest'; import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; -import { act } from '@testing-library/react-native'; +import { act, waitFor } from '@testing-library/react-native'; // eslint-disable-next-line import/no-namespace import * as assetsControllers from '@metamask/assets-controllers'; import { @@ -52,7 +52,6 @@ const mockDefaultNetworks: ProcessedNetwork[] = [ describe('useTrendingRequest', () => { beforeEach(() => { jest.clearAllMocks(); - jest.useFakeTimers(); // Set up default mocks for network hooks mockUseNetworksByNamespace.mockReturnValue({ networks: mockDefaultNetworks, @@ -75,35 +74,6 @@ describe('useTrendingRequest', () => { } as unknown as ReturnType); }); - afterEach(() => { - jest.useRealTimers(); - }); - - it('returns an object with results, isLoading, error, and fetch function', () => { - const spyGetTrendingTokens = jest.spyOn( - assetsControllers, - 'getTrendingTokens', - ); - spyGetTrendingTokens.mockResolvedValue([]); - - const { result, unmount } = renderHookWithProvider(() => - useTrendingRequest({ - chainIds: ['eip155:1'], - }), - ); - - expect(result.current).toHaveProperty('results'); - expect(result.current).toHaveProperty('isLoading'); - expect(result.current).toHaveProperty('error'); - expect(result.current).toHaveProperty('fetch'); - expect(typeof result.current.fetch).toBe('function'); - expect(Array.isArray(result.current.results)).toBe(true); - expect(typeof result.current.isLoading).toBe('boolean'); - - spyGetTrendingTokens.mockRestore(); - unmount(); - }); - it('returns trending tokens results when fetch succeeds', async () => { const spyGetTrendingTokens = jest.spyOn( assetsControllers, @@ -137,12 +107,10 @@ describe('useTrendingRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(spyGetTrendingTokens).toHaveBeenCalledTimes(1); }); - expect(spyGetTrendingTokens).toHaveBeenCalledTimes(1); expect(result.current.results).toEqual(mockResults); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe(null); @@ -168,21 +136,19 @@ describe('useTrendingRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(result.current.isLoading).toBe(true); }); - expect(result.current.isLoading).toBe(true); - await act(async () => { if (resolvePromise) { resolvePromise([]); } - await Promise.resolve(); }); - expect(result.current.isLoading).toBe(false); + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); spyGetTrendingTokens.mockRestore(); unmount(); @@ -202,12 +168,10 @@ describe('useTrendingRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(result.current.error).toEqual(mockError); }); - expect(result.current.error).toEqual(mockError); expect(result.current.results).toEqual([]); expect(result.current.isLoading).toBe(false); @@ -241,70 +205,20 @@ describe('useTrendingRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(result.current.error).toEqual(mockError); }); - expect(result.current.error).toEqual(mockError); - spyGetTrendingTokens.mockResolvedValue(mockResults as never); await act(async () => { - result.current.fetch(); - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await result.current.fetch(); }); - expect(result.current.error).toBe(null); - expect(result.current.results).toEqual(mockResults); - expect(result.current.isLoading).toBe(false); - - spyGetTrendingTokens.mockRestore(); - unmount(); - }); - - it('uses default popular networks when chainIds is empty', async () => { - const spyGetTrendingTokens = jest.spyOn( - assetsControllers, - 'getTrendingTokens', - ); - const mockResults: assetsControllers.TrendingAsset[] = [ - { - assetId: 'eip155:1/erc20:0x123', - symbol: 'TOKEN1', - name: 'Token 1', - decimals: 18, - price: '1', - aggregatedUsdVolume: 1, - marketCap: 1, - }, - ]; - spyGetTrendingTokens.mockResolvedValue(mockResults as never); - - const { result, unmount } = renderHookWithProvider(() => - useTrendingRequest({ - chainIds: [], - }), - ); - - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(result.current.error).toBe(null); }); - expect(mockUseNetworksByNamespace).toHaveBeenCalledWith({ - networkType: 'popular', - }); - expect(mockUseNetworksToUse).toHaveBeenCalledWith({ - networks: mockDefaultNetworks, - networkType: 'popular', - }); - expect(spyGetTrendingTokens).toHaveBeenCalledWith( - expect.objectContaining({ - chainIds: ['eip155:1', 'eip155:137'], - }), - ); expect(result.current.results).toEqual(mockResults); expect(result.current.isLoading).toBe(false); @@ -312,32 +226,55 @@ describe('useTrendingRequest', () => { unmount(); }); - it('uses default popular networks when chainIds is not provided', async () => { - const spyGetTrendingTokens = jest.spyOn( - assetsControllers, - 'getTrendingTokens', - ); - const mockResults: assetsControllers.TrendingAsset[] = []; - spyGetTrendingTokens.mockResolvedValue(mockResults as never); - - renderHookWithProvider(() => useTrendingRequest({})); - - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); - }); - - expect(mockUseNetworksByNamespace).toHaveBeenCalledWith({ - networkType: 'popular', - }); - expect(spyGetTrendingTokens).toHaveBeenCalledWith( - expect.objectContaining({ - chainIds: ['eip155:1', 'eip155:137'], - }), - ); - - spyGetTrendingTokens.mockRestore(); - }); + it.each([ + { description: 'empty array', options: { chainIds: [] } }, + { description: 'not provided', options: {} }, + ])( + 'uses default popular networks when chainIds is $description', + async ({ options }) => { + const spyGetTrendingTokens = jest.spyOn( + assetsControllers, + 'getTrendingTokens', + ); + const mockResults: assetsControllers.TrendingAsset[] = [ + { + assetId: 'eip155:1/erc20:0x123', + symbol: 'TOKEN1', + name: 'Token 1', + decimals: 18, + price: '1', + aggregatedUsdVolume: 1, + marketCap: 1, + }, + ]; + spyGetTrendingTokens.mockResolvedValue(mockResults as never); + + const { result } = renderHookWithProvider(() => + useTrendingRequest(options), + ); + + await waitFor(() => { + expect(spyGetTrendingTokens).toHaveBeenCalledTimes(1); + }); + + expect(mockUseNetworksByNamespace).toHaveBeenCalledWith({ + networkType: 'popular', + }); + expect(mockUseNetworksToUse).toHaveBeenCalledWith({ + networks: mockDefaultNetworks, + networkType: 'popular', + }); + expect(spyGetTrendingTokens).toHaveBeenCalledWith( + expect.objectContaining({ + chainIds: ['eip155:1', 'eip155:137'], + }), + ); + expect(result.current.results).toEqual(mockResults); + expect(result.current.isLoading).toBe(false); + + spyGetTrendingTokens.mockRestore(); + }, + ); it('uses provided chainIds when available instead of default networks', async () => { const spyGetTrendingTokens = jest.spyOn( @@ -357,9 +294,8 @@ describe('useTrendingRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(spyGetTrendingTokens).toHaveBeenCalledTimes(1); }); expect(spyGetTrendingTokens).toHaveBeenCalledWith( @@ -371,12 +307,42 @@ describe('useTrendingRequest', () => { spyGetTrendingTokens.mockRestore(); }); - it('coalesces multiple rapid calls into a single fetch', async () => { + it('handles stale results when multiple requests are triggered', async () => { const spyGetTrendingTokens = jest.spyOn( assetsControllers, 'getTrendingTokens', ); - spyGetTrendingTokens.mockResolvedValue([]); + const mockResults1: assetsControllers.TrendingAsset[] = [ + { + assetId: 'eip155:1/erc20:0x123', + symbol: 'TOKEN1', + name: 'Token 1', + decimals: 18, + price: '1', + aggregatedUsdVolume: 1, + marketCap: 1, + }, + ]; + const mockResults2: assetsControllers.TrendingAsset[] = [ + { + assetId: 'eip155:1/erc20:0x456', + symbol: 'TOKEN2', + name: 'Token 2', + decimals: 18, + price: '2', + aggregatedUsdVolume: 2, + marketCap: 2, + }, + ]; + + let resolveFirstRequest: ((value: unknown[]) => void) | undefined; + const firstRequestPromise = new Promise((resolve) => { + resolveFirstRequest = resolve; + }); + + spyGetTrendingTokens + .mockReturnValueOnce(firstRequestPromise as never) + .mockResolvedValueOnce(mockResults2 as never); const { result, unmount } = renderHookWithProvider(() => useTrendingRequest({ @@ -384,26 +350,25 @@ describe('useTrendingRequest', () => { }), ); - await act(async () => { - jest.advanceTimersByTime(DEBOUNCE_WAIT); - await Promise.resolve(); + await waitFor(() => { + expect(result.current.isLoading).toBe(true); }); - spyGetTrendingTokens.mockClear(); - await act(async () => { - result.current.fetch(); - result.current.fetch(); - result.current.fetch(); + await result.current.fetch(); + }); - jest.advanceTimersByTime(DEBOUNCE_WAIT - 100); - expect(spyGetTrendingTokens).not.toHaveBeenCalled(); + await waitFor(() => { + expect(result.current.results).toEqual(mockResults2); + }); - jest.advanceTimersByTime(DEBOUNCE_WAIT + 200); - await Promise.resolve(); + await act(async () => { + if (resolveFirstRequest) { + resolveFirstRequest(mockResults1); + } }); - expect(spyGetTrendingTokens).toHaveBeenCalledTimes(1); + expect(result.current.results).toEqual(mockResults2); spyGetTrendingTokens.mockRestore(); unmount(); diff --git a/app/components/UI/Trending/hooks/useTrendingRequest/index.ts b/app/components/UI/Trending/hooks/useTrendingRequest/useTrendingRequest.ts similarity index 60% rename from app/components/UI/Trending/hooks/useTrendingRequest/index.ts rename to app/components/UI/Trending/hooks/useTrendingRequest/useTrendingRequest.ts index da61d8ed85a4..4e720cfb3be6 100644 --- a/app/components/UI/Trending/hooks/useTrendingRequest/index.ts +++ b/app/components/UI/Trending/hooks/useTrendingRequest/useTrendingRequest.ts @@ -1,5 +1,4 @@ import { useCallback, useMemo, useEffect, useState, useRef } from 'react'; -import { debounce } from 'lodash'; import type { CaipChainId } from '@metamask/utils'; import { getTrendingTokens, @@ -13,8 +12,6 @@ import { } from '../../../../hooks/useNetworksByNamespace/useNetworksByNamespace'; import { useNetworksToUse } from '../../../../hooks/useNetworksToUse/useNetworksToUse'; -export const DEBOUNCE_WAIT = 500; - /** * Hook for handling trending tokens request * @returns {Object} An object containing the trending tokens results, loading state, error, and a function to trigger fetch @@ -61,43 +58,20 @@ export const useTrendingRequest = (options: { // Track the current request ID to prevent stale results from overwriting current ones const requestIdRef = useRef(0); - // Stabilize the chainIds array reference to prevent unnecessary re-memoization + // Stabilize the chainIds array reference to prevent unnecessary re-fetching const stableChainIds = useStableArray(chainIds); - // Memoize the options object to ensure stable reference - const memoizedOptions = useMemo( - () => ({ - chainIds: stableChainIds, - sortBy, - minLiquidity, - minVolume24hUsd, - maxVolume24hUsd, - minMarketCap, - maxMarketCap, - }), - [ - stableChainIds, - sortBy, - minLiquidity, - minVolume24hUsd, - maxVolume24hUsd, - minMarketCap, - maxMarketCap, - ], - ); - - const [results, setResults] = useState - > | null>(null); + const [results, setResults] = useState< + Awaited> + >([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const fetchTrendingTokens = useCallback(async () => { - if (!memoizedOptions.chainIds.length) { - ++requestIdRef.current; - setResults(null); + if (!stableChainIds.length) { + setResults([]); setIsLoading(false); return; } @@ -109,13 +83,13 @@ export const useTrendingRequest = (options: { try { const resultsToStore = await getTrendingTokens({ - chainIds: memoizedOptions.chainIds, - sortBy: memoizedOptions.sortBy, - minLiquidity: memoizedOptions.minLiquidity, - minVolume24hUsd: memoizedOptions.minVolume24hUsd, - maxVolume24hUsd: memoizedOptions.maxVolume24hUsd, - minMarketCap: memoizedOptions.minMarketCap, - maxMarketCap: memoizedOptions.maxMarketCap, + chainIds: stableChainIds, + sortBy, + minLiquidity, + minVolume24hUsd, + maxVolume24hUsd, + minMarketCap, + maxMarketCap, }); // Only update state if this is still the current request if (currentRequestId === requestIdRef.current) { @@ -125,7 +99,7 @@ export const useTrendingRequest = (options: { // Only update state if this is still the current request if (currentRequestId === requestIdRef.current) { setError(err as Error); - setResults(null); + setResults([]); } } finally { // Only update loading state if this is still the current request @@ -133,42 +107,25 @@ export const useTrendingRequest = (options: { setIsLoading(false); } } - }, [memoizedOptions]); - - const debouncedFetchTrendingTokens = useMemo( - () => debounce(fetchTrendingTokens, DEBOUNCE_WAIT), - [fetchTrendingTokens], - ); + }, [ + stableChainIds, + sortBy, + minLiquidity, + minVolume24hUsd, + maxVolume24hUsd, + minMarketCap, + maxMarketCap, + ]); // Automatically trigger fetch when options change - // Cancel previous debounced function BEFORE triggering new one to prevent race conditions useEffect(() => { - // Cancel any pending debounced calls from previous render - debouncedFetchTrendingTokens.cancel(); - - // If chainIds is empty, don't trigger fetch - if (!stableChainIds.length) { - setResults(null); - setIsLoading(false); - return; - } - - // Immediately show loading state so UI can render skeleton right away - setIsLoading(true); - - // Fetch new data - debouncedFetchTrendingTokens(); - - // Cleanup: cancel on unmount or when dependencies change - return () => { - debouncedFetchTrendingTokens.cancel(); - }; - }, [debouncedFetchTrendingTokens, stableChainIds, memoizedOptions]); + fetchTrendingTokens(); + }, [fetchTrendingTokens]); return { - results: results || [], + results, isLoading, error, - fetch: debouncedFetchTrendingTokens, + fetch: fetchTrendingTokens, }; }; diff --git a/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.test.ts b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.test.ts new file mode 100644 index 000000000000..8cb4b31f938a --- /dev/null +++ b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.test.ts @@ -0,0 +1,171 @@ +import { useTrendingSearch } from './useTrendingSearch'; +import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; +import { waitFor } from '@testing-library/react-native'; +import type { TrendingAsset } from '@metamask/assets-controllers'; +import { useSearchRequest } from '../useSearchRequest/useSearchRequest'; +import { useTrendingRequest } from '../useTrendingRequest/useTrendingRequest'; +import { sortTrendingTokens } from '../../utils/sortTrendingTokens'; + +// Mock dependencies +jest.mock('../useSearchRequest/useSearchRequest'); +jest.mock('../useTrendingRequest/useTrendingRequest'); +jest.mock('../../utils/sortTrendingTokens'); + +const mockUseSearchRequest = useSearchRequest as jest.MockedFunction< + typeof useSearchRequest +>; +const mockUseTrendingRequest = useTrendingRequest as jest.MockedFunction< + typeof useTrendingRequest +>; +const mockSortTrendingTokens = sortTrendingTokens as jest.MockedFunction< + typeof sortTrendingTokens +>; + +describe('useTrendingSearch', () => { + const mockTrendingResults: TrendingAsset[] = [ + { + assetId: 'eip155:1/erc20:0x123', + symbol: 'ETH', + name: 'Ethereum', + decimals: 18, + price: '2000', + aggregatedUsdVolume: 1000000, + marketCap: 500000000, + }, + { + assetId: 'eip155:1/erc20:0x456', + symbol: 'DAI', + name: 'Dai Stablecoin', + decimals: 18, + price: '1', + aggregatedUsdVolume: 500000, + marketCap: 100000000, + }, + ]; + + const mockSearchResults = [ + { + assetId: 'eip155:1/erc20:0x789', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + price: '1', + aggregatedUsdVolume: 800000, + marketCap: 300000000, + }, + ]; + + const mockFetchTrendingTokens = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + // Default mock implementations + mockUseSearchRequest.mockReturnValue({ + results: [], + isLoading: false, + error: null, + search: jest.fn(), + }); + + mockUseTrendingRequest.mockReturnValue({ + results: mockTrendingResults, + isLoading: false, + error: null, + fetch: mockFetchTrendingTokens, + }); + + mockSortTrendingTokens.mockImplementation((tokens) => tokens); + }); + + it('returns sorted trending results when no search query provided', async () => { + const sortedResults = [mockTrendingResults[1], mockTrendingResults[0]]; + mockSortTrendingTokens.mockReturnValue(sortedResults); + + const { result } = renderHookWithProvider(() => useTrendingSearch()); + + await waitFor(() => { + expect(result.current.data).toEqual(sortedResults); + }); + + expect(mockSortTrendingTokens).toHaveBeenCalledWith( + mockTrendingResults, + expect.any(String), + ); + expect(result.current.isLoading).toBe(false); + }); + + it('returns combined search and trending results when search query provided', async () => { + mockUseSearchRequest.mockReturnValue({ + results: mockSearchResults, + isLoading: false, + error: null, + search: jest.fn(), + }); + + const { result } = renderHookWithProvider(() => + useTrendingSearch('USDC', 'h24_trending'), + ); + + await waitFor(() => { + expect(result.current.data).toHaveLength(3); + }); + + expect(result.current.data).toEqual( + expect.arrayContaining([ + ...mockTrendingResults, + expect.objectContaining({ symbol: 'USDC' }), + ]), + ); + }); + + it('removes duplicate results when combining search and trending', async () => { + const duplicateResult = mockTrendingResults[0]; + mockUseSearchRequest.mockReturnValue({ + results: [duplicateResult, mockSearchResults[0]], + isLoading: false, + error: null, + search: jest.fn(), + }); + + const { result } = renderHookWithProvider(() => + useTrendingSearch('ETH', 'h24_trending'), + ); + + await waitFor(() => { + expect(result.current.data).toHaveLength(3); + }); + + const assetIds = result.current.data.map((item) => item.assetId); + const uniqueAssetIds = new Set(assetIds); + expect(assetIds.length).toBe(uniqueAssetIds.size); + }); + + it('returns trending loading state when no search query', () => { + mockUseTrendingRequest.mockReturnValue({ + results: [], + isLoading: true, + error: null, + fetch: mockFetchTrendingTokens, + }); + + const { result } = renderHookWithProvider(() => useTrendingSearch()); + + expect(result.current.isLoading).toBe(true); + }); + + it('returns search loading state when search query provided', () => { + mockUseSearchRequest.mockReturnValue({ + results: [], + isLoading: true, + error: null, + search: jest.fn(), + }); + + const { result } = renderHookWithProvider(() => + useTrendingSearch('ETH', 'h24_trending'), + ); + + expect(result.current.isLoading).toBe(true); + }); +}); diff --git a/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts new file mode 100644 index 000000000000..e2a9ed53d5cd --- /dev/null +++ b/app/components/UI/Trending/hooks/useTrendingSearch/useTrendingSearch.ts @@ -0,0 +1,62 @@ +import { useMemo } from 'react'; +import type { CaipChainId } from '@metamask/utils'; +import { SortTrendingBy, TrendingAsset } from '@metamask/assets-controllers'; +import { useSearchRequest } from '../useSearchRequest/useSearchRequest'; +import { useTrendingRequest } from '../useTrendingRequest/useTrendingRequest'; +import { sortTrendingTokens } from '../../utils/sortTrendingTokens'; +import { PriceChangeOption } from '../../components/TrendingTokensBottomSheet'; + +/** + * Hook for handling trending tokens search that returns trending tokens and tokens from search API + * @returns {Object} An object containing the trending tokens results, token search results, loading state, error, and a function to trigger fetch + */ +export const useTrendingSearch = ( + searchQuery?: string, + sortBy?: SortTrendingBy, + chainIds?: CaipChainId[] | null, +) => { + // Trending will return tokens that have just been created which wont be picked up by search API + // so if you see a token on trending and search on omnisearch which uses the search endpoint... + // There is a chance you will get 0 results + const { results: searchResults, isLoading: isSearchLoading } = + useSearchRequest({ + query: searchQuery || '', + limit: 20, + chainIds: [], + }); + + const { + results: trendingResults, + isLoading: isTrendingLoading, + fetch: fetchTrendingTokens, + } = useTrendingRequest({ + sortBy, + chainIds: chainIds ?? undefined, + }); + + const data = useMemo(() => { + if (!searchQuery) { + return sortTrendingTokens(trendingResults, PriceChangeOption.PriceChange); + } + + // Combine trending and search results, avoiding duplicates + const resultMap = new Map( + trendingResults.map((result) => [result.assetId, result]), + ); + + searchResults.forEach((result) => { + const asset = result as TrendingAsset; + if (!resultMap.has(asset.assetId)) { + resultMap.set(asset.assetId, asset); + } + }); + + return Array.from(resultMap.values()); + }, [searchQuery, trendingResults, searchResults]); + + return { + data, + isLoading: searchQuery ? isSearchLoading : isTrendingLoading, + refetch: fetchTrendingTokens, + }; +}; diff --git a/app/components/Views/Asset/__snapshots__/index.test.js.snap b/app/components/Views/Asset/__snapshots__/index.test.js.snap index 7c3dfce5c3e7..af3d82419ea3 100644 --- a/app/components/Views/Asset/__snapshots__/index.test.js.snap +++ b/app/components/Views/Asset/__snapshots__/index.test.js.snap @@ -1992,7 +1992,7 @@ exports[`Asset Multichain Functionality should exclude mixed token/SOL transacti } } > - Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy. + Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy. @@ -3766,7 +3766,7 @@ exports[`Asset Multichain Functionality should exclude transactions with empty a } } > - Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy. + Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy. @@ -5581,7 +5581,7 @@ exports[`Asset Multichain Functionality should filter SPL token transactions cor } } > - Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy. + Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy. @@ -7355,7 +7355,7 @@ exports[`Asset Multichain Functionality should filter native SOL transactions co } } > - Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy. + Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy. @@ -9129,7 +9129,7 @@ exports[`Asset Multichain Functionality should handle state with no multichain t } } > - Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy. + Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy. @@ -10944,7 +10944,7 @@ exports[`Asset Multichain Functionality should handle unknown SPL token filterin } } > - Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy. + Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy. @@ -13248,7 +13248,7 @@ exports[`Asset Multichain Functionality should render non-EVM assets with Multic } } > - Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy. + Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy. @@ -15022,7 +15022,7 @@ exports[`Asset Multichain Functionality should sort filtered transactions by tim } } > - Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy. + Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy. diff --git a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.test.tsx b/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.test.tsx index 008b80dec839..24757f1c29df 100644 --- a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.test.tsx +++ b/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.test.tsx @@ -22,19 +22,30 @@ const mockUseTrendingRequest = jest.fn().mockReturnValue({ error: null, fetch: mockFetchTrendingTokens, }); -jest.mock('../../../UI/Trending/hooks/useTrendingRequest', () => ({ - useTrendingRequest: (options: unknown) => mockUseTrendingRequest(options), -})); +jest.mock( + '../../../UI/Trending/hooks/useTrendingRequest/useTrendingRequest', + () => ({ + useTrendingRequest: (options: unknown) => mockUseTrendingRequest(options), + }), +); -const mockUseSectionData = jest.fn(); +const mockUseTrendingSearch = jest.fn(); + +jest.mock( + '../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch', + () => ({ + useTrendingSearch: ( + searchQuery?: string, + sortBy?: unknown, + chainIds?: unknown, + ) => mockUseTrendingSearch({ searchQuery, sortBy, chainIds }), + }), +); // Mock sections.config to avoid complex Perps dependencies -// Make useSectionData return the same data as useTrendingRequest jest.mock('../../TrendingView/config/sections.config', () => ({ SECTIONS_CONFIG: { tokens: { - useSectionData: (params?: { searchQuery?: string }) => - mockUseSectionData(params), getSearchableText: (item: { name?: string; symbol?: string }) => `${item.name || ''} ${item.symbol || ''}`.toLowerCase(), }, @@ -216,7 +227,7 @@ describe('TrendingTokensFullView', () => { error: null, fetch: jest.fn(), }); - mockUseSectionData.mockReturnValue({ + mockUseTrendingSearch.mockReturnValue({ data: [], isLoading: false, refetch: jest.fn(), @@ -352,7 +363,7 @@ describe('TrendingTokensFullView', () => { fetch: jest.fn(), }); - mockUseSectionData.mockReturnValue({ + mockUseTrendingSearch.mockReturnValue({ data: mockTokens, isLoading: false, refetch: jest.fn(), @@ -369,10 +380,10 @@ describe('TrendingTokensFullView', () => { expect(getByText('Token 2')).toBeOnTheScreen(); }); - it('calls useSectionData with correct initial parameters', () => { + it('calls useTrendingSearch with correct initial parameters', () => { renderWithProvider(, { state: mockState }, false); - expect(mockUseSectionData).toHaveBeenCalledWith({ + expect(mockUseTrendingSearch).toHaveBeenCalledWith({ sortBy: undefined, chainIds: null, searchQuery: undefined, @@ -395,7 +406,7 @@ describe('TrendingTokensFullView', () => { }); await waitFor(() => { - expect(mockUseSectionData).toHaveBeenLastCalledWith({ + expect(mockUseTrendingSearch).toHaveBeenLastCalledWith({ sortBy: 'h6_trending', chainIds: null, searchQuery: undefined, @@ -419,7 +430,7 @@ describe('TrendingTokensFullView', () => { }); await waitFor(() => { - expect(mockUseSectionData).toHaveBeenLastCalledWith({ + expect(mockUseTrendingSearch).toHaveBeenLastCalledWith({ sortBy: undefined, chainIds: ['eip155:1'], searchQuery: undefined, @@ -440,7 +451,7 @@ describe('TrendingTokensFullView', () => { fetch: jest.fn(), }); - mockUseSectionData.mockReturnValue({ + mockUseTrendingSearch.mockReturnValue({ data: mockTokens, isLoading: false, refetch: jest.fn(), @@ -475,14 +486,14 @@ describe('TrendingTokensFullView', () => { }), ]; - mockUseTrendingRequest.mockReturnValueOnce({ + mockUseTrendingRequest.mockReturnValue({ results: mockTokens, isLoading: false, error: null, fetch: mockFetchTrendingTokens, }); - mockUseSectionData.mockReturnValue({ + mockUseTrendingSearch.mockReturnValue({ data: mockTokens, isLoading: false, refetch: mockFetchTrendingTokens, diff --git a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx b/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx index 81a520162845..2ea31d167afb 100644 --- a/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx +++ b/app/components/Views/TrendingTokens/TrendingTokensFullView/TrendingTokensFullView.tsx @@ -41,6 +41,7 @@ import { } from '../../../UI/Trending/components/TrendingTokensBottomSheet'; import { sortTrendingTokens } from '../../../UI/Trending/utils/sortTrendingTokens'; import { SECTIONS_CONFIG } from '../../TrendingView/config/sections.config'; +import { useTrendingSearch } from '../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch'; interface TrendingTokensNavigationParamList { [key: string]: undefined | object; @@ -201,11 +202,7 @@ const TrendingTokensFullView = () => { data: tokensSectionData, isLoading, refetch: refetchTokensSection, - } = SECTIONS_CONFIG.tokens.useSectionData({ - searchQuery: searchQuery || undefined, - sortBy, - chainIds: selectedNetwork, - }); + } = useTrendingSearch(searchQuery || undefined, sortBy, selectedNetwork); const searchResults = useMemo(() => { // When search is not active, use the full section data diff --git a/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.test.ts b/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.test.ts index 7aba9bc80de8..7afd60bad5b0 100644 --- a/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.test.ts +++ b/app/components/Views/TrendingView/ExploreSearchScreen/components/ExploreSearchResults/config/useExploreSearch.test.ts @@ -24,18 +24,39 @@ const mockPredictionMarkets = [ { id: '4', title: 'Trump election results' }, ]; -const mockUseTrendingRequest = jest.fn(); -const mockUseSearchRequest = jest.fn(); +const mockSites = [ + { + id: '1', + name: 'Uniswap', + url: 'https://uniswap.org', + displayUrl: 'uniswap.org', + }, + { + id: '2', + name: 'OpenSea', + url: 'https://opensea.io', + displayUrl: 'opensea.io', + }, + { id: '3', name: 'Aave', url: 'https://aave.com', displayUrl: 'aave.com' }, + { + id: '4', + name: 'Compound', + url: 'https://compound.finance', + displayUrl: 'compound.finance', + }, +]; + +const mockUseTrendingSearch = jest.fn(); const mockUsePerpsMarkets = jest.fn(); const mockUsePredictMarketData = jest.fn(); +const mockUseSitesData = jest.fn(); -jest.mock('../../../../../../UI/Trending/hooks/useTrendingRequest', () => ({ - useTrendingRequest: () => mockUseTrendingRequest(), -})); - -jest.mock('../../../../../../UI/Trending/hooks/useSearchRequest', () => ({ - useSearchRequest: () => mockUseSearchRequest(), -})); +jest.mock( + '../../../../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch', + () => ({ + useTrendingSearch: () => mockUseTrendingSearch(), + }), +); jest.mock('../../../../../../UI/Perps/hooks/usePerpsMarkets', () => ({ usePerpsMarkets: () => mockUsePerpsMarkets(), @@ -45,29 +66,38 @@ jest.mock('../../../../../../UI/Predict/hooks/usePredictMarketData', () => ({ usePredictMarketData: () => mockUsePredictMarketData(), })); +jest.mock('../../../../SectionSites/hooks/useSitesData', () => ({ + useSitesData: () => mockUseSitesData(), +})); + describe('useExploreSearch', () => { beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); - mockUseTrendingRequest.mockReturnValue({ - results: mockTrendingTokens, - isLoading: false, - }); - - mockUseSearchRequest.mockReturnValue({ - results: mockTrendingTokens, + mockUseTrendingSearch.mockReturnValue({ + data: mockTrendingTokens, isLoading: false, + refetch: jest.fn(), }); mockUsePerpsMarkets.mockReturnValue({ markets: mockPerpsMarkets, isLoading: false, + refresh: jest.fn(), + isRefreshing: false, }); mockUsePredictMarketData.mockReturnValue({ marketData: mockPredictionMarkets, isFetching: false, + refetch: jest.fn(), + }); + + mockUseSitesData.mockReturnValue({ + sites: mockSites, + isLoading: false, + refetch: jest.fn(), }); }); @@ -82,6 +112,7 @@ describe('useExploreSearch', () => { expect(result.current.data.tokens).toHaveLength(3); expect(result.current.data.perps).toHaveLength(3); expect(result.current.data.predictions).toHaveLength(3); + expect(result.current.data.sites).toHaveLength(3); }); it('returns top 3 items when query contains only whitespace', () => { @@ -90,6 +121,7 @@ describe('useExploreSearch', () => { expect(result.current.data.tokens).toHaveLength(3); expect(result.current.data.perps).toHaveLength(3); expect(result.current.data.predictions).toHaveLength(3); + expect(result.current.data.sites).toHaveLength(3); }); it('filters tokens by symbol when query matches', async () => { @@ -208,6 +240,7 @@ describe('useExploreSearch', () => { expect(result.current.data.tokens).toHaveLength(0); expect(result.current.data.perps).toHaveLength(0); expect(result.current.data.predictions).toHaveLength(0); + expect(result.current.data.sites).toHaveLength(0); }); }); @@ -237,19 +270,29 @@ describe('useExploreSearch', () => { }); it('returns loading states for each section', () => { - mockUseTrendingRequest.mockReturnValue({ - results: [], + mockUseTrendingSearch.mockReturnValue({ + data: [], isLoading: true, + refetch: jest.fn(), }); mockUsePerpsMarkets.mockReturnValue({ markets: [], isLoading: true, + refresh: jest.fn(), + isRefreshing: false, }); mockUsePredictMarketData.mockReturnValue({ marketData: [], isFetching: true, + refetch: jest.fn(), + }); + + mockUseSitesData.mockReturnValue({ + sites: [], + isLoading: true, + refetch: jest.fn(), }); const { result } = renderHook(() => useExploreSearch('')); @@ -257,6 +300,7 @@ describe('useExploreSearch', () => { expect(result.current.isLoading.tokens).toBe(true); expect(result.current.isLoading.perps).toBe(true); expect(result.current.isLoading.predictions).toBe(true); + expect(result.current.isLoading.sites).toBe(true); }); it('filters across multiple sections simultaneously', async () => { diff --git a/app/components/Views/TrendingView/SectionSites/hooks/useSitesData.ts b/app/components/Views/TrendingView/SectionSites/hooks/useSitesData.ts index df7ae454c8ce..21b042710f54 100644 --- a/app/components/Views/TrendingView/SectionSites/hooks/useSitesData.ts +++ b/app/components/Views/TrendingView/SectionSites/hooks/useSitesData.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import Logger from '../../../../../util/Logger'; import type { SiteData } from '../SiteRowItem/SiteRowItem'; @@ -29,6 +29,7 @@ interface UseSitesDataResult { sites: SiteData[]; isLoading: boolean; error: Error | null; + refetch: () => void; } const PORTFOLIO_API_BASE_URL = 'https://portfolio.api.cx.metamask.io/'; @@ -57,47 +58,51 @@ export const useSitesData = ({ const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - const fetchSites = async () => { - try { - setIsLoading(true); - setError(null); - - // Use current timestamp - const timestamp = Date.now(); - const url = `${PORTFOLIO_API_BASE_URL}explore/sites?limit=${limit}&ts=${timestamp}`; - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`Failed to fetch sites: ${response.statusText}`); - } - - const data = (await response.json()) as ApiSitesResponse; - - // Transform API response to SiteData format - const transformedSites: SiteData[] = data.dapps.map((dapp) => ({ - id: dapp.id, - name: dapp.name, - url: dapp.website, - displayUrl: extractDisplayUrl(dapp.website), - logoUrl: dapp.logoSrc, - featured: dapp.featured, - })); - - setSites(transformedSites); - } catch (err) { - const fetchError = err instanceof Error ? err : new Error(String(err)); - Logger.error(fetchError, '[useSitesData] Error fetching sites'); - setError(fetchError); - // Don't use fallback data - return empty array to show the error - setSites([]); - } finally { - setIsLoading(false); + const fetchSites = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + + // Use current timestamp + const timestamp = Date.now(); + const url = `${PORTFOLIO_API_BASE_URL}explore/sites?limit=${limit}&ts=${timestamp}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch sites: ${response.statusText}`); } - }; - fetchSites(); + const data = (await response.json()) as ApiSitesResponse; + + // Transform API response to SiteData format + const transformedSites: SiteData[] = data.dapps.map((dapp) => ({ + id: dapp.id, + name: dapp.name, + url: dapp.website, + displayUrl: extractDisplayUrl(dapp.website), + logoUrl: dapp.logoSrc, + featured: dapp.featured, + })); + + setSites(transformedSites); + } catch (err) { + const fetchError = err instanceof Error ? err : new Error(String(err)); + Logger.error(fetchError, '[useSitesData] Error fetching sites'); + setError(fetchError); + // Don't use fallback data - return empty array to show the error + setSites([]); + } finally { + setIsLoading(false); + } }, [limit]); - return { sites, isLoading, error }; + useEffect(() => { + fetchSites(); + }, [fetchSites]); + + const refetch = useCallback(() => { + fetchSites(); + }, [fetchSites]); + + return { sites, isLoading, error, refetch }; }; diff --git a/app/components/Views/TrendingView/TrendingView.test.tsx b/app/components/Views/TrendingView/TrendingView.test.tsx index fd5fc276aba1..327b39c09049 100644 --- a/app/components/Views/TrendingView/TrendingView.test.tsx +++ b/app/components/Views/TrendingView/TrendingView.test.tsx @@ -94,14 +94,17 @@ jest.mock( ); // Mock useTrendingRequest to return empty results -jest.mock('../../../components/UI/Trending/hooks/useTrendingRequest', () => ({ - useTrendingRequest: jest.fn(() => ({ - results: [], - isLoading: false, - error: null, - fetch: jest.fn(), - })), -})); +jest.mock( + '../../../components/UI/Trending/hooks/useTrendingRequest/useTrendingRequest', + () => ({ + useTrendingRequest: jest.fn(() => ({ + results: [], + isLoading: false, + error: null, + fetch: jest.fn(), + })), + }), +); describe('TrendingView', () => { const mockUseSelector = useSelector as jest.MockedFunction< diff --git a/app/components/Views/TrendingView/TrendingView.tsx b/app/components/Views/TrendingView/TrendingView.tsx index 5b24fd6c0a42..5593c6cd6446 100644 --- a/app/components/Views/TrendingView/TrendingView.tsx +++ b/app/components/Views/TrendingView/TrendingView.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useMemo, useEffect } from 'react'; -import { ScrollView, TouchableOpacity } from 'react-native'; +import React, { useCallback, useMemo, useEffect, useState } from 'react'; +import { ScrollView, TouchableOpacity, RefreshControl } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { useSelector } from 'react-redux'; @@ -68,6 +68,8 @@ const TrendingFeed: React.FC = () => { const navigation = useNavigation(); const { isEnabled } = useMetrics(); const { colors } = useTheme(); + const [refreshing, setRefreshing] = useState(false); + const [refreshTrigger, setRefreshTrigger] = useState(0); // Update state when returning to TrendingFeed useEffect(() => { @@ -106,6 +108,22 @@ const TrendingFeed: React.FC = () => { navigation.navigate(Routes.EXPLORE_SEARCH); }, [navigation]); + // Clean up timeout when component unmounts or refreshing changes + useEffect(() => { + if (refreshing) { + const timeoutId = setTimeout(() => { + setRefreshing(false); + }, 1000); + + return () => clearTimeout(timeoutId); + } + }, [refreshing]); + + const handleRefresh = useCallback(() => { + setRefreshing(true); + setRefreshTrigger((prev) => prev + 1); + }, []); + return ( @@ -147,13 +165,21 @@ const TrendingFeed: React.FC = () => { + } > {HOME_SECTIONS_ARRAY.map((section) => ( - + ))} diff --git a/app/components/Views/TrendingView/components/SectionCard/SectionCard.tsx b/app/components/Views/TrendingView/components/SectionCard/SectionCard.tsx index a1239da199a0..3fa1d09bbf32 100644 --- a/app/components/Views/TrendingView/components/SectionCard/SectionCard.tsx +++ b/app/components/Views/TrendingView/components/SectionCard/SectionCard.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useEffect } from 'react'; import { StyleSheet } from 'react-native'; import { Theme } from '../../../../../util/theme/models'; import { useAppThemeFromContext } from '../../../../../util/theme'; @@ -20,15 +20,25 @@ const createStyles = (theme: Theme) => }); interface SectionCardProps { sectionId: SectionId; + refreshTrigger?: number; } -const SectionCard: React.FC = ({ sectionId }) => { +const SectionCard: React.FC = ({ + sectionId, + refreshTrigger, +}) => { const navigation = useNavigation(); const theme = useAppThemeFromContext(); const styles = useMemo(() => createStyles(theme), [theme]); const section = SECTIONS_CONFIG[sectionId]; - const { data, isLoading } = section.useSectionData(); + const { data, isLoading, refetch } = section.useSectionData(); + + useEffect(() => { + if (refreshTrigger && refreshTrigger > 0 && refetch) { + refetch(); + } + }, [refreshTrigger, refetch]); const renderFlatItem: ListRenderItem = useCallback( ({ item }) => , diff --git a/app/components/Views/TrendingView/components/SectionCarrousel/SectionCarrousel.tsx b/app/components/Views/TrendingView/components/SectionCarrousel/SectionCarrousel.tsx index 3a389de35ead..858c40355fef 100644 --- a/app/components/Views/TrendingView/components/SectionCarrousel/SectionCarrousel.tsx +++ b/app/components/Views/TrendingView/components/SectionCarrousel/SectionCarrousel.tsx @@ -1,6 +1,6 @@ import { Box, BoxBorderColor } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; -import React, { useRef } from 'react'; +import React, { useRef, useEffect } from 'react'; import { Dimensions } from 'react-native'; import { FlashList, FlashListRef } from '@shopify/flash-list'; import { SectionId, SECTIONS_CONFIG } from '../../config/sections.config'; @@ -13,15 +13,25 @@ const CARD_HEIGHT = 220; export interface SectionCarrouselProps { sectionId: SectionId; + refreshTrigger?: number; } -const SectionCarrousel: React.FC = ({ sectionId }) => { +const SectionCarrousel: React.FC = ({ + sectionId, + refreshTrigger, +}) => { const navigation = useNavigation(); const tw = useTailwind(); const flashListRef = useRef>(null); const section = SECTIONS_CONFIG[sectionId]; - const { data, isLoading } = section.useSectionData(); + const { data, isLoading, refetch } = section.useSectionData(); + + useEffect(() => { + if (refreshTrigger && refreshTrigger > 0 && refetch) { + refetch(); + } + }, [refreshTrigger, refetch]); const skeletonCount = 3; const skeletonData = Array.from({ length: skeletonCount }); diff --git a/app/components/Views/TrendingView/config/sections.config.tsx b/app/components/Views/TrendingView/config/sections.config.tsx index 0506d56ef740..989a94b1b1b7 100644 --- a/app/components/Views/TrendingView/config/sections.config.tsx +++ b/app/components/Views/TrendingView/config/sections.config.tsx @@ -1,9 +1,6 @@ import React from 'react'; import type { NavigationProp, ParamListBase } from '@react-navigation/native'; -import type { - TrendingAsset, - SortTrendingBy, -} from '@metamask/assets-controllers'; +import type { TrendingAsset } from '@metamask/assets-controllers'; import Routes from '../../../../constants/navigation/Routes'; import { strings } from '../../../../../locales/i18n'; import TrendingTokenRowItem from '../../../UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem'; @@ -17,20 +14,16 @@ import type { PerpsNavigationParamList } from '../../../UI/Perps/types/navigatio import PredictMarketSkeleton from '../../../UI/Predict/components/PredictMarketSkeleton'; import SectionCard from '../components/SectionCard/SectionCard'; import SectionCarrousel from '../components/SectionCarrousel/SectionCarrousel'; -import { useTrendingRequest } from '../../../UI/Trending/hooks/useTrendingRequest'; -import { sortTrendingTokens } from '../../../UI/Trending/utils/sortTrendingTokens'; -import { PriceChangeOption } from '../../../UI/Trending/components/TrendingTokensBottomSheet'; import { usePredictMarketData } from '../../../UI/Predict/hooks/usePredictMarketData'; import { usePerpsMarkets } from '../../../UI/Perps/hooks'; import { PerpsConnectionProvider } from '../../../UI/Perps/providers/PerpsConnectionProvider'; import { PerpsStreamProvider } from '../../../UI/Perps/providers/PerpsStreamManager'; -import { useSearchRequest } from '../../../UI/Trending/hooks/useSearchRequest'; import { Box, IconName } from '@metamask/design-system-react-native'; import type { SiteData } from '../SectionSites/SiteRowItem/SiteRowItem'; import SiteRowItemWrapper from '../SectionSites/SiteRowItemWrapper'; import SiteSkeleton from '../SectionSites/SiteSkeleton/SiteSkeleton'; import { useSitesData } from '../SectionSites/hooks/useSitesData'; -import { CaipChainId } from '@metamask/utils'; +import { useTrendingSearch } from '../../../UI/Trending/hooks/useTrendingSearch/useTrendingSearch'; export type SectionId = 'predictions' | 'tokens' | 'perps' | 'sites'; @@ -40,12 +33,6 @@ interface SectionData { refetch?: () => void; } -interface SectionParams { - searchQuery?: string; - sortBy?: SortTrendingBy; - chainIds?: CaipChainId[] | null; -} - interface SectionConfig { id: SectionId; title: string; @@ -58,11 +45,11 @@ interface SectionConfig { Skeleton: React.ComponentType; getSearchableText: (item: unknown) => string; keyExtractor: (item: unknown) => string; - Section: React.ComponentType; - useSectionData: (params?: SectionParams) => { + Section: React.ComponentType<{ refreshTrigger?: number }>; + useSectionData: (searchQuery?: string) => { data: unknown[]; isLoading: boolean; - refetch?: () => void; + refetch: () => void; }; } @@ -97,60 +84,13 @@ export const SECTIONS_CONFIG: Record = { getSearchableText: (item) => `${(item as TrendingAsset).symbol} ${(item as TrendingAsset).name}`.toLowerCase(), keyExtractor: (item) => `token-${(item as TrendingAsset).assetId}`, - Section: () => , - useSectionData: (params?: SectionParams) => { - const { searchQuery, sortBy, chainIds } = params ?? {}; - // Trending will return tokens that have just been created which wont be picked up by search API - // so if you see a token on trending and search on omnisearch which uses the search endpoint... - // There is a chance you will get 0 results - const { results: searchResults, isLoading: isSearchLoading } = - useSearchRequest({ - query: searchQuery || '', - limit: 20, - chainIds: [], - }); - - const { - results: trendingResults, - isLoading: isTrendingLoading, - fetch: fetchTrendingTokens, - } = useTrendingRequest({ - sortBy, - chainIds: chainIds ?? undefined, - }); - - if (!searchQuery) { - const sortedResults = sortTrendingTokens( - trendingResults, - PriceChangeOption.PriceChange, - ); - return { - data: sortedResults, - isLoading: isTrendingLoading, - refetch: () => { - fetchTrendingTokens(); - }, - }; - } - - const resultMap = new Map( - trendingResults.map((result) => [result.assetId, result]), - ); - - searchResults.forEach((result) => { - const asset = result as TrendingAsset; - if (!resultMap.has(asset.assetId)) { - resultMap.set(asset.assetId, asset); - } - }); + Section: ({ refreshTrigger }) => ( + + ), + useSectionData: (searchQuery) => { + const { data, isLoading, refetch } = useTrendingSearch(searchQuery); - return { - data: Array.from(resultMap.values()), - isLoading: isSearchLoading, - refetch: () => { - fetchTrendingTokens(); - }, - }; + return { data, isLoading, refetch }; }, }, perps: { @@ -184,17 +124,21 @@ export const SECTIONS_CONFIG: Record = { getSearchableText: (item) => `${(item as PerpsMarketData).symbol} ${(item as PerpsMarketData).name || ''}`.toLowerCase(), keyExtractor: (item) => `perp-${(item as PerpsMarketData).symbol}`, - Section: () => ( + Section: ({ refreshTrigger }) => ( - + ), useSectionData: () => { - const { markets, isLoading } = usePerpsMarkets(); + const { markets, isLoading, refresh, isRefreshing } = usePerpsMarkets(); - return { data: markets, isLoading }; + return { + data: markets, + isLoading: isLoading || isRefreshing, + refetch: refresh, + }; }, }, predictions: { @@ -215,16 +159,20 @@ export const SECTIONS_CONFIG: Record = { getSearchableText: (item) => (item as PredictMarketType).title.toLowerCase(), keyExtractor: (item) => `prediction-${(item as PredictMarketType).id}`, - Section: () => , - useSectionData: (params?: SectionParams) => { - const { searchQuery } = params ?? {}; - const { marketData, isFetching } = usePredictMarketData({ + Section: ({ refreshTrigger }) => ( + + ), + useSectionData: (searchQuery) => { + const { marketData, isFetching, refetch } = usePredictMarketData({ category: 'trending', pageSize: searchQuery ? 20 : 6, q: searchQuery || undefined, }); - return { data: marketData, isLoading: isFetching }; + return { data: marketData, isLoading: isFetching, refetch }; }, }, sites: { @@ -241,10 +189,12 @@ export const SECTIONS_CONFIG: Record = { getSearchableText: (item) => `${(item as SiteData).name} ${(item as SiteData).displayUrl}`.toLowerCase(), keyExtractor: (item) => `site-${(item as SiteData).id}`, - Section: () => , + Section: ({ refreshTrigger }) => ( + + ), useSectionData: () => { - const { sites, isLoading } = useSitesData({ limit: 100 }); - return { data: sites, isLoading }; + const { sites, isLoading, refetch } = useSitesData({ limit: 100 }); + return { data: sites, isLoading, refetch }; }, }, }; @@ -277,13 +227,16 @@ export const useSectionsData = ( searchQuery?: string, ): Record => { const { data: trendingTokens, isLoading: isTokensLoading } = - SECTIONS_CONFIG.tokens.useSectionData({ searchQuery }); + SECTIONS_CONFIG.tokens.useSectionData(searchQuery); + const { data: perpsMarkets, isLoading: isPerpsLoading } = - SECTIONS_CONFIG.perps.useSectionData(); + SECTIONS_CONFIG.perps.useSectionData(searchQuery); + const { data: predictionMarkets, isLoading: isPredictionsLoading } = - SECTIONS_CONFIG.predictions.useSectionData({ searchQuery }); + SECTIONS_CONFIG.predictions.useSectionData(searchQuery); + const { data: sites, isLoading: isSitesLoading } = - SECTIONS_CONFIG.sites.useSectionData(); + SECTIONS_CONFIG.sites.useSectionData(searchQuery); return { tokens: { diff --git a/app/core/DeeplinkManager/handlers/legacy/__tests__/handlePredictUrl.test.ts b/app/core/DeeplinkManager/handlers/legacy/__tests__/handlePredictUrl.test.ts index 88ff6221446c..69fbd26e4d0f 100644 --- a/app/core/DeeplinkManager/handlers/legacy/__tests__/handlePredictUrl.test.ts +++ b/app/core/DeeplinkManager/handlers/legacy/__tests__/handlePredictUrl.test.ts @@ -74,7 +74,7 @@ describe('handlePredictUrl', () => { }); }); - it('handles multiple URL parameters', async () => { + it('handles multiple URL parameters with utm_source in entryPoint', async () => { await handlePredictUrl({ predictPath: '?market=xyz123&utm_source=campaign&debug=true', }); @@ -83,7 +83,7 @@ describe('handlePredictUrl', () => { screen: Routes.PREDICT.MARKET_DETAILS, params: { marketId: 'xyz123', - entryPoint: 'deeplink', + entryPoint: 'deeplink_campaign', }, }); }); @@ -158,13 +158,13 @@ describe('handlePredictUrl', () => { }); }); - it('navigates to market list when only other parameters provided', async () => { + it('navigates to market list with utm_source in entryPoint when only utm_source provided', async () => { await handlePredictUrl({ predictPath: '?utm_source=campaign' }); expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { screen: Routes.PREDICT.MARKET_LIST, params: { - entryPoint: 'deeplink', + entryPoint: 'deeplink_campaign', }, }); }); @@ -247,7 +247,7 @@ describe('handlePredictUrl', () => { expect(DevLogger.log).toHaveBeenCalledWith( '[handlePredictUrl] Parsed navigation parameters:', - { market: '23246' }, + { market: '23246', utmSource: undefined }, ); }); @@ -346,7 +346,7 @@ describe('handlePredictUrl', () => { }); }); - it('sets entryPoint to deeplink when origin is undefined', async () => { + it('sets entryPoint to deeplink when origin is undefined and no utm_source', async () => { await handlePredictUrl({ predictPath: '?market=23246', origin: undefined, @@ -361,7 +361,7 @@ describe('handlePredictUrl', () => { }); }); - it('sets entryPoint to deeplink when origin is deeplink', async () => { + it('sets entryPoint to deeplink when origin is deeplink and no utm_source', async () => { await handlePredictUrl({ predictPath: '?market=23246', origin: 'deeplink', @@ -409,4 +409,143 @@ describe('handlePredictUrl', () => { ); }); }); + + describe('utm_source parameter handling', () => { + it('sets entryPoint to deeplink_test when utm_source is test', async () => { + await handlePredictUrl({ + predictPath: '?market=23246&utm_source=test', + }); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { + screen: Routes.PREDICT.MARKET_DETAILS, + params: { + marketId: '23246', + entryPoint: 'deeplink_test', + }, + }); + }); + + it('sets entryPoint to deeplink_twitter when utm_source is twitter', async () => { + await handlePredictUrl({ + predictPath: '?marketId=12345&utm_source=twitter', + }); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { + screen: Routes.PREDICT.MARKET_DETAILS, + params: { + marketId: '12345', + entryPoint: 'deeplink_twitter', + }, + }); + }); + + it('appends utm_source to carousel origin', async () => { + await handlePredictUrl({ + predictPath: '?market=23246&utm_source=test', + origin: 'carousel', + }); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { + screen: Routes.PREDICT.MARKET_DETAILS, + params: { + marketId: '23246', + entryPoint: 'carousel_test', + }, + }); + }); + + it('appends utm_source to deeplink origin', async () => { + await handlePredictUrl({ + predictPath: '?market=23246&utm_source=test', + origin: 'deeplink', + }); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { + screen: Routes.PREDICT.MARKET_DETAILS, + params: { + marketId: '23246', + entryPoint: 'deeplink_test', + }, + }); + }); + + it('appends utm_source to notification origin', async () => { + await handlePredictUrl({ + predictPath: '?market=23246&utm_source=campaign', + origin: 'notification', + }); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { + screen: Routes.PREDICT.MARKET_DETAILS, + params: { + marketId: '23246', + entryPoint: 'notification_campaign', + }, + }); + }); + + it('does not append utm_source when it equals origin', async () => { + await handlePredictUrl({ + predictPath: '?market=23246&utm_source=carousel', + origin: 'carousel', + }); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { + screen: Routes.PREDICT.MARKET_DETAILS, + params: { + marketId: '23246', + entryPoint: 'carousel', + }, + }); + }); + + it('does not append utm_source when it equals default deeplink', async () => { + await handlePredictUrl({ + predictPath: '?market=23246&utm_source=deeplink', + }); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { + screen: Routes.PREDICT.MARKET_DETAILS, + params: { + marketId: '23246', + entryPoint: 'deeplink', + }, + }); + }); + + it('navigates to market list with deeplink_test entryPoint when no market but utm_source present', async () => { + await handlePredictUrl({ + predictPath: '?utm_source=test', + }); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, { + screen: Routes.PREDICT.MARKET_LIST, + params: { + entryPoint: 'deeplink_test', + }, + }); + }); + + it('logs parsed utm_source in navigation parameters', async () => { + await handlePredictUrl({ + predictPath: '?market=23246&utm_source=test', + }); + + expect(DevLogger.log).toHaveBeenCalledWith( + '[handlePredictUrl] Parsed navigation parameters:', + { market: '23246', utmSource: 'test' }, + ); + }); + + it('logs entry point with utm_source suffix', async () => { + await handlePredictUrl({ + predictPath: '?market=23246&utm_source=test', + }); + + expect(DevLogger.log).toHaveBeenCalledWith( + '[handlePredictUrl] Entry point:', + 'deeplink_test', + ); + }); + }); }); diff --git a/app/core/DeeplinkManager/handlers/legacy/handlePredictUrl.ts b/app/core/DeeplinkManager/handlers/legacy/handlePredictUrl.ts index 65e36b17ead7..f835cd92d231 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handlePredictUrl.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handlePredictUrl.ts @@ -12,6 +12,7 @@ interface HandlePredictUrlParams { */ interface PredictNavigationParams { market?: string; // Market ID + utmSource?: string; // UTM source for analytics tracking } /** @@ -28,9 +29,11 @@ const parsePredictNavigationParams = ( // Support both 'market' and 'marketId' parameter names const marketId = urlParams.get('market') || urlParams.get('marketId'); + const utmSource = urlParams.get('utm_source'); return { market: marketId || undefined, + utmSource: utmSource || undefined, }; }; @@ -76,13 +79,14 @@ const handleMarketNavigation = (marketId: string, entryPoint: string) => { * - https://metamask.app.link/predict * - https://metamask.app.link/predict?market=23246 * - https://metamask.app.link/predict?marketId=23246 + * - https://metamask.app.link/predict?market=23246&utm_source=test * - https://link.metamask.io/predict?market=23246 * - https://link.metamask.io/predict?marketId=23246 * - * Origin handling: - * - Uses origin value directly as entryPoint for analytics tracking - * - Defaults to 'deeplink' if origin is not provided - * - Examples: 'carousel', 'notification', 'deeplink', etc. + * Origin/EntryPoint handling: + * - Base entryPoint is origin if provided, otherwise 'deeplink' + * - If utm_source is present, always appends '_' + utm_source to the base + * - Examples: 'deeplink', 'deeplink_test', 'carousel_twitter', 'notification_campaign' * * Navigation behavior: * - No market param: Navigate to market list @@ -100,10 +104,6 @@ export const handlePredictUrl = async ({ ); try { - // Use origin as entry point, default to 'deeplink' if not provided - const entryPoint = origin || 'deeplink'; - DevLogger.log('[handlePredictUrl] Entry point:', entryPoint); - // Parse navigation parameters from URL const navParams = parsePredictNavigationParams(predictPath); DevLogger.log( @@ -111,6 +111,19 @@ export const handlePredictUrl = async ({ navParams, ); + // Determine entry point: + // - Base is origin if provided, otherwise 'deeplink' + // - If utm_source is present and different from base, append '_' + utm_source + // - If utm_source equals base, don't append (avoid 'deeplink_deeplink') + // - Examples: 'deeplink_test', 'carousel_twitter', 'notification_campaign' + const baseEntryPoint = origin || 'deeplink'; + const shouldAppendUtmSource = + navParams.utmSource && navParams.utmSource !== baseEntryPoint; + const entryPoint = shouldAppendUtmSource + ? `${baseEntryPoint}_${navParams.utmSource}` + : baseEntryPoint; + DevLogger.log('[handlePredictUrl] Entry point:', entryPoint); + // If market ID is provided, navigate to market details if (navParams.market) { handleMarketNavigation(navParams.market, entryPoint); diff --git a/app/core/Engine/controllers/token-balances-controller-init.test.ts b/app/core/Engine/controllers/token-balances-controller-init.test.ts index 1a3e5eb7331d..89d65e9cc5e1 100644 --- a/app/core/Engine/controllers/token-balances-controller-init.test.ts +++ b/app/core/Engine/controllers/token-balances-controller-init.test.ts @@ -52,7 +52,7 @@ describe('TokenBalancesControllerInit', () => { expect(controllerMock).toHaveBeenCalledWith({ messenger: expect.any(Object), state: undefined, - interval: 180_000, + interval: 30_000, allowExternalServices: expect.any(Function), queryMultipleAccounts: expect.any(Boolean), accountsApiChainIds: expect.any(Function), diff --git a/app/core/Engine/controllers/token-balances-controller-init.ts b/app/core/Engine/controllers/token-balances-controller-init.ts index 45ac78aceaf8..d09f90d3d5f9 100644 --- a/app/core/Engine/controllers/token-balances-controller-init.ts +++ b/app/core/Engine/controllers/token-balances-controller-init.ts @@ -24,8 +24,7 @@ export const tokenBalancesControllerInit: ControllerInitFunction< const controller = new TokenBalancesController({ messenger: controllerMessenger, state: persistedState.TokenBalancesController, - // TODO: This is long, can we decrease it? - interval: 180_000, + interval: 30_000, allowExternalServices: () => selectBasicFunctionalityEnabled(getState()), queryMultipleAccounts: preferencesState.isMultiAccountBalancesEnabled, accountsApiChainIds: () => diff --git a/app/selectors/multichain/evm.test.tsx b/app/selectors/multichain/evm.test.tsx index 04e8bf7d7a7a..e8d3873099ed 100644 --- a/app/selectors/multichain/evm.test.tsx +++ b/app/selectors/multichain/evm.test.tsx @@ -156,7 +156,7 @@ describe('Multichain Selectors', () => { '0x1': { balance: '0x1', stakedBalance: '0x2', - isStaked: true, + isStaked: false, name: '', }, '0x89': { @@ -500,11 +500,13 @@ describe('Multichain Selectors', () => { } as unknown as RootState; const result = selectEvmTokensWithZeroBalanceFilter(testState); - expect(result).toBeDefined(); expect(result.every((token) => token.isNative === true)).toBeTruthy(); expect(result.every((token) => token.balance === '0')).toBeTruthy(); - expect(result.length).toBe(2); // Native tokens should remain + expect( + result.some((token) => token.name === 'Staked Ethereum'), + ).toBeTruthy(); + expect(result.length).toBe(3); // Native tokens should remain and Staked Ethereum }); }); diff --git a/app/selectors/multichain/evm.ts b/app/selectors/multichain/evm.ts index e6fc7ae2c869..98e6cf160889 100644 --- a/app/selectors/multichain/evm.ts +++ b/app/selectors/multichain/evm.ts @@ -18,11 +18,7 @@ import { } from '../networkController'; import { TokenI } from '../../components/UI/Tokens/types'; import { renderFromWei, weiToFiat } from '../../util/number'; -import { - hexToBN, - toChecksumHexAddress, - toHex, -} from '@metamask/controller-utils'; +import { hexToBN, toChecksumHexAddress } from '@metamask/controller-utils'; import { selectConversionRate, selectCurrencyRates, @@ -79,7 +75,7 @@ export const selectedAccountNativeTokenCachedBalanceByChainIdForAddress = result[chainId] = { balance: account.balance, stakedBalance: account.stakedBalance ?? '0x0', - isStaked: account.stakedBalance !== '0x0', + isStaked: false, name: '', }; } @@ -190,13 +186,7 @@ export const selectNativeTokensAcrossChainsForAddress = createSelector( // Non-staked tokens tokensByChain[nativeChainId].push(tokenByChain); - if ( - nativeTokenInfoByChainId && - nativeTokenInfoByChainId.isStaked && - nativeTokenInfoByChainId.stakedBalance !== '0x00' && - nativeTokenInfoByChainId.stakedBalance !== toHex(0) && - nativeTokenInfoByChainId.stakedBalance !== '0' - ) { + if (nativeTokenInfoByChainId && !nativeTokenInfoByChainId.isStaked) { // Staked tokens tokensByChain[nativeChainId].push({ ...nativeTokenInfoByChainId, diff --git a/app/util/bridge/index.test.ts b/app/util/bridge/index.test.ts new file mode 100644 index 000000000000..37f4af3aa334 --- /dev/null +++ b/app/util/bridge/index.test.ts @@ -0,0 +1,106 @@ +import { NATIVE_SWAPS_TOKEN_ADDRESS } from '../../constants/bridge'; +import { isSwapsNativeAsset } from './index'; + +describe('isSwapsNativeAsset', () => { + describe('Native Token Detection', () => { + it('returns true for token with native address', () => { + const token = { address: NATIVE_SWAPS_TOKEN_ADDRESS }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(true); + }); + + it('returns false for token with non-native address', () => { + const token = { address: '0x1234567890123456789012345678901234567890' }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(false); + }); + }); + + describe('Edge Cases', () => { + it('returns false for undefined token', () => { + const result = isSwapsNativeAsset(undefined); + + expect(result).toBe(false); + }); + + it('returns false for null token', () => { + const result = isSwapsNativeAsset(null as unknown as undefined); + + expect(result).toBe(false); + }); + + it('returns false for token without address property', () => { + const token = {} as { address: string }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(false); + }); + + it('returns false for token with null address', () => { + const token = { address: null as unknown as string }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(false); + }); + + it('returns false for token with empty string address', () => { + const token = { address: '' }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(false); + }); + }); + + describe('Address Formatting', () => { + it('returns false for address with incorrect length', () => { + const token = { address: '0x00' }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(false); + }); + + it('returns false for address without 0x prefix', () => { + const token = { address: '0000000000000000000000000000000000000000' }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(false); + }); + }); + + describe('Token Object Variations', () => { + it('returns true when token has additional properties', () => { + const token = { + address: NATIVE_SWAPS_TOKEN_ADDRESS, + symbol: 'ETH', + decimals: 18, + name: 'Ethereum', + }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(true); + }); + + it('returns false when token has wrong address but other properties', () => { + const token = { + address: '0x1111111111111111111111111111111111111111', + symbol: 'ETH', + decimals: 18, + name: 'Ethereum', + }; + + const result = isSwapsNativeAsset(token); + + expect(result).toBe(false); + }); + }); +}); diff --git a/app/util/bridge/index.ts b/app/util/bridge/index.ts new file mode 100644 index 000000000000..521bf04145d0 --- /dev/null +++ b/app/util/bridge/index.ts @@ -0,0 +1,5 @@ +import { NATIVE_SWAPS_TOKEN_ADDRESS } from '../../constants/bridge'; + +export function isSwapsNativeAsset(token: { address: string } | undefined) { + return Boolean(token) && token?.address === NATIVE_SWAPS_TOKEN_ADDRESS; +} diff --git a/e2e/api-mocking/mock-responses/polymarket/market-feed-responses/polymarket-sports-feed.ts b/e2e/api-mocking/mock-responses/polymarket/market-feed-responses/polymarket-sports-feed.ts index 24a0ad60a933..928ac67bb393 100644 --- a/e2e/api-mocking/mock-responses/polymarket/market-feed-responses/polymarket-sports-feed.ts +++ b/e2e/api-mocking/mock-responses/polymarket/market-feed-responses/polymarket-sports-feed.ts @@ -6,6 +6,1471 @@ export const POLYMARKET_SPORTS_FEED = { data: [ + { + id: '79682', + ticker: 'nba-bos-bkn-2025-11-18', + slug: 'nba-bos-bkn-2025-11-18', + title: 'Celtics vs. Nets', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30PM ET:\nIf the Celtics win, the market will resolve to "Celtics".\nIf the Nets win, the market will resolve to "Nets".\nIf the game is postponed, this market will remain open until the game has been completed.\nIf the game is canceled entirely, with no make-up game, this market will resolve 50-50.\nThe result will be determined based on the final score including any overtime periods.', + resolutionSource: 'https://www.nba.com/', + startDate: '2025-11-12T15:04:44.432208Z', + creationDate: '2025-11-19T00:30:00Z', + endDate: '2025-11-19T00:30:00Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + active: true, + closed: false, + archived: false, + new: false, + featured: false, + restricted: true, + liquidity: 967230.7467, + volume: 701903.457739, + openInterest: 0, + createdAt: '2025-11-12T15:00:10.347325Z', + updatedAt: '2025-11-18T23:47:14.979347Z', + competitive: 0.9999750006249843, + volume24hr: 266696.2573040002, + volume1wk: 284106.0153140001, + volume1mo: 284106.0153140001, + volume1yr: 284106.0153140001, + enableOrderBook: true, + liquidityClob: 967230.7467, + negRisk: false, + commentCount: 0, + markets: [ + { + id: '689293', + question: '1H Spread: Celtics (-5.5)', + conditionId: + '0x0ffdc6438d3c57c2e50b603dfbf96501d6c566982c52d0376af48f0dd4edea86', + slug: 'nba-bos-bkn-2025-11-18-1h-spread-away-5pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '627.5741', + startDate: '2025-11-18T15:19:52.819753Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the first half of the NBA game between Celtics and Nets, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Celtics" if the Celtics are winning by 6 or more points at halftime.\n\nOtherwise, this market will resolve to "Nets".\n\nThe result will be determined based on the score at halftime only.\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Celtics", "Nets"]', + outcomePrices: '["0.53", "0.47"]', + volume: '105.531817', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T15:10:18.318536Z', + updatedAt: '2025-11-18T23:43:15.286479Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: '1H Spread -5.5', + groupItemThreshold: '1', + questionID: + '0x543ce85b9d0aab8dcd1d5044655d7506b6661fe9787b510ea8ab9a019f652af0', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 105.531817, + liquidityNum: 627.5741, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["108068371497168852567577230155842073347895130810989413557553404143029769084894", "43624146331208085366589596755497297254594488561848600867667077247429522668678"]', + umaBond: '500', + umaReward: '2', + volumeClob: 105.531817, + liquidityClob: 627.5741, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T15:19:31Z', + cyom: false, + competitive: 0.9991008092716555, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.04, + oneHourPriceChange: 0.095, + lastTradePrice: 0.55, + bestBid: 0.51, + bestAsk: 0.55, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'first_half_spreads', + line: 5.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T15:10:35.529464Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '689294', + question: 'Celtics vs. Nets: 1H O/U 114.5', + conditionId: + '0x45d52f3f412a350e92539c5beb0c7f9fd2995d5f34f8375c9a8dfea8b57aae2a', + slug: 'nba-bos-bkn-2025-11-18-1h-total-114pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '2689.3913', + startDate: '2025-11-18T15:19:53.073647Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the first half of the NBA game between Celtics and Nets, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 115 or more points in the first half.\n\nIf the combined first half total is less than 115, this market will resolve to "Under".\n\nThe result will be determined based on the score at halftime only.\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.51", "0.49"]', + volume: '7.203702', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T15:10:18.33167Z', + updatedAt: '2025-11-18T23:40:01.80831Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: '1H O/U 114.5', + groupItemThreshold: '2', + questionID: + '0x045246ded032f4a148e04cf2adbf6b0eb66d3fa45c168da8779250a6043da737', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 7.203702, + liquidityNum: 2689.3913, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["80273045175225620791531841201963655788862885846642502594618864501230582161389", "92894320856806821182874547461302017038362877946795172984205013666469931548553"]', + umaBond: '500', + umaReward: '2', + volumeClob: 7.203702, + liquidityClob: 2689.3913, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T15:19:31Z', + cyom: false, + competitive: 0.9999000099990001, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.04, + lastTradePrice: 0.54, + bestBid: 0.49, + bestAsk: 0.53, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'first_half_totals', + line: 114.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T15:10:35.526986Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '689512', + question: 'Spread: Celtics (-11.5)', + conditionId: + '0x613c9f81a0d81ea82dc9ea86a9e304fbe799144d635d0f28a2e19bcfe611992e', + slug: 'nba-bos-bkn-2025-11-18-spread-away-11pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '43363.2438', + startDate: '2025-11-18T17:22:14.693441Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Celtics" if the Celtics win the game by 12 or more points.\n\nOtherwise, this market will resolve to "Nets". If the game ends in a tie, this market will resolve to "Nets".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Celtics", "Nets"]', + outcomePrices: '["0.48", "0.52"]', + volume: '805.107158', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T17:10:19.321801Z', + updatedAt: '2025-11-18T23:44:51.829053Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'Spread -11.5', + groupItemThreshold: '1', + questionID: + '0xe5457dd19365531506edff95ca0fa4c310a86650cf0727b0e6994b775e788860', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 805.107158, + liquidityNum: 43363.2438, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["110508943375323235088649629392596121572557723445156378117896978939545868157193", "60557095252490039251801883872812055334181367912488898087280078177857052043210"]', + umaBond: '500', + umaReward: '2', + volumeClob: 805.107158, + liquidityClob: 43363.2438, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T17:21:53Z', + cyom: false, + competitive: 0.9996001599360256, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.02, + oneHourPriceChange: 0.01, + lastTradePrice: 0.48, + bestBid: 0.47, + bestAsk: 0.49, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'spreads', + line: 11.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T17:10:35.936242Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '689826', + question: 'Celtics vs. Nets: O/U 221.5', + conditionId: + '0xd6fa8571dbfb69007cc05a1b95caf30ce5458aa70e816d8ebd6089bd51a52c5a', + slug: 'nba-bos-bkn-2025-11-18-total-221pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '19881.6956', + startDate: '2025-11-18T18:11:28.860564Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 222 or more points in this game.\n\nIf the combined total is less than 222, this market will resolve to "Under".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.545", "0.455"]', + volume: '1729.722825', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T18:10:18.666997Z', + updatedAt: '2025-11-18T23:45:29.056618Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'O/U 221.5', + groupItemThreshold: '2', + questionID: + '0x591dbd052803740e216f6008ad14d7a53e9041c1c617d5cb8ba1a5fd6670a3ec', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 1729.722825, + liquidityNum: 19881.6956, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["832422272837128527633855122000131439570281100276352454929937152326590055020", "5979089740070234605187413316264690235725412472413817808042163368309104259068"]', + umaBond: '500', + umaReward: '2', + volumeClob: 1729.722825, + liquidityClob: 19881.6956, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T18:11:07Z', + cyom: false, + competitive: 0.9979790923380155, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.03, + oneHourPriceChange: 0.005, + lastTradePrice: 0.56, + bestBid: 0.53, + bestAsk: 0.56, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'totals', + line: 221.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T18:10:36.541917Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '678176', + question: 'Celtics vs. Nets', + conditionId: + '0x81daa857b8fa34cd3627c8cdbe5d92ea98756bcbe1e5cfcfffb94754e4d5ed86', + slug: 'nba-bos-bkn-2025-11-18', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '477142.6791', + startDate: '2025-11-12T15:02:01.40148Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30PM ET:\nIf the Celtics win, the market will resolve to "Celtics".\nIf the Nets win, the market will resolve to "Nets".\nIf the game is postponed, this market will remain open until the game has been completed.\nIf the game is canceled entirely, with no make-up game, this market will resolve 50-50.\nThe result will be determined based on the final score including any overtime periods.', + outcomes: '["Celtics", "Nets"]', + outcomePrices: '["0.84", "0.17"]', + volume: '532412.789965', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-12T15:00:10.351643Z', + updatedAt: '2025-11-18T23:46:16.11869Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemThreshold: '0', + questionID: + '0xb8289b9c070d9c819ef7d648cbce30a1b6561a4a28d0b34e8f8de637111a833a', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 532412.789965, + liquidityNum: 477142.6791, + endDateIso: '2025-11-19', + startDateIso: '2025-11-12', + hasReviewedDates: true, + volume24hr: 200715.9993420002, + volume1wk: 218125.7573520001, + volume1mo: 218125.7573520001, + volume1yr: 218125.7573520001, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["51851880223290407825872150827934296608070009371891114025629582819868766043137", "51090123154876409384652748958994213129207000557350215937559106819875795938227"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 200715.9993420002, + volume1wkClob: 218125.7573520001, + volume1moClob: 218125.7573520001, + volume1yrClob: 218125.7573520001, + volumeClob: 532412.789965, + liquidityClob: 477142.6791, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-12T15:01:39Z', + cyom: false, + competitive: 0.8990986535997662, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.01, + oneDayPriceChange: 0.02, + lastTradePrice: 0.84, + bestBid: 0.83, + bestAsk: 0.84, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'moneyline', + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-12T15:00:29.791763Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688448', + question: 'Spread: Celtics (-10.5)', + conditionId: + '0x21ece3b69fb3b2f7667396eb9cfe257cd7cab9272f45873ea2f9bd13422234de', + slug: 'nba-bos-bkn-2025-11-18-spread-away-10pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '80605.0765', + startDate: '2025-11-18T01:11:19.229167Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Celtics" if the Celtics win the game by 11 or more points.\n\nOtherwise, this market will resolve to "Nets". If the game ends in a tie, this market will resolve to "Nets".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Celtics", "Nets"]', + outcomePrices: '["0.52", "0.48"]', + volume: '63848.161548', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T01:10:09.537313Z', + updatedAt: '2025-11-18T23:45:55.003443Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'Spread -10.5', + groupItemThreshold: '1', + questionID: + '0x5ff5dc5772d99ad920d2d767d7e74161badb21635483c08a84bb5f97cdd5f844', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 63848.161548, + liquidityNum: 80605.0765, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + volume24hr: 31524.451474999994, + volume1wk: 31524.451474999994, + volume1mo: 31524.451474999994, + volume1yr: 31524.451474999994, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["15017088853136392715353363284417713345295930606902158148962994184018811155033", "31294989122330110339210962344998388017906273135145439870640923354883632922883"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 31524.451474999994, + volume1wkClob: 31524.451474999994, + volume1moClob: 31524.451474999994, + volume1yrClob: 31524.451474999994, + volumeClob: 63848.161548, + liquidityClob: 80605.0765, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T01:10:57Z', + cyom: false, + competitive: 0.9996001599360256, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.02, + oneHourPriceChange: 0.015, + lastTradePrice: 0.53, + bestBid: 0.51, + bestAsk: 0.53, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'spreads', + line: 10.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T01:10:30.688629Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688449', + question: 'Celtics vs. Nets: O/U 226.5', + conditionId: + '0x2b19101c7879712d90166aaaaa09516655355b13f6e7b24ea708711abba942a1', + slug: 'nba-bos-bkn-2025-11-18-total-226pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '3992.4928', + startDate: '2025-11-18T01:11:21.278667Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 227 or more points in this game.\n\nIf the combined total is less than 227, this market will resolve to "Under".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.41", "0.59"]', + volume: '6932.788611', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T01:10:09.551434Z', + updatedAt: '2025-11-18T23:42:50.265104Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'O/U 226.5', + groupItemThreshold: '2', + questionID: + '0x450371c0b5994b33885707f2bfb9ee999bf3b0579006412ec3c04ee42e7a561e', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 6932.788611, + liquidityNum: 3992.4928, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + volume24hr: 3717.689379, + volume1wk: 3717.689379, + volume1mo: 3717.689379, + volume1yr: 3717.689379, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["76722784725406479033816505799115738771577853749471150568066526289006907548097", "61370667709924119848179172295528012846625445600635652547147788701541392106209"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 3717.689379, + volume1wkClob: 3717.689379, + volume1moClob: 3717.689379, + volume1yrClob: 3717.689379, + volumeClob: 6932.788611, + liquidityClob: 3992.4928, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T01:10:59Z', + cyom: false, + competitive: 0.9919650828290844, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.04, + oneHourPriceChange: 0.005, + lastTradePrice: 0.41, + bestBid: 0.39, + bestAsk: 0.43, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'totals', + line: 226.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T01:10:30.690859Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688450', + question: 'Celtics vs. Nets: O/U 225.5', + conditionId: + '0x494ec08c4e7cdd89237726c9ee3788e331afe31478151b059fd2057f57a9b800', + slug: 'nba-bos-bkn-2025-11-18-total-225pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '72211.6828', + startDate: '2025-11-18T01:11:23.428521Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 226 or more points in this game.\n\nIf the combined total is less than 226, this market will resolve to "Under".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.44", "0.56"]', + volume: '12607.698767', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T01:10:09.629649Z', + updatedAt: '2025-11-18T23:40:55.329752Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'O/U 225.5', + groupItemThreshold: '2', + questionID: + '0x04a29a2be8bc3c20a305e1df40fef105ca0637e05eeffbfac8c634348f48888a', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 12607.698767, + liquidityNum: 72211.6828, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + volume24hr: 9954.698767, + volume1wk: 9954.698767, + volume1mo: 9954.698767, + volume1yr: 9954.698767, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["97129870761170918923180556712942881854487815285195135993616056935681553284090", "40593460465290656665478412331796391076324195375488680231140055220774245206460"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 9954.698767, + volume1wkClob: 9954.698767, + volume1moClob: 9954.698767, + volume1yrClob: 9954.698767, + volumeClob: 12607.698767, + liquidityClob: 72211.6828, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T01:11:01Z', + cyom: false, + competitive: 0.9964129135113591, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.02, + lastTradePrice: 0.44, + bestBid: 0.43, + bestAsk: 0.45, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'totals', + line: 225.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T01:10:30.697792Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688523', + question: 'Celtics vs. Nets: O/U 224.5', + conditionId: + '0x3445358f3d2da12309e802c19a1f43f2796b5882a5268fe051adb6af06ca1a1a', + slug: 'nba-bos-bkn-2025-11-18-total-224pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '72976.3334', + startDate: '2025-11-18T03:11:22.595094Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 225 or more points in this game.\n\nIf the combined total is less than 225, this market will resolve to "Under".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.47", "0.53"]', + volume: '11687.976138', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T03:10:12.198457Z', + updatedAt: '2025-11-18T23:43:28.187367Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'O/U 224.5', + groupItemThreshold: '2', + questionID: + '0x6de6e25564c93048aae75f842c5b7b29e5e12b3521b80c1c9726807e2a06043e', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 11687.976138, + liquidityNum: 72976.3334, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + volume24hr: 7675.424232, + volume1wk: 7675.424232, + volume1mo: 7675.424232, + volume1yr: 7675.424232, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["16621484196414011909217908382924720127305711534944452872252868528945503319192", "100749359696614342742636201272451806856306250894090828155945767225409200220347"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 7675.424232, + volume1wkClob: 7675.424232, + volume1moClob: 7675.424232, + volume1yrClob: 7675.424232, + volumeClob: 11687.976138, + liquidityClob: 72976.3334, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T03:11:01Z', + cyom: false, + competitive: 0.9991008092716555, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.02, + oneHourPriceChange: 0.01, + lastTradePrice: 0.44, + bestBid: 0.46, + bestAsk: 0.48, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'totals', + line: 224.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T03:10:31.154428Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688564', + question: '1H Spread: Celtics (-6.5)', + conditionId: + '0x308f829c2f4ff60c11cae42183354d544dfe422209437d7c44761a50603d033b', + slug: 'nba-bos-bkn-2025-11-18-1h-spread-away-6pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '12052.4439', + startDate: '2025-11-18T04:11:25.136196Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the first half of the NBA game between Celtics and Nets, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Celtics" if the Celtics are winning by 7 or more points at halftime.\n\nOtherwise, this market will resolve to "Nets".\n\nThe result will be determined based on the score at halftime only.\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Celtics", "Nets"]', + outcomePrices: '["0.48", "0.52"]', + volume: '656.263332', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T04:10:12.261699Z', + updatedAt: '2025-11-18T23:42:14.967828Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: '1H Spread -6.5', + groupItemThreshold: '1', + questionID: + '0x416638ca2160a4ef2e33bd0cffa9d28307b3339ac511226d2d4303620ba90356', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 656.263332, + liquidityNum: 12052.4439, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + volume24hr: 618.263332, + volume1wk: 618.263332, + volume1mo: 618.263332, + volume1yr: 618.263332, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["4963002790613324419284496989661196417373618132279101117158250974630996660307", "90268915572391101649635323642845926283824621572679706381594150589569923099102"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 618.263332, + volume1wkClob: 618.263332, + volume1moClob: 618.263332, + volume1yrClob: 618.263332, + volumeClob: 656.263332, + liquidityClob: 12052.4439, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T04:11:03Z', + cyom: false, + competitive: 0.9996001599360256, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.04, + lastTradePrice: 0.49, + bestBid: 0.46, + bestAsk: 0.5, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'first_half_spreads', + line: 6.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T04:10:31.009852Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688565', + question: 'Celtics vs. Nets: 1H O/U 115.5', + conditionId: + '0x22ad0cf63be3337c8f37be4e52c56034f122e8cc093117315646fd911c834c56', + slug: 'nba-bos-bkn-2025-11-18-1h-total-115pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '12295.1497', + startDate: '2025-11-18T04:11:23.179889Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the first half of the NBA game between Celtics and Nets, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 116 or more points in the first half.\n\nIf the combined first half total is less than 116, this market will resolve to "Under".\n\nThe result will be determined based on the score at halftime only.\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.475", "0.525"]', + volume: '348', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T04:10:12.270778Z', + updatedAt: '2025-11-18T23:45:04.283394Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: '1H O/U 115.5', + groupItemThreshold: '2', + questionID: + '0xe1fe53363a86f8214ce4ae6c2bf409e53515fd7811ea80ee2af0bb4885c8207f', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 348, + liquidityNum: 12295.1497, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["7771702060422906754074450399318753851509384625615474603337160021460625177251", "9922939295025170195709634841943132146273305318870409059850092632419843085424"]', + umaBond: '500', + umaReward: '2', + volumeClob: 348, + liquidityClob: 12295.1497, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T04:11:01Z', + cyom: false, + competitive: 0.9993753903810119, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.05, + lastTradePrice: 0.47, + bestBid: 0.45, + bestAsk: 0.5, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'first_half_totals', + line: 115.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T04:10:31.012112Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688566', + question: 'Celtics vs. Nets: 1H Moneyline', + conditionId: + '0xefe913eab37fe7dde5fc7ec2c61b3123454bbd73fc16b9662cd6da4f9a66160f', + slug: 'nba-bos-bkn-2025-11-18-1h-moneyline', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '12757.5446', + startDate: '2025-11-18T04:11:23.941603Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the first half of the NBA game between Celtics and Nets, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Celtics" if the Celtics are winning at halftime.\n\nThis market will resolve to "Nets" if the Nets are winning at halftime.\n\nIf the score is tied at halftime, this market will resolve 50-50.\n\nThe result will be determined based on the score at halftime only.\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Celtics", "Nets"]', + outcomePrices: '["0.73", "0.27"]', + volume: '419.642781', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T04:10:12.278159Z', + updatedAt: '2025-11-18T23:45:36.461348Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: '1H Moneyline', + groupItemThreshold: '1', + questionID: + '0xfa58cb72cd7e9c39aa33efa30ec56818c82f36b4eb5c2a402018f92dd40444e9', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 419.642781, + liquidityNum: 12757.5446, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + volume24hr: 1.162785, + volume1wk: 1.162785, + volume1mo: 1.162785, + volume1yr: 1.162785, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["102804267221904213201233058380882893359460020750775519453277126777690870971790", "13454062702372938255024866176925984686641428276405442396707214375066581010754"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 1.162785, + volume1wkClob: 1.162785, + volume1moClob: 1.162785, + volume1yrClob: 1.162785, + volumeClob: 419.642781, + liquidityClob: 12757.5446, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T04:11:01Z', + cyom: false, + competitive: 0.9497578117580017, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.04, + lastTradePrice: 0.75, + bestBid: 0.71, + bestAsk: 0.75, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'first_half_moneyline', + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T04:10:31.014464Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688699', + question: 'Celtics vs. Nets: O/U 223.5', + conditionId: + '0x24cffe50c541ec7c16bf709f24e60022c082fbddcb3c8613f123061a73f9a8c6', + slug: 'nba-bos-bkn-2025-11-18-total-223pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '84014.0855', + startDate: '2025-11-18T06:11:25.970734Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 224 or more points in this game.\n\nIf the combined total is less than 224, this market will resolve to "Under".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.49", "0.51"]', + volume: '16918.667689', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T06:10:14.09503Z', + updatedAt: '2025-11-18T23:45:44.617012Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'O/U 223.5', + groupItemThreshold: '2', + questionID: + '0x8ca0a7edef5c048c278e9ef6297a9bc083e3c6e1051c3db48e8bf9e6629edd44', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 16918.667689, + liquidityNum: 84014.0855, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + volume24hr: 10715.925140999998, + volume1wk: 10715.925140999998, + volume1mo: 10715.925140999998, + volume1yr: 10715.925140999998, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["80345929518963922914579552901588580268641837341337118340857200618531834142042", "73077330761122866279880774797492478180079457800370023589782582976772002999412"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 10715.925140999998, + volume1wkClob: 10715.925140999998, + volume1moClob: 10715.925140999998, + volume1yrClob: 10715.925140999998, + volumeClob: 16918.667689, + liquidityClob: 84014.0855, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T06:11:03Z', + cyom: false, + competitive: 0.9999000099990001, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.02, + oneHourPriceChange: -0.005, + lastTradePrice: 0.5, + bestBid: 0.48, + bestAsk: 0.5, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'totals', + line: 223.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T06:10:32.11275Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '688901', + question: 'Celtics vs. Nets: O/U 222.5', + conditionId: + '0x48a4bc6590d4740b26f58efb8256df3068384cd2ebe6be7ed4a30c2dcd119f77', + slug: 'nba-bos-bkn-2025-11-18-total-222pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '72621.3536', + startDate: '2025-11-18T12:11:24.316451Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 223 or more points in this game.\n\nIf the combined total is less than 223, this market will resolve to "Under".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.505", "0.495"]', + volume: '53423.903406', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T12:10:12.823108Z', + updatedAt: '2025-11-18T23:46:14.020265Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'O/U 222.5', + groupItemThreshold: '2', + questionID: + '0x872cb77fcabac2504c5b5ab69220e0bb495a084b4d283382330f1a118f67486f', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 53423.903406, + liquidityNum: 72621.3536, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + volume24hr: 1772.642851, + volume1wk: 1772.642851, + volume1mo: 1772.642851, + volume1yr: 1772.642851, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["37324283674897121705473745032454811414820166816489006396700304627124687954761", "69869157668836146234743787424584122212834316440389897395554037104596161454548"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 1772.642851, + volume1wkClob: 1772.642851, + volume1moClob: 1772.642851, + volume1yrClob: 1772.642851, + volumeClob: 53423.903406, + liquidityClob: 72621.3536, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T12:11:03Z', + cyom: false, + competitive: 0.9999750006249843, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.01, + lastTradePrice: 0.51, + bestBid: 0.5, + bestAsk: 0.51, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'totals', + line: 222.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T12:10:33.962494Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + ], + tags: [ + { + id: '1', + label: 'Sports', + slug: 'sports', + forceShow: false, + publishedAt: '2023-10-24 22:37:50.296+00', + updatedBy: 15, + createdAt: '2023-10-24T22:37:50.31Z', + updatedAt: '2024-07-05T21:07:21.800664Z', + forceHide: true, + }, + { + id: '745', + label: 'NBA', + slug: 'nba', + forceShow: false, + publishedAt: '2023-12-18 18:24:38.08+00', + createdAt: '2023-12-18T18:24:38.098Z', + updatedAt: '2024-06-18T14:52:57.582861Z', + }, + { + id: '100639', + label: 'Games', + slug: 'games', + forceShow: false, + createdAt: '2024-09-23T22:41:37.670714Z', + }, + { + id: '28', + label: 'Basketball', + slug: 'basketball', + forceShow: false, + publishedAt: '2023-11-02 21:04:25.152+00', + createdAt: '2023-11-02T21:04:25.158Z', + updatedAt: '2024-07-26T21:06:45.637044Z', + }, + ], + cyom: false, + showAllOutcomes: true, + showMarketImages: false, + enableNegRisk: false, + automaticallyActive: true, + eventDate: '2025-11-18', + startTime: '2025-11-19T00:30:00Z', + eventWeek: 3, + seriesSlug: 'nba-2026', + negRiskAugmented: false, + pendingDeployment: false, + deploying: false, + gameId: 20022715, + homeTeamName: 'Nets', + awayTeamName: 'Celtics', + }, + { + id: '82079', + ticker: 'nba-bkn-bos-2025-11-21', + slug: 'nba-bkn-bos-2025-11-21', + title: 'Nets vs. Celtics', + description: + 'In the upcoming NBA game, scheduled for November 21 at 7:30PM ET:\nIf the Nets win, the market will resolve to "Nets".\nIf the Celtics win, the market will resolve to "Celtics".\nIf the game is postponed, this market will remain open until the game has been completed.\nIf the game is canceled entirely, with no make-up game, this market will resolve 50-50.\nThe result will be determined based on the final score including any overtime periods.', + resolutionSource: 'https://www.nba.com/', + startDate: '2025-11-15T15:07:26.209563Z', + creationDate: '2025-11-22T00:30:00Z', + endDate: '2025-11-22T00:30:00Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + active: true, + closed: false, + archived: false, + new: false, + featured: false, + restricted: true, + liquidity: 8097.8394, + volume: 2282.398791, + openInterest: 0, + createdAt: '2025-11-15T15:00:17.443931Z', + updatedAt: '2025-11-18T23:47:10.602215Z', + competitive: 0.9148921570869833, + volume24hr: 2113.108143, + volume1wk: 2169.315459, + volume1mo: 2169.315459, + volume1yr: 2169.315459, + enableOrderBook: true, + liquidityClob: 8097.8394, + negRisk: false, + commentCount: 0, + markets: [ + { + id: '684066', + question: 'Nets vs. Celtics', + conditionId: + '0x3d857ac4405fd659ecab2035addaca5ae82389f915de8238f69657bf285b99bb', + slug: 'nba-bkn-bos-2025-11-21', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-22T00:30:00Z', + liquidity: '8097.8394', + startDate: '2025-11-15T15:02:01.459624Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 21 at 7:30PM ET:\nIf the Nets win, the market will resolve to "Nets".\nIf the Celtics win, the market will resolve to "Celtics".\nIf the game is postponed, this market will remain open until the game has been completed.\nIf the game is canceled entirely, with no make-up game, this market will resolve 50-50.\nThe result will be determined based on the final score including any overtime periods.', + outcomes: '["Nets", "Celtics"]', + outcomePrices: '["0.195", "0.805"]', + volume: '2282.398791', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-15T15:00:17.448172Z', + updatedAt: '2025-11-18T23:44:01.033308Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemThreshold: '0', + questionID: + '0x02539f6584eab9fa26fb89f940161c8209db80e6a7dab0298dc8499677e55664', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 2282.398791, + liquidityNum: 8097.8394, + endDateIso: '2025-11-22', + startDateIso: '2025-11-15', + hasReviewedDates: true, + volume24hr: 2113.108143, + volume1wk: 2169.315459, + volume1mo: 2169.315459, + volume1yr: 2169.315459, + gameStartTime: '2025-11-22 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["77082201186894401474643479835635103211027040145660208923394923749088891246507", "86390850168506290098845752703048424446013236195602054012737317399661275675531"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 2113.108143, + volume1wkClob: 2169.315459, + volume1moClob: 2169.315459, + volume1yrClob: 2169.315459, + volumeClob: 2282.398791, + liquidityClob: 8097.8394, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-15T15:01:39Z', + cyom: false, + competitive: 0.9148921570869833, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.01, + oneDayPriceChange: 0.01, + oneHourPriceChange: -0.005, + lastTradePrice: 0.21, + bestBid: 0.19, + bestAsk: 0.2, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'moneyline', + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-15T15:00:33.460383Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + ], + tags: [ + { + id: '1', + label: 'Sports', + slug: 'sports', + forceShow: false, + publishedAt: '2023-10-24 22:37:50.296+00', + updatedBy: 15, + createdAt: '2023-10-24T22:37:50.31Z', + updatedAt: '2024-07-05T21:07:21.800664Z', + forceHide: true, + }, + { + id: '745', + label: 'NBA', + slug: 'nba', + forceShow: false, + publishedAt: '2023-12-18 18:24:38.08+00', + createdAt: '2023-12-18T18:24:38.098Z', + updatedAt: '2024-06-18T14:52:57.582861Z', + }, + { + id: '100639', + label: 'Games', + slug: 'games', + forceShow: false, + createdAt: '2024-09-23T22:41:37.670714Z', + }, + { + id: '28', + label: 'Basketball', + slug: 'basketball', + forceShow: false, + publishedAt: '2023-11-02 21:04:25.152+00', + createdAt: '2023-11-02T21:04:25.158Z', + updatedAt: '2024-07-26T21:06:45.637044Z', + }, + ], + cyom: false, + showAllOutcomes: true, + showMarketImages: false, + enableNegRisk: false, + automaticallyActive: true, + eventDate: '2025-11-21', + startTime: '2025-11-22T00:30:00Z', + eventWeek: 3, + seriesSlug: 'nba-2026', + negRiskAugmented: false, + pendingDeployment: false, + deploying: false, + gameId: 20022514, + homeTeamName: 'Celtics', + awayTeamName: 'Nets', + }, { id: '23656', ticker: 'super-bowl-champion-2026-731', diff --git a/e2e/api-mocking/mock-responses/polymarket/polymarket-activity-response.ts b/e2e/api-mocking/mock-responses/polymarket/polymarket-activity-response.ts index 13bbd7b96fc4..0bd047f82fb5 100644 --- a/e2e/api-mocking/mock-responses/polymarket/polymarket-activity-response.ts +++ b/e2e/api-mocking/mock-responses/polymarket/polymarket-activity-response.ts @@ -399,3 +399,31 @@ export const POLYMARKET_CLAIMED_POSITIONS_ACTIVITY_RESPONSE = [ profileImageOptimized: '', }, ]; +export const POLYMARKET_OPENED_POSITION_ACTIVITY_RESPONSE = [ + { + proxyWallet: PROXY_WALLET_ADDRESS, + timestamp: Math.floor(Date.now() / 1000), // Current timestamp + conditionId: + '0x81daa857b8fa34cd3627c8cdbe5d92ea98756bcbe1e5cfcfffb94754e4d5ed86', + type: 'TRADE', + size: 11.904758, // Shares received + usdcSize: 10, // Amount spent + transactionHash: + '0x6a14089acbb670682a700ba57e10c9b1f46d188ae8eebd75cd9c62ec9ad06f8d', + price: 0.84, // Price per share + asset: + '51851880223290407825872150827934296608070009371891114025629582819868766043137', + side: 'BUY', + outcomeIndex: 0, + title: 'Celtics vs. Nets', + slug: 'nba-bos-bkn-2025-11-18', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + eventSlug: 'nba-bos-bkn-2025-11-18', + outcome: 'Celtics', + name: 'cropMaster', + pseudonym: 'cropMaster', + bio: '', + profileImage: '', + profileImageOptimized: '', + }, +]; diff --git a/e2e/api-mocking/mock-responses/polymarket/polymarket-constants.ts b/e2e/api-mocking/mock-responses/polymarket/polymarket-constants.ts index 6543b9a6b0c4..2e03c5f2c313 100644 --- a/e2e/api-mocking/mock-responses/polymarket/polymarket-constants.ts +++ b/e2e/api-mocking/mock-responses/polymarket/polymarket-constants.ts @@ -19,6 +19,11 @@ export const POST_CLAIM_USDC_BALANCE_WEI = export const POST_CASH_OUT_USDC_BALANCE_WEI = '0x00000000000000000000000000000000000000000000000000000000037f14a0'; // 58.66 USDC + +// Post-open-position USDC balance (17.76 USDC = 17,760,000 = 0x10eff00) +// Base balance (28.16) - investment (10.00) - fees (~0.40) = 17.76 USDC +export const POST_OPEN_POSITION_USDC_BALANCE_WEI = + '0x00000000000000000000000000000000000000000000000000000000010eff00'; // 17,760,000 in hex // Mock contract addresses export const SAFE_FACTORY_ADDRESS = '0xaacfeea03eb1561c4e67d661e40682bd20e3541b'; diff --git a/e2e/api-mocking/mock-responses/polymarket/polymarket-event-details-response.ts b/e2e/api-mocking/mock-responses/polymarket/polymarket-event-details-response.ts index 931c09e3b1b3..5d6061274f4f 100644 --- a/e2e/api-mocking/mock-responses/polymarket/polymarket-event-details-response.ts +++ b/e2e/api-mocking/mock-responses/polymarket/polymarket-event-details-response.ts @@ -609,3 +609,403 @@ export const POLYMARKET_EVENT_DETAILS_SPURS_PELICANS_RESPONSE = { homeTeamName: 'Pelicans', awayTeamName: 'Spurs', }; + +/** + * Mock response data for Polymarket event details API endpoint + * Event ID: 79682 - Celtics vs. Nets + */ +export const POLYMARKET_EVENT_DETAILS_CELTICS_NETS_RESPONSE = { + id: '79682', + ticker: 'nba-bos-bkn-2025-11-18', + slug: 'nba-bos-bkn-2025-11-18', + title: 'Celtics vs. Nets', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30PM ET:\nIf the Celtics win, the market will resolve to "Celtics".\nIf the Nets win, the market will resolve to "Nets".\nIf the game is postponed, this market will remain open until the game has been completed.\nIf the game is canceled entirely, with no make-up game, this market will resolve 50-50.\nThe result will be determined based on the final score including any overtime periods.', + resolutionSource: 'https://www.nba.com/', + startDate: '2025-11-12T15:04:44.432208Z', + creationDate: '2025-11-19T00:30:00Z', + endDate: '2025-11-19T00:30:00Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + active: true, + closed: false, + archived: false, + new: false, + featured: false, + restricted: true, + liquidity: 257627.355, + volume: 344942.069042, + openInterest: 0, + createdAt: '2025-11-12T15:00:10.347325Z', + updatedAt: '2025-11-18T21:52:16.333732Z', + competitive: 0.8990986535997662, + volume24hr: 200715.9993420002, + volume1wk: 218125.7573520001, + volume1mo: 218125.7573520001, + volume1yr: 218125.7573520001, + enableOrderBook: true, + liquidityClob: 257627.355, + negRisk: false, + commentCount: 0, + markets: [ + { + id: '678176', + question: 'Celtics vs. Nets', + conditionId: + '0x81daa857b8fa34cd3627c8cdbe5d92ea98756bcbe1e5cfcfffb94754e4d5ed86', + slug: 'nba-bos-bkn-2025-11-18', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '257627.355', + startDate: '2025-11-12T15:02:01.40148Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30PM ET:\nIf the Celtics win, the market will resolve to "Celtics".\nIf the Nets win, the market will resolve to "Nets".\nIf the game is postponed, this market will remain open until the game has been completed.\nIf the game is canceled entirely, with no make-up game, this market will resolve 50-50.\nThe result will be determined based on the final score including any overtime periods.', + outcomes: '["Celtics", "Nets"]', + outcomePrices: '["0.84", "0.17"]', + volume: '344942.069042', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-12T15:00:10.351643Z', + updatedAt: '2025-11-18T21:51:18.360627Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemThreshold: '0', + questionID: + '0xb8289b9c070d9c819ef7d648cbce30a1b6561a4a28d0b34e8f8de637111a833a', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 344942.069042, + liquidityNum: 257627.355, + endDateIso: '2025-11-19', + startDateIso: '2025-11-12', + hasReviewedDates: true, + volume24hr: 200715.9993420002, + volume1wk: 218125.7573520001, + volume1mo: 218125.7573520001, + volume1yr: 218125.7573520001, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["51851880223290407825872150827934296608070009371891114025629582819868766043137", "51090123154876409384652748958994213129207000557350215937559106819875795938227"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 200715.9993420002, + volume1wkClob: 218125.7573520001, + volume1moClob: 218125.7573520001, + volume1yrClob: 218125.7573520001, + volumeClob: 344942.069042, + liquidityClob: 257627.355, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-12T15:01:39Z', + cyom: false, + competitive: 0.8990986535997662, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.01, + oneDayPriceChange: -0.015, + oneHourPriceChange: -0.005, + lastTradePrice: 0.84, + bestBid: 0.83, + bestAsk: 0.84, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'moneyline', + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-12T15:00:29.791763Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '689512', + question: 'Spread: Celtics (-11.5)', + conditionId: + '0x613c9f81a0d81ea82dc9ea86a9e304fbe799144d635d0f28a2e19bcfe611992e', + slug: 'nba-bos-bkn-2025-11-18-spread-away-11pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '43363.2438', + startDate: '2025-11-18T17:22:14.693441Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Celtics" if the Celtics win the game by 12 or more points.\n\nOtherwise, this market will resolve to "Nets". If the game ends in a tie, this market will resolve to "Nets".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Celtics", "Nets"]', + outcomePrices: '["0.48", "0.52"]', + volume: '805.107158', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T17:10:19.321801Z', + updatedAt: '2025-11-18T23:44:51.829053Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'Spread -11.5', + groupItemThreshold: '1', + questionID: + '0xe5457dd19365531506edff95ca0fa4c310a86650cf0727b0e6994b775e788860', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 805.107158, + liquidityNum: 43363.2438, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["110508943375323235088649629392596121572557723445156378117896978939545868157193", "60557095252490039251801883872812055334181367912488898087280078177857052043210"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 0, + volume1wkClob: 0, + volume1moClob: 0, + volume1yrClob: 0, + volumeClob: 805.107158, + liquidityClob: 43363.2438, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T17:21:53Z', + cyom: false, + competitive: 0.9996001599360256, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.02, + oneHourPriceChange: 0.01, + lastTradePrice: 0.48, + bestBid: 0.47, + bestAsk: 0.49, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'spreads', + line: 11.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T17:10:35.936242Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + { + id: '689826', + question: 'Celtics vs. Nets: O/U 221.5', + conditionId: + '0xd6fa8571dbfb69007cc05a1b95caf30ce5458aa70e816d8ebd6089bd51a52c5a', + slug: 'nba-bos-bkn-2025-11-18-total-221pt5', + resolutionSource: 'https://www.nba.com/', + endDate: '2025-11-19T00:30:00Z', + liquidity: '19881.6956', + startDate: '2025-11-18T18:11:28.860564Z', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + description: + 'In the upcoming NBA game, scheduled for November 18 at 7:30 PM ET:\n\nThis market will resolve to "Over" if the Celtics and Nets combine to score 222 or more points in this game.\n\nIf the combined total is less than 222, this market will resolve to "Under".\n\nIf the game is postponed, this market will remain open until the game has been completed. If the game is canceled entirely, with no make-up game, this market will resolve 50-50.', + outcomes: '["Over", "Under"]', + outcomePrices: '["0.545", "0.455"]', + volume: '1729.722825', + active: true, + closed: false, + marketMakerAddress: '', + createdAt: '2025-11-18T18:10:18.666997Z', + updatedAt: '2025-11-18T23:45:29.056618Z', + new: false, + featured: false, + submitted_by: '0x91430CaD2d3975766499717fA0D66A78D814E5c5', + archived: false, + resolvedBy: '0x65070BE91477460D8A7AeEb94ef92fe056C2f2A7', + restricted: true, + groupItemTitle: 'O/U 221.5', + groupItemThreshold: '2', + questionID: + '0x591dbd052803740e216f6008ad14d7a53e9041c1c617d5cb8ba1a5fd6670a3ec', + enableOrderBook: true, + orderPriceMinTickSize: 0.01, + orderMinSize: 5, + volumeNum: 1729.722825, + liquidityNum: 19881.6956, + endDateIso: '2025-11-19', + startDateIso: '2025-11-18', + hasReviewedDates: true, + gameStartTime: '2025-11-19 00:30:00+00', + secondsDelay: 3, + clobTokenIds: + '["832422272837128527633855122000131439570281100276352454929937152326590055020", "5979089740070234605187413316264690235725412472413817808042163368309104259068"]', + umaBond: '500', + umaReward: '2', + volume24hrClob: 0, + volume1wkClob: 0, + volume1moClob: 0, + volume1yrClob: 0, + volumeClob: 1729.722825, + liquidityClob: 19881.6956, + customLiveness: 0, + acceptingOrders: true, + negRisk: false, + negRiskRequestID: '', + ready: false, + funded: false, + acceptingOrdersTimestamp: '2025-11-18T18:11:07Z', + cyom: false, + competitive: 0.9979790923380155, + pagerDutyNotificationEnabled: false, + approved: true, + rewardsMinSize: 0, + rewardsMaxSpread: 0, + spread: 0.03, + oneHourPriceChange: 0.005, + lastTradePrice: 0.56, + bestBid: 0.53, + bestAsk: 0.56, + automaticallyActive: true, + clearBookOnStart: true, + manualActivation: false, + negRiskOther: false, + sportsMarketType: 'totals', + line: 221.5, + umaResolutionStatuses: '[]', + pendingDeployment: false, + deploying: false, + deployingTimestamp: '2025-11-18T18:10:36.541917Z', + rfqEnabled: false, + holdingRewardsEnabled: false, + feesEnabled: false, + }, + ], + outcomes: [ + { + id: 'Celtics', + title: 'Celtics', + price: 0.84, + tokens: [ + { + id: '51851880223290407825872150827934296608070009371891114025629582819868766043137', + price: 0.84, + }, + ], + }, + { + id: 'Nets', + title: 'Nets', + price: 0.17, + tokens: [ + { + id: '51090123154876409384652748958994213129207000557350215937559106819875795938227', + price: 0.17, + }, + ], + }, + ], + series: [ + { + id: '10345', + ticker: 'nba-2026', + slug: 'nba-2026', + title: 'NBA 2026', + seriesType: 'single', + recurrence: 'daily', + image: + 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + active: true, + closed: false, + archived: false, + featured: false, + restricted: true, + createdAt: '2025-10-02T17:23:18.780864Z', + updatedAt: '2025-11-18T21:52:23.126366Z', + volume: 6345091.541381, + liquidity: 3154734.2218, + commentCount: 1129, + }, + ], + tags: [ + { + id: '1', + label: 'Sports', + slug: 'sports', + forceShow: false, + publishedAt: '2023-10-24 22:37:50.296+00', + updatedBy: 15, + createdAt: '2023-10-24T22:37:50.31Z', + updatedAt: '2024-07-05T21:07:21.800664Z', + forceHide: true, + }, + { + id: '745', + label: 'NBA', + slug: 'nba', + forceShow: false, + publishedAt: '2023-12-18 18:24:38.08+00', + createdAt: '2023-12-18T18:24:38.098Z', + updatedAt: '2024-06-18T14:52:57.582861Z', + }, + { + id: '100639', + label: 'Games', + slug: 'games', + forceShow: false, + createdAt: '2024-09-23T22:41:37.670714Z', + }, + { + id: '28', + label: 'Basketball', + slug: 'basketball', + forceShow: false, + publishedAt: '2023-11-02 21:04:25.152+00', + createdAt: '2023-11-02T21:04:25.158Z', + updatedAt: '2024-07-26T21:06:45.637044Z', + }, + ], + cyom: false, + showAllOutcomes: true, + showMarketImages: false, + enableNegRisk: false, + automaticallyActive: true, + eventDate: '2025-11-18', + startTime: '2025-11-19T00:30:00Z', + eventWeek: 3, + seriesSlug: 'nba-2026', + negRiskAugmented: false, + pendingDeployment: false, + deploying: false, + gameId: 20022715, + homeTeamName: 'Nets', + awayTeamName: 'Celtics', +}; diff --git a/e2e/api-mocking/mock-responses/polymarket/polymarket-mocks.ts b/e2e/api-mocking/mock-responses/polymarket/polymarket-mocks.ts index 8d8581975971..74804f629c2f 100644 --- a/e2e/api-mocking/mock-responses/polymarket/polymarket-mocks.ts +++ b/e2e/api-mocking/mock-responses/polymarket/polymarket-mocks.ts @@ -8,15 +8,18 @@ import { POLYMARKET_CURRENT_POSITIONS_RESPONSE, POLYMARKET_RESOLVED_LOST_POSITIONS_RESPONSE, POLYMARKET_WINNING_POSITIONS_RESPONSE, + POLYMARKET_NEW_OPEN_POSITION_CELTICS_NETS_RESPONSE, } from './polymarket-positions-response'; import { POLYMARKET_EVENT_DETAILS_BLUE_JAYS_MARINERS_RESPONSE, POLYMARKET_EVENT_DETAILS_SPURS_PELICANS_RESPONSE, + POLYMARKET_EVENT_DETAILS_CELTICS_NETS_RESPONSE, } from './polymarket-event-details-response'; import { POLYMARKET_UPNL_RESPONSE } from './polymarket-upnl-response'; import { POLYMARKET_ACTIVITY_RESPONSE, POLYMARKET_CLAIMED_POSITIONS_ACTIVITY_RESPONSE, + POLYMARKET_OPENED_POSITION_ACTIVITY_RESPONSE, } from './polymarket-activity-response'; import { POLYMARKET_ORDER_BOOK_RESPONSE, @@ -26,6 +29,7 @@ import { POLYMARKET_BILLS_ORDER_BOOK_RESPONSE, POLYMARKET_SPURS_ORDER_BOOK_RESPONSE, POLYMARKET_PELICANS_ORDER_BOOK_RESPONSE, + POLYMARKET_CELTICS_ORDER_BOOK_RESPONSE, } from './polymarket-order-book-response'; import { POLYMARKET_SPORTS_FEED } from './market-feed-responses/polymarket-sports-feed'; import { POLYMARKET_CRYPTO_FEED } from './market-feed-responses/polymarket-crypto-feed'; @@ -42,6 +46,7 @@ import { CONDITIONAL_TOKENS_CONTRACT_ADDRESS, POST_CASH_OUT_USDC_BALANCE_WEI, POST_CLAIM_USDC_BALANCE_WEI, + POST_OPEN_POSITION_USDC_BALANCE_WEI, POLYGON_EIP7702_CONTRACT_ADDRESS, EIP7702_CODE_FORMAT, } from './polymarket-constants'; @@ -55,6 +60,9 @@ import { createTransactionSentinelResponse } from './polymarket-transaction-sent // Global variable to track current USDC balance let currentUSDCBalance = MOCK_RPC_RESPONSES.USDC_BALANCE_RESULT; +// Global Set to track when Celtics vs Nets orders have been submitted +const celticsOrderSubmitted = new Set(); + /** * Mock Priority System * Higher numbers = checked first (higher priority) @@ -153,10 +161,7 @@ export const POLYMARKET_EVENT_DETAILS_MOCKS = async (mockServer: Mockttp) => { .forGet('/proxy') .matching((request) => { const url = new URL(request.url).searchParams.get('url'); - return Boolean( - url && - /^https:\/\/gamma-api\.polymarket\.com\/events\/[0-9]+$/.test(url), - ); + return Boolean(url?.includes('gamma-api.polymarket.com/events/')); }) .thenCallback((request) => { const url = new URL(request.url).searchParams.get('url'); @@ -171,6 +176,14 @@ export const POLYMARKET_EVENT_DETAILS_MOCKS = async (mockServer: Mockttp) => { }; } + if (eventId === '79682') { + // Return Celtics vs Nets event details from mock response file + return { + statusCode: 200, + json: POLYMARKET_EVENT_DETAILS_CELTICS_NETS_RESPONSE, + }; + } + // Default to Blue Jays vs Mariners for other event IDs return { statusCode: 200, @@ -192,9 +205,8 @@ export const POLYMARKET_CURRENT_POSITIONS_MOCKS = async ( const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/positions\?.*user=0x[a-fA-F0-9]{40}.*$/.test( - url, - ), + url.includes('data-api.polymarket.com/positions') && + url.includes('user=0x'), ); }) .asPriority(PRIORITY.BASE) @@ -245,9 +257,8 @@ export const POLYMARKET_POSITIONS_WITH_WINNINGS_MOCKS = async ( const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/positions\?.*user=0x[a-fA-F0-9]{40}.*$/.test( - url, - ) && + url.includes('data-api.polymarket.com/positions') && + url.includes('user=0x') && !url.includes('redeemable=true'), ); }) @@ -288,9 +299,8 @@ export const POLYMARKET_POSITIONS_WITH_WINNINGS_MOCKS = async ( const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/positions\?.*user=0x[a-fA-F0-9]{40}.*$/.test( - url, - ) && + url.includes('data-api.polymarket.com/positions') && + url.includes('user=0x') && url.includes('redeemable=true'), ); }) @@ -388,7 +398,6 @@ export const POLYMARKET_PRICES_MOCKS = async (mockServer: Mockttp) => { '110743925263777693447488608878982152642205002490046349037358337248548507433643' ) { // Best ask (BUY) = 0.62, Best bid (SELL) = 0.61 - // Using mid price for display: (0.62 + 0.61) / 2 = 0.615, but for accuracy use best ask for BUY and best bid for SELL pricesResponse[tokenId] = { BUY: '0.62', // Best ask - what you'd pay to buy SELL: '0.61', // Best bid - what you'd receive to sell @@ -405,6 +414,29 @@ export const POLYMARKET_PRICES_MOCKS = async (mockServer: Mockttp) => { SELL: '0.37', // Best bid - what you'd receive to sell }; } + // Celtics token (Celtics vs Nets market) + else if ( + tokenId === + '51851880223290407825872150827934296608070009371891114025629582819868766043137' + ) { + // Best ask (BUY) = 0.84, Best bid (SELL) = 0.83 (from HAR file) + pricesResponse[tokenId] = { + BUY: '0.84', // Best ask - what you'd pay to buy + SELL: '0.83', // Best bid - what you'd receive to sell + }; + } + // Nets token (Celtics vs Nets market) + else if ( + tokenId === + '51090123154876409384652748958994213129207000557350215937559106819875795938227' + ) { + // Best ask (BUY) = 0.17, Best bid (SELL) = 0.17 + // The app displays the SELL price (entry.sell), so both should be 0.17 to show 17¢ + pricesResponse[tokenId] = { + BUY: '0.17', // Best ask - what you'd pay to buy + SELL: '0.17', // Best bid - what you'd receive to sell (this is what's displayed) + }; + } // Default prices for other tokens (can be extended as needed) else { pricesResponse[tokenId] = { @@ -432,7 +464,8 @@ export const POLYMARKET_ORDER_BOOK_MOCKS = async (mockServer: Mockttp) => { const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/clob\.polymarket\.com\/book\?token_id=\d+$/.test(url), + url.includes('clob.polymarket.com/book') && + url.includes('token_id='), ); }) .asPriority(PRIORITY.BASE) @@ -486,6 +519,12 @@ export const POLYMARKET_ORDER_BOOK_MOCKS = async (mockServer: Mockttp) => { ) { // Pelicans token orderBookResponse = POLYMARKET_PELICANS_ORDER_BOOK_RESPONSE; + } else if ( + tokenId === + '51851880223290407825872150827934296608070009371891114025629582819868766043137' + ) { + // Celtics token (Celtics vs Nets) + orderBookResponse = POLYMARKET_CELTICS_ORDER_BOOK_RESPONSE; } else { // Default to 76ers for unknown token IDs orderBookResponse = POLYMARKET_ORDER_BOOK_RESPONSE; @@ -557,9 +596,8 @@ export const POLYMARKET_ACTIVITY_MOCKS = async (mockServer: Mockttp) => { const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/activity\?.*user=0x[a-fA-F0-9]{40}/.test( - url, - ), + url.includes('data-api.polymarket.com/activity') && + url.includes('user=0x'), ); }) .asPriority(PRIORITY.BASE) @@ -591,9 +629,8 @@ export const POLYMARKET_UPNL_MOCKS = async (mockServer: Mockttp) => { const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/upnl\?user=0x[a-fA-F0-9]{40}$/.test( - url, - ), + url.includes('data-api.polymarket.com/upnl') && + url.includes('user=0x'), ); }) .asPriority(PRIORITY.BASE) @@ -690,8 +727,19 @@ export const POLYMARKET_USDC_BALANCE_MOCKS = async ( } else if ( toAddress?.toLowerCase() === USDC_CONTRACT_ADDRESS.toLowerCase() ) { - // USDC contract call - return current global balance - result = currentUSDCBalance; + // USDC contract call - check function selector + if (callData?.toLowerCase()?.startsWith('0x70a08231')) { + // balanceOf(address) selector - return current global balance + result = currentUSDCBalance; + } else if (callData?.toLowerCase()?.startsWith('0xdd62ed3e')) { + // allowance(address,address) selector - return max allowance (uint256 max) + // This indicates full allowance is granted + result = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + } else { + // Other USDC contract calls - return current global balance as fallback + result = currentUSDCBalance; + } } else if ( toAddress?.toLowerCase() === MULTICALL_CONTRACT_ADDRESS.toLowerCase() ) { @@ -781,8 +829,7 @@ export const POLYMARKET_MARKET_FEEDS_MOCKS = async (mockServer: Mockttp) => { .matching((request) => { const url = new URL(request.url).searchParams.get('url'); return Boolean( - url && - /^https:\/\/gamma-api\.polymarket\.com\/events\/pagination/.test(url), + url?.includes('gamma-api.polymarket.com/events/pagination'), ); }) .asPriority(PRIORITY.BASE) @@ -839,9 +886,7 @@ export const POLYMARKET_MARKET_FEEDS_MOCKS = async (mockServer: Mockttp) => { .forGet('/proxy') .matching((request) => { const url = new URL(request.url).searchParams.get('url'); - return Boolean( - url && /^https:\/\/gamma-api\.polymarket\.com\/public-search/.test(url), - ); + return Boolean(url?.includes('gamma-api.polymarket.com/public-search')); }) .asPriority(PRIORITY.BASE) .thenCallback(() => ({ @@ -904,6 +949,152 @@ export const POLYMARKET_TRANSACTION_SENTINEL_MOCKS = async ( } }); }; + +/** + * Mock for adding Celtics vs Nets position to positions list after order is submitted + * This override adds the Celtics vs Nets position only after the open position flow is completed + * + * Mocks endpoint: https://data-api.polymarket.com/positions?limit=100&offset=0&user=...&sortBy=CURRENT&redeemable=false&eventId=79682 + * - Always uses PROXY_WALLET_ADDRESS for the proxyWallet field (regardless of user in URL) + * - When eventId=79682 (Celtics vs Nets), returns only the Celtics position + * - When no eventId, returns all positions including Celtics (if order was submitted) + * - Also mocks the position appearing in the main positions list on the predict page + * + * @param mockServer - The mockttp server instance + */ +export const POLYMARKET_ADD_CELTICS_POSITION_MOCKS = async ( + mockServer: Mockttp, +) => { + await mockServer + .forGet('/proxy') + .matching((request) => { + const url = new URL(request.url).searchParams.get('url'); + return Boolean( + url && + url.includes('data-api.polymarket.com/positions') && + url.includes('user=0x') && + !url.includes('redeemable=true'), + ); + }) + .asPriority(PRIORITY.API_OVERRIDE) // Higher priority to override the base positions mock + .thenCallback((request) => { + const url = new URL(request.url).searchParams.get('url'); + const eventIdMatch = url?.match(/eventId=([0-9]+)/); + const eventId = eventIdMatch ? eventIdMatch[1] : null; + + // Check if Celtics vs Nets order has been submitted + const proxyAddressLower = PROXY_WALLET_ADDRESS.toLowerCase(); + const celticsOrderSubmittedForProxy = + celticsOrderSubmitted.has(proxyAddressLower); + + // If eventId=79682 (Celtics vs Nets), return only the Celtics position + if (eventId === '79682') { + if (!celticsOrderSubmittedForProxy) { + // Return empty array if order hasn't been submitted yet + return { + statusCode: 200, + json: [], + }; + } + + // Return Celtics vs Nets position with PROXY_WALLET_ADDRESS + const dynamicResponse = + POLYMARKET_NEW_OPEN_POSITION_CELTICS_NETS_RESPONSE.map( + (position) => ({ + ...position, + proxyWallet: PROXY_WALLET_ADDRESS, + }), + ); + + return { + statusCode: 200, + json: dynamicResponse, + }; + } + + // For main positions list (no eventId filter), combine existing positions with Celtics position + // only if Celtics order was submitted. Put Celtics position at the top of the list. + let allPositions = [...POLYMARKET_CURRENT_POSITIONS_RESPONSE]; + if (celticsOrderSubmittedForProxy) { + allPositions = [ + ...POLYMARKET_NEW_OPEN_POSITION_CELTICS_NETS_RESPONSE, + ...POLYMARKET_CURRENT_POSITIONS_RESPONSE, + ]; + } + + // Filter positions by eventId if provided (for other eventIds) + let filteredPositions = allPositions; + if (eventId) { + filteredPositions = allPositions.filter( + (position) => position.eventId === eventId, + ); + } + + // Always use PROXY_WALLET_ADDRESS for proxyWallet field + const dynamicResponse = filteredPositions.map((position) => ({ + ...position, + proxyWallet: PROXY_WALLET_ADDRESS, + })); + + return { + statusCode: 200, + json: dynamicResponse, + }; + }); +}; + +/** + * Mock for adding Celtics vs Nets activity entry to activity list after order is submitted + * This override adds the Celtics vs Nets BUY activity only after the open position flow is completed + * @param mockServer - The mockttp server instance + */ +export const POLYMARKET_ADD_CELTICS_ACTIVITY_MOCKS = async ( + mockServer: Mockttp, +) => { + await mockServer + .forGet('/proxy') + .matching((request) => { + const url = new URL(request.url).searchParams.get('url'); + return Boolean( + url && + url.includes('data-api.polymarket.com/activity') && + url.includes('user=0x'), + ); + }) + .asPriority(PRIORITY.API_OVERRIDE) // Higher priority to override the base activity mock + .thenCallback((request) => { + const url = new URL(request.url).searchParams.get('url'); + const userMatch = url?.match(/user=(0x[a-fA-F0-9]{40})/); + const userAddress = userMatch ? userMatch[1] : USER_WALLET_ADDRESS; + + // Check if Celtics vs Nets order has been submitted + const proxyAddressLower = PROXY_WALLET_ADDRESS.toLowerCase(); + const celticsOrderSubmittedForProxy = + celticsOrderSubmitted.has(proxyAddressLower); + + // Combine existing activity with Celtics activity only if Celtics order was submitted + // Put Celtics activity at the top (most recent first) + let allActivity = [...POLYMARKET_ACTIVITY_RESPONSE]; + if (celticsOrderSubmittedForProxy) { + allActivity = [ + ...POLYMARKET_OPENED_POSITION_ACTIVITY_RESPONSE, + ...POLYMARKET_ACTIVITY_RESPONSE, + ]; + } + + // Update the mock response with the actual user address + const dynamicResponse = allActivity.map((activity) => ({ + ...activity, + proxyWallet: userAddress, + })); + + return { + statusCode: 200, + json: dynamicResponse, + }; + }); +}; + /** * Sets up mocks for USDC balance refresh calls after claim or cash-out operations * This mock should be triggered after claim/cash-out transactions to update the displayed balance @@ -925,6 +1116,8 @@ export const POLYMARKET_UPDATE_USDC_BALANCE_MOCKS = async ( balance = POST_CLAIM_USDC_BALANCE_WEI; // 48.16 USDC } else if (positionType === 'cash-out') { balance = POST_CASH_OUT_USDC_BALANCE_WEI; // 58.66 USDC + } else if (positionType === 'open-position') { + balance = POST_OPEN_POSITION_USDC_BALANCE_WEI; // 17.76 USDC } else { throw new Error(`Unknown positionType: ${positionType}`); } @@ -947,9 +1140,20 @@ export const POLYMARKET_UPDATE_USDC_BALANCE_MOCKS = async ( // Handle USDC balance calls if (body?.method === 'eth_call') { const toAddress = body?.params?.[0]?.to?.toLowerCase(); + const callData = body?.params?.[0]?.data; if (toAddress === USDC_CONTRACT_ADDRESS.toLowerCase()) { - // USDC contract call - return updated balance - result = balance; + // USDC contract call - check function selector + if (callData?.toLowerCase()?.startsWith('0x70a08231')) { + // balanceOf(address) selector - return updated balance + result = balance; + } else if (callData?.toLowerCase()?.startsWith('0xdd62ed3e')) { + // allowance(address,address) selector - return max allowance (uint256 max) + result = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + } else { + // Other USDC contract calls - return updated balance as fallback + result = balance; + } } else { // For other eth_call, return empty result (let base mocks handle if needed) result = MOCK_RPC_RESPONSES.EMPTY_RESULT; @@ -995,9 +1199,11 @@ export const POLYMARKET_POST_CASH_OUT_MOCKS = async (mockServer: Mockttp) => { .matching(async (request) => { try { const urlParam = new URL(request.url).searchParams.get('url'); - const relayerEndpointPattern = - /predict\.(dev-)?api\.cx\.metamask\.io\/order/; - if (!urlParam || !relayerEndpointPattern.test(urlParam)) { + if ( + !urlParam || + !urlParam.includes('predict.') || + !urlParam.includes('api.cx.metamask.io/order') + ) { return false; } @@ -1079,6 +1285,242 @@ export const POLYMARKET_POST_CASH_OUT_MOCKS = async (mockServer: Mockttp) => { await POLYMARKET_UPDATE_USDC_BALANCE_MOCKS(mockServer, 'cash-out'); }; +/** + * Mocks for opening a position (BUY order) and balance update + * This mock should be triggered before placing the order + * - Mocks the MetaMask relayer endpoint (predict.dev-api.cx.metamask.io/order) + * - Updates global USDC balance to post-open-position amount (18.11 USDC) + * - Adds position and activity only AFTER the order is successfully submitted + * Note: Celtics vs Nets is available in the sports feed, so no search mock is needed + * @param mockServer - The mockttp server instance + */ +export const POLYMARKET_POST_OPEN_POSITION_MOCKS = async ( + mockServer: Mockttp, +) => { + // Track whether the order has been successfully submitted + // This ensures the position only appears AFTER the order is placed + const orderSubmitted = new Set(); + + // Mock MetaMask relayer endpoint for order submission (BUY orders) + // In e2e, all requests go through /proxy with the actual URL in the url query parameter + // Matches request payload structure with PROXY_WALLET_ADDRESS as maker and USER_WALLET_ADDRESS as signer + // Response uses decimal string format (not wei) + // Uses flexible matching: requires BUY order to relayer endpoint, with optional strict field validation + await mockServer + .forPost('/proxy') + .matching(async (request) => { + try { + const urlParam = new URL(request.url).searchParams.get('url'); + if ( + !urlParam || + !urlParam.includes('predict.') || + !urlParam.includes('api.cx.metamask.io/order') + ) { + return false; + } + + const bodyText = await request.body.getText(); + const body = bodyText ? JSON.parse(bodyText) : {}; + const order = body?.order; + + // Flexible matching: require BUY order to relayer endpoint + // Validates key fields when present, but doesn't require all fields to match strict pattern + // This handles both well-formed orders and edge cases with missing/optional fields + if (!order || order.side !== 'BUY') { + return false; + } + + // Validate orderType if present (should be FOK for open positions) + if (body.orderType !== undefined && body.orderType !== 'FOK') { + return false; + } + + // Validate addresses if present (should match expected addresses for open positions) + if (order.maker !== undefined && order.signer !== undefined) { + const makerMatch = + order.maker?.toLowerCase() === PROXY_WALLET_ADDRESS.toLowerCase(); + const signerMatch = + order.signer?.toLowerCase() === USER_WALLET_ADDRESS.toLowerCase(); + if (!makerMatch || !signerMatch) { + return false; + } + } + + // If order has signature field, validate it's a valid signature format + if (order.signature !== undefined) { + if ( + typeof order.signature !== 'string' || + !order.signature.startsWith('0x') || + order.signature.length < 10 + ) { + return false; + } + } + + return true; + } catch { + return false; + } + }) + .asPriority(PRIORITY.API_OVERRIDE) + .thenCallback(async (request) => { + try { + const bodyText = await request.body.getText(); + const body = bodyText ? JSON.parse(bodyText) : {}; + const order = body?.order; + const userAddress = + order?.signer?.toLowerCase() || USER_WALLET_ADDRESS.toLowerCase(); + const proxyAddress = + order?.maker?.toLowerCase() || PROXY_WALLET_ADDRESS.toLowerCase(); + + // Check if it's a Celtics vs Nets token + const isCelticsToken = + order?.tokenId === + '51851880223290407825872150827934296608070009371891114025629582819868766043137'; + + // Track both addresses - positions/activity may use either + orderSubmitted.add(userAddress); + orderSubmitted.add(proxyAddress); + + // Track Celtics orders separately for position addition + if (isCelticsToken) { + celticsOrderSubmitted.add(userAddress); + celticsOrderSubmitted.add(proxyAddress); + } + + return { + statusCode: 200, + json: { + success: true, + errorMsg: '', + status: 'matched', + orderID: + '0x3bd7640f8ec62a31ab9f95f0b94582d3a7fb159dbaed773eb5fcca45c43bcdb9', + transactionsHashes: [ + '0x6a14089acbb670682a700ba57e10c9b1f46d188ae8eebd75cd9c62ec9ad06f8d', + ], + takingAmount: '11.904758', // Shares received for $10 investment + makingAmount: '9.999996', + }, + }; + } catch { + // Fallback response if parsing fails - still track the addresses + const userAddress = USER_WALLET_ADDRESS.toLowerCase(); + const proxyAddress = PROXY_WALLET_ADDRESS.toLowerCase(); + orderSubmitted.add(userAddress); + orderSubmitted.add(proxyAddress); + // Note: Can't check tokenId in catch block, so don't add to celticsOrderSubmitted + + return { + statusCode: 200, + json: { + success: true, + errorMsg: '', + status: 'matched', + orderID: + '0x3bd7640f8ec62a31ab9f95f0b94582d3a7fb159dbaed773eb5fcca45c43bcdb9', + transactionsHashes: [ + '0x6a14089acbb670682a700ba57e10c9b1f46d188ae8eebd75cd9c62ec9ad06f8d', + ], + takingAmount: '11.904758', + makingAmount: '9.999996', + }, + }; + } + }); + + // Mock CLOB API order endpoint (called after relayer endpoint) + // This handles both POST /order and POST /book?token_id=... endpoints + // Higher priority to ensure it catches order requests before the broad cash-out CLOB mock + await mockServer + .forPost('/proxy') + .matching((request) => { + const urlParam = new URL(request.url).searchParams.get('url'); + return Boolean( + urlParam && + (urlParam.includes('clob.polymarket.com/order') || + (urlParam.includes('clob.polymarket.com/book') && + urlParam.includes('token_id='))), + ); + }) + .asPriority(PRIORITY.API_OVERRIDE + 2) // Higher priority than cash-out CLOB mock + .thenCallback(async (request) => { + try { + const bodyText = await request.body.getText(); + const body = bodyText ? JSON.parse(bodyText) : {}; + const order = body?.order; + + // Check if it's a BUY order (for opening positions) + const isBuyOrder = order?.side === 'BUY'; + const isCelticsToken = + order?.tokenId === + '51851880223290407825872150827934296608070009371891114025629582819868766043137'; + + if (isBuyOrder) { + const userAddress = + order?.signer?.toLowerCase() || USER_WALLET_ADDRESS.toLowerCase(); + const proxyAddress = + order?.maker?.toLowerCase() || PROXY_WALLET_ADDRESS.toLowerCase(); + + // Only track Celtics vs Nets orders for positions/activity + if (isCelticsToken) { + orderSubmitted.add(userAddress); + orderSubmitted.add(proxyAddress); + celticsOrderSubmitted.add(userAddress); + celticsOrderSubmitted.add(proxyAddress); + } + + // Return success for any BUY order + // Use the amounts from the order if available, otherwise use defaults + const makingAmount = order?.makerAmount + ? (parseInt(order.makerAmount, 10) / 1000000).toString() + : '9.999996'; + const takingAmount = order?.takerAmount + ? (parseInt(order.takerAmount, 10) / 1000000).toString() + : '11.904758'; + + return { + statusCode: 200, + json: { + errorMsg: '', + orderID: + '0x3bd7640f8ec62a31ab9f95f0b94582d3a7fb159dbaed773eb5fcca45c43bcdb9', + takingAmount, // Shares received + makingAmount, // Amount spent + status: 'matched', + transactionsHashes: [ + '0x6a14089acbb670682a700ba57e10c9b1f46d188ae8eebd75cd9c62ec9ad06f8d', + ], + success: true, + }, + }; + } + + // For non-BUY orders, let other mocks handle them + return { + statusCode: 200, + json: { + success: false, + errorMsg: 'Order not matched', + }, + }; + } catch { + return { + statusCode: 200, + json: { + success: false, + errorMsg: 'Invalid request', + }, + }; + } + }); + await POLYMARKET_ADD_CELTICS_POSITION_MOCKS(mockServer); + await POLYMARKET_ADD_CELTICS_ACTIVITY_MOCKS(mockServer); + + // Update balance after opening position + // await POLYMARKET_UPDATE_USDC_BALANCE_MOCKS(mockServer, 'open-position'); +}; + /** * Dedicated mock for loading USDC balance specifically for withdraw flow * This ensures balance refresh for withdraw/deposit flows doesn't interfere with cash-out @@ -1141,9 +1583,8 @@ export const POLYMARKET_REMOVE_CLAIMED_POSITIONS_MOCKS = async ( const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/positions\?.*user=0x[a-fA-F0-9]{40}.*$/.test( - url, - ) && + url.includes('data-api.polymarket.com/positions') && + url.includes('user=0x') && url.includes('redeemable=true'), ); }) @@ -1170,9 +1611,8 @@ export const POLYMARKET_ADD_CLAIMED_POSITIONS_TO_ACTIVITY_MOCKS = async ( const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/activity\?.*user=0x[a-fA-F0-9]{40}/.test( - url, - ), + url.includes('data-api.polymarket.com/activity') && + url.includes('user=0x'), ); }) .asPriority(PRIORITY.API_OVERRIDE) // Higher priority to override the original activity mock @@ -1226,9 +1666,8 @@ export const POLYMARKET_REMOVE_CASHED_OUT_POSITION_MOCKS = async ( const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/positions\?.*user=0x[a-fA-F0-9]{40}.*$/.test( - url, - ) && + url.includes('data-api.polymarket.com/positions') && + url.includes('user=0x') && !url.includes('redeemable=true'), ); }) @@ -1274,9 +1713,8 @@ export const POLYMARKET_REMOVE_CASHED_OUT_POSITION_MOCKS = async ( const url = new URL(request.url).searchParams.get('url'); return Boolean( url && - /^https:\/\/data-api\.polymarket\.com\/activity\?.*user=0x[a-fA-F0-9]{40}/.test( - url, - ), + url.includes('data-api.polymarket.com/activity') && + url.includes('user=0x'), ); }) .asPriority(PRIORITY.API_OVERRIDE) // Higher priority to override the original activity mock diff --git a/e2e/api-mocking/mock-responses/polymarket/polymarket-order-book-response.ts b/e2e/api-mocking/mock-responses/polymarket/polymarket-order-book-response.ts index 9d69a95235bc..05c830fc32b6 100644 --- a/e2e/api-mocking/mock-responses/polymarket/polymarket-order-book-response.ts +++ b/e2e/api-mocking/mock-responses/polymarket/polymarket-order-book-response.ts @@ -158,3 +158,27 @@ export const POLYMARKET_PELICANS_ORDER_BOOK_RESPONSE = { { price: '0.43', size: '2500' }, ], }; + +// Celtics vs Nets order book (Celtics token) +// Market condition ID: 0x81daa857b8fa34cd3627c8cdbe5d92ea98756bcbe1e5cfcfffb94754e4d5ed86 +// All asks at 0.84 to ensure consistent price regardless of order size +export const POLYMARKET_CELTICS_ORDER_BOOK_RESPONSE = { + market: '0x81daa857b8fa34cd3627c8cdbe5d92ea98756bcbe1e5cfcfffb94754e4d5ed86', + asset_id: + '51851880223290407825872150827934296608070009371891114025629582819868766043137', + timestamp: '1761177638154', + hash: '97f541df6e9baa53e3583f8ebe69d06e86a2198c', + bids: [ + { price: '0.83', size: '10000' }, // Best bid for selling + { price: '0.82', size: '5000' }, + { price: '0.81', size: '3000' }, + { price: '0.80', size: '2000' }, + ], + asks: [ + { price: '0.84', size: '1000000' }, // Single price level with massive liquidity - enough for any reasonable order size + // No higher-priced asks to prevent price increases for larger orders + ], + min_order_size: '5', + tick_size: '0.01', + neg_risk: false, +}; diff --git a/e2e/api-mocking/mock-responses/polymarket/polymarket-positions-response.ts b/e2e/api-mocking/mock-responses/polymarket/polymarket-positions-response.ts index 3ac1042a036f..7befc57ba4af 100644 --- a/e2e/api-mocking/mock-responses/polymarket/polymarket-positions-response.ts +++ b/e2e/api-mocking/mock-responses/polymarket/polymarket-positions-response.ts @@ -162,6 +162,40 @@ export const POLYMARKET_CURRENT_POSITIONS_RESPONSE = [ negativeRisk: true, }, ]; + +export const POLYMARKET_NEW_OPEN_POSITION_CELTICS_NETS_RESPONSE = [ + { + proxyWallet: PROXY_WALLET_ADDRESS, + asset: + '51851880223290407825872150827934296608070009371891114025629582819868766043137', + conditionId: + '0x81daa857b8fa34cd3627c8cdbe5d92ea98756bcbe1e5cfcfffb94754e4d5ed86', + size: 11.904758, + avgPrice: 0.83, + initialValue: 10, + currentValue: 10.7142822, + cashPnl: 0.83333306, + percentPnl: 8.433734939, + totalBought: 11.904758, + realizedPnl: 0, + percentRealizedPnl: 0, + curPrice: 0.9, + redeemable: false, + mergeable: false, + title: 'Celtics vs. Nets', + slug: 'nba-bos-bkn-2025-11-18', + icon: 'https://polymarket-upload.s3.us-east-2.amazonaws.com/super+cool+basketball+in+red+and+blue+wow.png', + eventId: '79682', + eventSlug: 'nba-bos-bkn-2025-11-18', + outcome: 'Celtics', + outcomeIndex: 0, + oppositeOutcome: 'Nets', + oppositeAsset: + '51090123154876409384652748958994213129207000557350215937559106819875795938227', + endDate: '2025-11-19', + negativeRisk: false, + }, +]; /* *endpoint: /positions?user&redeemable=true This contains all lost positions in resolved markets (no winning positions) diff --git a/e2e/pages/Predict/PredictDetailsPage.ts b/e2e/pages/Predict/PredictDetailsPage.ts index 62dc405280d9..681795bfea76 100644 --- a/e2e/pages/Predict/PredictDetailsPage.ts +++ b/e2e/pages/Predict/PredictDetailsPage.ts @@ -1,6 +1,7 @@ import { Matchers, Gestures } from '../../framework'; import { PredictBalanceSelectorsIDs, + PredictBuyPreviewSelectorsIDs, PredictMarketDetailsSelectorsIDs, PredictMarketDetailsSelectorsText, } from '../../selectors/Predict/Predict.selectors'; @@ -37,6 +38,12 @@ class PredictDetailsPage { return Matchers.getElementByID(PredictBalanceSelectorsIDs.BALANCE_CARD); } + get placeBetButton(): DetoxElement { + return Matchers.getElementByID( + PredictBuyPreviewSelectorsIDs.PLACE_BET_BUTTON, + ); + } + async tapBackButton(): Promise { await Gestures.waitAndTap(this.backButton, { elemDescription: 'Back button', @@ -63,6 +70,56 @@ class PredictDetailsPage { elemDescription: 'Cash out button', }); } + + async tapOpenPositionValue(): Promise { + // Use regex to match both "Celtics\n83¢" and "Celtics • 83¢" formats + const celticsButton = (await Matchers.getElementByText( + /Celtics[\s•\n]*83¢/, + )) as unknown as DetoxElement; + + await Gestures.waitAndTap(celticsButton, { + elemDescription: 'Celtics outcome button', + }); + } + + async tapPositionAmount(amount: string): Promise { + const digits = amount.split(''); + + for (const digit of digits) { + const digitElement = (await Matchers.getElementByText( + digit, + )) as unknown as DetoxElement; + await Gestures.waitAndTap(digitElement, { + elemDescription: `tap ${digit} on keypad`, + }); + } + } + + async tapDoneButton(): Promise { + const continueButton = (await Matchers.getElementByText( + 'Done', + )) as unknown as DetoxElement; + + await Gestures.waitAndTap(continueButton, { + elemDescription: 'Done button', + }); + } + + async tapContinueButton(): Promise { + const continueButton = (await Matchers.getElementByText( + 'Continue', + )) as unknown as DetoxElement; + + await Gestures.waitAndTap(continueButton, { + elemDescription: 'Continue button', + }); + } + + async tapOpenPosition(): Promise { + await Gestures.waitAndTap(this.placeBetButton, { + elemDescription: 'Place bet button', + }); + } } export default new PredictDetailsPage(); diff --git a/e2e/pages/Predict/PredictMarketList.ts b/e2e/pages/Predict/PredictMarketList.ts index c1251ff23870..da85b5d23257 100644 --- a/e2e/pages/Predict/PredictMarketList.ts +++ b/e2e/pages/Predict/PredictMarketList.ts @@ -19,6 +19,9 @@ class PredictMarketList { get categoryTabs(): DetoxElement { return Matchers.getElementByID(PredictMarketListSelectorsIDs.CATEGORY_TABS); } + get backButton(): DetoxElement { + return Matchers.getElementByID(PredictMarketListSelectorsIDs.BACK_BUTTON); + } getMarketCard(category: CategoryTab, cardIndex: number): DetoxElement { return Matchers.getElementByID( @@ -93,6 +96,12 @@ class PredictMarketList { elemDescription: `Tap No in ${category} feed index ${cardIndex}`, }); } + + async tapBackButton(): Promise { + await Gestures.waitAndTap(this.backButton, { + elemDescription: 'Tap Back button on market feed', + }); + } } export default new PredictMarketList(); diff --git a/e2e/selectors/Predict/Predict.selectors.ts b/e2e/selectors/Predict/Predict.selectors.ts index 3916ee8cd6a6..f3bd60b363f1 100644 --- a/e2e/selectors/Predict/Predict.selectors.ts +++ b/e2e/selectors/Predict/Predict.selectors.ts @@ -30,7 +30,7 @@ export const PredictMarketListSelectorsIDs = { SPORTS_TAB: 'predict-market-list-sports-tab', CRYPTO_TAB: 'predict-market-list-crypto-tab', POLITICS_TAB: 'predict-market-list-politics-tab', - + BACK_BUTTON: 'back-button', // Empty state EMPTY_STATE: 'predict-market-list-empty-state', } as const; @@ -107,6 +107,15 @@ export const PredictPositionSelectorsIDs = { RESOLVED_POSITION_CARD: 'predict-resolved-position-card', } as const; +// ======================================== +// PREDICT BUY PREVIEW SELECTORS +// ======================================== + +export const PredictBuyPreviewSelectorsIDs = { + // Buy/Place bet button + PLACE_BET_BUTTON: 'predict-buy-preview-place-bet-button', +} as const; + // ======================================== // PREDICT CASH OUT SELECTORS // ======================================== diff --git a/e2e/specs/predict/predict-open-position.spec.ts b/e2e/specs/predict/predict-open-position.spec.ts new file mode 100644 index 000000000000..59a37382e672 --- /dev/null +++ b/e2e/specs/predict/predict-open-position.spec.ts @@ -0,0 +1,117 @@ +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { SmokePredictions } from '../../tags'; +import { loginToApp } from '../../viewHelper'; +import TabBarComponent from '../../pages/wallet/TabBarComponent'; +import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet'; +import PredictMarketList from '../../pages/Predict/PredictMarketList'; +import PredictDetailsPage from '../../pages/Predict/PredictDetailsPage'; +import Assertions from '../../framework/Assertions'; +import WalletView from '../../pages/wallet/WalletView'; +import { remoteFeatureFlagPredictEnabled } from '../../api-mocking/mock-responses/feature-flags-mocks'; +import { Mockttp } from 'mockttp'; +import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { + POLYMARKET_COMPLETE_MOCKS, + POLYMARKET_POSITIONS_WITH_WINNINGS_MOCKS, + POLYMARKET_POST_OPEN_POSITION_MOCKS, + POLYMARKET_UPDATE_USDC_BALANCE_MOCKS, +} from '../../api-mocking/mock-responses/polymarket/polymarket-mocks'; +import ActivitiesView from '../../pages/Transactions/ActivitiesView'; + +/* +Test Scenario: Open position on Celtics vs. Nets market + Verifies the open position flow for a predictions market: + 1. Navigate to Predictions tab and open market list + 2. Select Celtics vs. Nets market from sports category + 3. Open a position with $10 investment + 4. Verify position appears in Positions tab + 5. Verify balance updates to $17.76 + 6. Verify position appears in Activities tab +*/ +const positionDetails = { + name: 'Celtics vs. Nets', + positionAmount: '10', + newBalance: '$17.76', + category: 'sports' as const, + marketIndex: 1, +}; + +const PredictionMarketFeature = async (mockServer: Mockttp) => { + await setupRemoteFeatureFlagsMock( + mockServer, + remoteFeatureFlagPredictEnabled(true), + ); + await POLYMARKET_COMPLETE_MOCKS(mockServer); + await POLYMARKET_POSITIONS_WITH_WINNINGS_MOCKS(mockServer, false); // do not include winnings. Claim Button is animated and problematic for e2e +}; + +describe(SmokePredictions('Predictions'), () => { + it('opens position on Celtics vs. Nets market', async () => { + await withFixtures( + { + fixture: new FixtureBuilder().withPolygon().build(), + restartDevice: true, + testSpecificMock: PredictionMarketFeature, + }, + async ({ mockServer }) => { + await loginToApp(); + + await WalletView.tapOnPredictionsTab(); + await TabBarComponent.tapActions(); + await WalletActionsBottomSheet.tapPredictButton(); + await device.disableSynchronization(); + + await Assertions.expectElementToBeVisible(PredictMarketList.container, { + description: 'Predict market list container should be visible', + }); + + await PredictMarketList.tapCategoryTab(positionDetails.category); + await PredictMarketList.tapMarketCard( + positionDetails.category, + positionDetails.marketIndex, + ); + await PredictDetailsPage.tapOpenPositionValue(); + + await POLYMARKET_POST_OPEN_POSITION_MOCKS(mockServer); + await POLYMARKET_UPDATE_USDC_BALANCE_MOCKS(mockServer, 'open-position'); + + await PredictDetailsPage.tapPositionAmount( + positionDetails.positionAmount, + ); + await PredictDetailsPage.tapDoneButton(); + + await PredictDetailsPage.tapOpenPosition(); + await device.enableSynchronization(); + + await Assertions.expectElementToBeVisible( + PredictDetailsPage.positionsTab, + { + description: + 'Position tab should appear after opening a new position', + }, + ); + + await Assertions.expectTextDisplayed(positionDetails.name, { + description: 'Position card for Celtics vs. Nets should appear', + }); + + await PredictDetailsPage.tapBackButton(); + await Assertions.expectTextDisplayed(positionDetails.newBalance, { + description: `USDC balance should display ${positionDetails.newBalance} after opening position`, + }); + await PredictMarketList.tapBackButton(); + + // Verify position appears in current positions list on homepage + + await Assertions.expectTextDisplayed(positionDetails.name, { + description: `Position card should have text "${positionDetails.name}"`, + }); + + await TabBarComponent.tapActivity(); + await ActivitiesView.tapOnPredictionsTab(); + await ActivitiesView.tapPredictPosition(positionDetails.name); + }, + ); + }); +}); diff --git a/e2e/specs/predict/predict-select-bet.spec.ts b/e2e/specs/predict/predict-select-bet.spec.ts deleted file mode 100644 index 03b6d026eb80..000000000000 --- a/e2e/specs/predict/predict-select-bet.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { withFixtures } from '../../framework/fixtures/FixtureHelper'; -import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; -import { SmokePredictions } from '../../tags'; -import { loginToApp } from '../../viewHelper'; -import TabBarComponent from '../../pages/wallet/TabBarComponent'; -import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet'; -import PredictMarketList from '../../pages/Predict/PredictMarketList'; -import PredictDetailsPage from '../../pages/Predict/PredictDetailsPage'; -import Assertions from '../../framework/Assertions'; - -import { remoteFeatureFlagPredictEnabled } from '../../api-mocking/mock-responses/feature-flags-mocks'; -import { Mockttp } from 'mockttp'; -import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper'; - -const PredictionMarketFeature = async (mockServer: Mockttp) => { - await setupRemoteFeatureFlagsMock( - mockServer, - remoteFeatureFlagPredictEnabled(true), - ); -}; - -describe(SmokePredictions('Predictions'), () => { - it('should open predict tab and view market details', async () => { - await withFixtures( - { - fixture: new FixtureBuilder().build(), - restartDevice: true, - testSpecificMock: PredictionMarketFeature, - }, - async () => { - await loginToApp(); - - // Navigate to actions - await TabBarComponent.tapActions(); - - await WalletActionsBottomSheet.tapPredictButton(); - - await Assertions.expectElementToBeVisible(PredictMarketList.container, { - description: 'Predict market list container should be visible', - }); - await PredictMarketList.tapCategoryTab('new'); - await PredictMarketList.tapMarketCard('new', 1); - await Assertions.expectElementToBeVisible( - PredictDetailsPage.container, - { - description: 'Predict details page container should be visible', - }, - ); - }, - ); - }); -}); diff --git a/locales/languages/en.json b/locales/languages/en.json index e2958f9be35f..54f24dcaed19 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3130,7 +3130,7 @@ "show_less": "Show less" }, "activity": "{{symbol}} activity", - "disclaimer": "Market data is provided by one or more third-party data sources, including CoinGecko. Such third-party content is provided solely for informational purposes and should not be treated as advice to buy, sell, or use any particular asset. MetaMask does not suggest the use of this content for any particular purpose and is not responsible for its accuracy." + "disclaimer": "Market data is provided by third-party sources like CoinGecko. Data is for informational purposes only. MetaMask is not responsible for its accuracy." }, "account_details": { "title": "Account Details", diff --git a/package.json b/package.json index 8d6868a8b575..d93573cf1b9c 100644 --- a/package.json +++ b/package.json @@ -233,7 +233,7 @@ "@metamask/json-rpc-engine": "^10.0.3", "@metamask/json-rpc-middleware-stream": "^8.0.7", "@metamask/key-tree": "^10.1.1", - "@metamask/keyring-api": "^21.1.0", + "@metamask/keyring-api": "^21.2.0", "@metamask/keyring-controller": "^24.0.0", "@metamask/keyring-internal-api": "^9.1.0", "@metamask/keyring-snap-client": "^8.1.0", @@ -288,7 +288,7 @@ "@metamask/token-search-discovery-controller": "^4.0.0", "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.3.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch", "@metamask/transaction-pay-controller": "^10.1.0", - "@metamask/tron-wallet-snap": "^1.10.0", + "@metamask/tron-wallet-snap": "^1.12.1", "@metamask/utils": "^11.8.1", "@ngraveio/bc-ur": "^1.1.6", "@nktkas/hyperliquid": "^0.25.9", diff --git a/yarn.lock b/yarn.lock index 2cfdd0a5b63b..37ac84c03151 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7462,7 +7462,7 @@ __metadata: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A89.0.1#~/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch": version: 89.0.1 - resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A89.0.1#~/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch::version=89.0.1&hash=6be0d3" + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A89.0.1#~/.yarn/patches/@metamask-assets-controllers-npm-89.0.1-02fa7acd54.patch::version=89.0.1&hash=fa830a" dependencies: "@ethereumjs/util": "npm:^9.1.0" "@ethersproject/abi": "npm:^5.7.0" @@ -7508,7 +7508,7 @@ __metadata: "@metamask/snaps-controllers": ^14.0.0 "@metamask/transaction-controller": ^61.0.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/b936b09bc22944626b3332844070c0fab559b7e3973873cc96c8321618e6879c1ee1215a588bb1f8f38029e4a69f796d01141ad1cb0726fd590df54ca111355b + checksum: 10/0f8c82256141e95b591b0bfff17cf6015ff9e0e4b330e68e95066319d0e415320a4eca48cb6d0f3bf27f3ff68d231ea1c656aa08844bf7699624ef09cd3ed587 languageName: node linkType: hard @@ -8391,15 +8391,15 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^21.0.0, @metamask/keyring-api@npm:^21.1.0": - version: 21.1.0 - resolution: "@metamask/keyring-api@npm:21.1.0" +"@metamask/keyring-api@npm:^21.0.0, @metamask/keyring-api@npm:^21.1.0, @metamask/keyring-api@npm:^21.2.0": + version: 21.2.0 + resolution: "@metamask/keyring-api@npm:21.2.0" dependencies: "@metamask/keyring-utils": "npm:^3.1.0" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.1.0" bitcoin-address-validation: "npm:^2.2.3" - checksum: 10/3371a5ab0e9ba0e9b23b30b03a7d83d029e223def5485ab1aa2ec793ba18ff3738422dbe3c47f9cf82411d2ca6ca918928bf998741f0977055071b7bf3042314 + checksum: 10/cc3cd9f9ef65b33aa0af2f4aa556fab7ebab78ce21a09b8e1cb6f328b456c444e0169d7aac08dd62425fb12895d68dfee0ddb6e3d8a43950a8ba1852c6b92609 languageName: node linkType: hard @@ -9703,10 +9703,10 @@ __metadata: languageName: node linkType: hard -"@metamask/tron-wallet-snap@npm:^1.10.0": - version: 1.10.0 - resolution: "@metamask/tron-wallet-snap@npm:1.10.0" - checksum: 10/c9b2f9cae0a2f9dcfd43934bd4321ed029e395122b61f1f3a8c3780dd4b44ac8669139b382da8b9230123639bf7a7554206223e3127d3e3317dfb802190cda46 +"@metamask/tron-wallet-snap@npm:^1.12.1": + version: 1.12.1 + resolution: "@metamask/tron-wallet-snap@npm:1.12.1" + checksum: 10/6f48c8dd6f625d7bb290bf3d39978839a0f4b905c14883e43fb35538b5ffa822f9611b8977fc54e9cb83711a95a9cbce93ad6a0149c4c31cfd1272af4b7055b0 languageName: node linkType: hard @@ -35678,7 +35678,7 @@ __metadata: "@metamask/json-rpc-engine": "npm:^10.0.3" "@metamask/json-rpc-middleware-stream": "npm:^8.0.7" "@metamask/key-tree": "npm:^10.1.1" - "@metamask/keyring-api": "npm:^21.1.0" + "@metamask/keyring-api": "npm:^21.2.0" "@metamask/keyring-controller": "npm:^24.0.0" "@metamask/keyring-internal-api": "npm:^9.1.0" "@metamask/keyring-snap-client": "npm:^8.1.0" @@ -35739,7 +35739,7 @@ __metadata: "@metamask/token-search-discovery-controller": "npm:^4.0.0" "@metamask/transaction-controller": "patch:@metamask/transaction-controller@npm%3A62.3.0#~/.yarn/patches/@metamask-transaction-controller-npm-61.0.0-cccac388c7.patch" "@metamask/transaction-pay-controller": "npm:^10.1.0" - "@metamask/tron-wallet-snap": "npm:^1.10.0" + "@metamask/tron-wallet-snap": "npm:^1.12.1" "@metamask/utils": "npm:^11.8.1" "@ngraveio/bc-ur": "npm:^1.1.6" "@nktkas/hyperliquid": "npm:^0.25.9"