Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Rainbow

React Native crypto wallet app (iOS & Android). Uses React Navigation, custom store creators for state/data, and ethers/viem for blockchain interactions.
React Native crypto wallet app (iOS & Android). Uses React Navigation, `@storesjs/stores` store creators for state/data, and ethers/viem for blockchain interactions.

## Verification

Expand All @@ -16,13 +16,13 @@ React Native crypto wallet app (iOS & Android). Uses React Navigation, custom st

### State management

Custom store creators built on Zustand, defined in `src/state/internal/`:
State/data stores use `@storesjs/stores`:

- **`createRainbowStore`** -- general-purpose store with optional MMKV persistence. Use for client state.
- **`createBaseStore`** -- general-purpose store with optional synchronous MMKV persistence. Use for client state.
- **`createQueryStore`** -- combines data fetching + state in one store. Reactive `$` params auto-refetch when dependencies change. Replaces the React Query + Zustand dual-store pattern. Use for server/async data.
- **`createDerivedStore`** -- read-only store that composes other stores. Use for computed/aggregated state.

Stores live in `src/state/` (one per domain) and in `src/features/*/data/stores/`. These in-repo creators are being replaced by the external [`stores`](https://github.com/christianbaroni/stores) package (same APIs: `createBaseStore`, `createQueryStore`, `createDerivedStore`). The migration is mostly an import swap.
Stores live in `src/state/` (one per domain) and in `src/features/*/data/stores/`.

Legacy systems still in use:

Expand Down
Binary file added artifacts/storesjs-stores-0.8.7.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
"@shopify/flash-list": "1.8.3",
"@shopify/react-native-performance": "4.1.2",
"@shopify/react-native-skia": "2.4.7",
"@storesjs/stores": "0.8.7",
"@storesjs/stores": "file:artifacts/storesjs-stores-0.8.7.tgz",
"@tanstack/react-query": "4.2.1",
"@tanstack/react-query-persist-client": "4.2.1",
"@tradle/react-native-http": "2.0.1",
Expand Down
4 changes: 2 additions & 2 deletions src/__swaps__/screens/Swap/components/GasPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback, useMemo, type PropsWithChildren, type ReactNode } from 'react';
import { Platform, type LayoutChangeEvent } from 'react-native';

import { createBaseStore } from '@storesjs/stores';
import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, withDelay, withSpring } from 'react-native-reanimated';

import { ACTION_BUTTON_HEIGHT } from '@/__swaps__/screens/Swap/constants';
Expand Down Expand Up @@ -30,7 +31,6 @@ import { useNavigation } from '@/navigation/Navigation';
import Routes from '@/navigation/routesNames';
import { gasTrendToTrendType, type ExplainSheetParams } from '@/navigation/types';
import { ChainId } from '@/state/backendNetworks/types';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { swapsStore, useSwapsStore } from '@/state/swaps/swapsStore';
import { THICK_BORDER_WIDTH } from '@/styles/constants';

Expand Down Expand Up @@ -221,7 +221,7 @@ function CurrentBaseFee() {
}

type GasPanelState = { gasPrice?: string; maxBaseFee?: string; maxPriorityFee?: string };
const useGasPanelStore = createRainbowStore<GasPanelState | undefined>(() => undefined);
const useGasPanelStore = createBaseStore<GasPanelState | undefined>(() => undefined);

function useGasPanelState<
Option extends 'maxBaseFee' | 'maxPriorityFee' | 'gasPrice' | undefined = undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/__swaps__/screens/Swap/resources/search/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createQueryStore } from '@storesjs/stores';
import { TOKEN_SEARCH_URL } from 'react-native-dotenv';

import { type SearchAsset } from '@/__swaps__/types/search';
import { RainbowFetchClient } from '@/framework/data/http/rainbowFetch';
import { type ChainId } from '@/state/backendNetworks/types';
import { createQueryStore } from '@/state/internal/createQueryStore';
import { useSwapsStore } from '@/state/swaps/swapsStore';
import { time } from '@/utils/time';

Expand Down
7 changes: 3 additions & 4 deletions src/__swaps__/screens/Swap/resources/search/searchV2.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getAddress, isAddress } from '@ethersproject/address';
import { Contract } from '@ethersproject/contracts';
import { createBaseStore, createQueryStore } from '@storesjs/stores';
import { groupBy } from 'lodash';
import qs from 'qs';
import { TOKEN_SEARCH_URL } from 'react-native-dotenv';
Expand All @@ -11,8 +12,6 @@ import { logger, RainbowError } from '@/logger';
import erc20ABI from '@/references/erc20-abi.json';
import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks';
import { ChainId } from '@/state/backendNetworks/types';
import { createQueryStore } from '@/state/internal/createQueryStore';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { useSwapsStore } from '@/state/swaps/swapsStore';
import { getUniqueId } from '@/utils/ethereumUtils';
import { time } from '@/utils/time';
Expand Down Expand Up @@ -73,9 +72,9 @@ type DiscoverSearchResults = {

// ============ Store Definitions ============================================== //

export const useSwapsSearchStore = createRainbowStore<{ searchQuery: string }>(() => ({ searchQuery: '' }));
export const useSwapsSearchStore = createBaseStore<{ searchQuery: string }>(() => ({ searchQuery: '' }));

export const useDiscoverSearchQueryStore = createRainbowStore<DiscoverSearchQueryState>(() => ({ isSearching: false, searchQuery: '' }));
export const useDiscoverSearchQueryStore = createBaseStore<DiscoverSearchQueryState>(() => ({ isSearching: false, searchQuery: '' }));

export const useTokenSearchStore = createQueryStore<VerifiedResults, TokenSearchParams<TokenLists.Verified>>(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, type LegacyRef } from 'react';
import { type LayoutChangeEvent } from 'react-native';

import { useListen } from '@storesjs/stores';
import { type SetterOrUpdater } from 'recoil';
import { DataProvider, RecyclerListView } from 'recyclerlistview';
import { useMemoOne } from 'use-memo-one';
Expand All @@ -20,7 +21,6 @@ import usePrevious from '@/hooks/usePrevious';
import { useRemoteConfig } from '@/model/remoteConfig';
import { useRecyclerListViewScrollToTopContext } from '@/navigation/RecyclerListViewScrollToTopContext';
import { useUserAssetsStore } from '@/state/assets/userAssets';
import { useListen } from '@/state/internal/hooks/useListen';
import { useTheme, type ThemeContextProps } from '@/theme/ThemeContext';
import deviceUtils from '@/utils/deviceUtils';

Expand Down
2 changes: 1 addition & 1 deletion src/components/live-token-text/LiveTokenText.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';

import { useListen } from '@storesjs/stores';
import { useAnimatedReaction, useAnimatedStyle, useSharedValue, withDelay, withTiming, type SharedValue } from 'react-native-reanimated';

import { AnimatedText, useForegroundColor, type TextProps } from '@/design-system';
import usePrevious from '@/hooks/usePrevious';
import { useRoute } from '@/navigation/Navigation';
import { useListen } from '@/state/internal/hooks/useListen';
import { addSubscribedToken, removeSubscribedToken, useLiveTokensStore, type TokenData } from '@/state/liveTokens/liveTokensStore';
import { useTheme } from '@/theme/ThemeContext';
import { toUnixTime } from '@/worklets/dates';
Expand Down
3 changes: 1 addition & 2 deletions src/components/rainbow-toast/RainbowToast.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { memo, useCallback, useEffect, useMemo } from 'react';
import { Platform, StyleSheet, View } from 'react-native';

import { createDerivedStore } from '@storesjs/stores';
import { createDerivedStore, useListen } from '@storesjs/stores';
import { LinearGradient } from 'expo-linear-gradient';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
Expand Down Expand Up @@ -50,7 +50,6 @@ import { TransactionStatus } from '@/entities/transactions';
import { IS_TEST } from '@/env';
import useAccountSettings from '@/hooks/useAccountSettings';
import useDimensions from '@/hooks/useDimensions';
import { useListen } from '@/state/internal/hooks/useListen';
import { useStoreSharedValue, type ReadOnlySharedValue } from '@/state/internal/hooks/useStoreSharedValue';
import { useWalletsStore } from '@/state/wallets/walletsStore';

Expand Down
5 changes: 2 additions & 3 deletions src/components/rainbow-toast/useRainbowToastsStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useMemo } from 'react';

import { createStoreActions } from '@storesjs/stores';
import { createBaseStore, createStoreActions } from '@storesjs/stores';

import { TOAST_HIDE_TIMEOUT_MS } from '@/components/rainbow-toast/constants';
import type { RainbowToast } from '@/components/rainbow-toast/types';
import type { RainbowTransaction } from '@/entities/transactions';
import { createRainbowStore } from '@/state/internal/createRainbowStore';

function toToastId(tx: RainbowTransaction): string {
const identity = tx.relayExecutionId ?? (tx.nonce !== null && tx.nonce !== undefined ? String(tx.nonce) : tx.hash);
Expand All @@ -26,7 +25,7 @@ export type ToastState = {
pendingRemoveToastIds: string[];
};

export const useRainbowToastsStore = createRainbowStore<ToastState>((set, get) => ({
export const useRainbowToastsStore = createBaseStore<ToastState>((set, get) => ({
toasts: {},
// Tracks toasts that are pending removal while the expanded state is opened.
pendingRemoveToastIds: [],
Expand Down
2 changes: 1 addition & 1 deletion src/components/value-chart/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { memo, useCallback, useMemo } from 'react';
import { useWindowDimensions } from 'react-native';

import { useListen } from '@storesjs/stores';
import Animated, {
useAnimatedStyle,
useDerivedValue,
Expand All @@ -27,7 +28,6 @@ import { getHyperliquidTokenId } from '@/features/perps/utils';
import { useCleanup } from '@/hooks/useCleanup';
import Routes from '@/navigation/routesNames';
import { type AssetAccentColors, type ExpandedSheetAsset } from '@/screens/expandedAssetSheet/context/ExpandedAssetSheetContext';
import { useListen } from '@/state/internal/hooks/useListen';
import { useListenerRouteGuard } from '@/state/internal/hooks/useListenerRouteGuard';
import { useStoreSharedValue } from '@/state/internal/hooks/useStoreSharedValue';
import { type TokenData } from '@/state/liveTokens/liveTokensStore';
Expand Down
5 changes: 3 additions & 2 deletions src/config/experimentalConfigStore.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { createBaseStore } from '@storesjs/stores';

import { defaultConfig, defaultConfigValues, type ExperimentalConfigKey } from '@/config/experimental';
import { IS_STORE_INSTALL } from '@/env';
import { createRainbowStore } from '@/state/internal/createRainbowStore';

export type ExperimentalConfigState = {
config: Record<ExperimentalConfigKey, boolean>;
getFlag: (key: ExperimentalConfigKey) => boolean;
toggleFlag: (key: ExperimentalConfigKey) => void;
};

export const useExperimentalConfigStore = createRainbowStore<ExperimentalConfigState>(
export const useExperimentalConfigStore = createBaseStore<ExperimentalConfigState>(
(set, get) => ({
config: defaultConfigValues,

Expand Down
4 changes: 2 additions & 2 deletions src/features/backup/stores/backupsStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Platform } from 'react-native';

import { createBaseStore } from '@storesjs/stores';
import { Mutex } from 'async-mutex';

import { fetchAllBackups, getGoogleAccountUserData, isCloudBackupAvailable, syncCloud } from '@/handlers/cloudBackup';
import walletBackupTypes from '@/helpers/walletBackupTypes';
import { logger, RainbowError } from '@/logger';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { getWallets } from '@/state/wallets/walletsStore';

import { parseTimestampFromFilename, type BackupFile, type CloudBackups } from '../backup';
Expand Down Expand Up @@ -97,7 +97,7 @@ const getMostRecentCloudBackup = (backups: BackupFile[]) => {
}, cloudBackups[0]);
};

export const backupsStore = createRainbowStore<BackupsStore>(
export const backupsStore = createBaseStore<BackupsStore>(
(set, get) => ({
timesPromptedForBackup: 0,
setTimesPromptedForBackup: timesPromptedForBackup => set({ timesPromptedForBackup }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type SkParagraph,
type SkPicture,
} from '@shopify/react-native-skia';
import { useListen } from '@storesjs/stores';
import { dequal } from 'dequal';
import { cloneDeep, merge } from 'lodash';
import { Gesture, GestureDetector, State as GestureState } from 'react-native-gesture-handler';
Expand Down Expand Up @@ -65,7 +66,6 @@ import Routes from '@/navigation/routesNames';
import { supportedCurrencies as supportedNativeCurrencies } from '@/references/supportedCurrencies';
import { userAssetsStoreManager } from '@/state/assets/userAssetsStoreManager';
import { type ChainId } from '@/state/backendNetworks/types';
import { useListen } from '@/state/internal/hooks/useListen';
import { useListenerRouteGuard } from '@/state/internal/hooks/useListenerRouteGuard';
import { type DeepPartial } from '@/types/objects';
import { deepFreeze } from '@/utils/deepFreeze';
Expand Down
5 changes: 2 additions & 3 deletions src/features/charts/line/components/SparklineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import React, { memo, useCallback } from 'react';
import { StyleSheet, View } from 'react-native';

import { Canvas, Picture } from '@shopify/react-native-skia';
import { useListen, type BaseStore } from '@storesjs/stores';
import Animated, { runOnUI, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';

import { SPRING_CONFIGS } from '@/components/animations/animationConfigs';
import { useWorkletClass } from '@/hooks/reanimated/useWorkletClass';
import { useCleanup } from '@/hooks/useCleanup';
import { useOnChange } from '@/hooks/useOnChange';
import { useStableValue } from '@/hooks/useStableValue';
import { useListen } from '@/state/internal/hooks/useListen';
import { type BaseRainbowStore } from '@/state/internal/types';
import { createBlankPicture } from '@/worklets/skia';

import { COMPACT_LINE_CHART_HORIZONTAL_OVERDRAW, CompactLineChartRenderer } from '../compact/CompactLineChartRenderer';
Expand All @@ -22,7 +21,7 @@ type SparklineChartProps<S extends LineChartDataStore> = {
chartId: string;
color: string;
height: number;
store: BaseRainbowStore<S>;
store: BaseStore<S>;
width: number;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { memo, useCallback, useMemo } from 'react';
import { Platform, StyleSheet, View } from 'react-native';

import { Canvas, Picture, type SkPicture } from '@shopify/react-native-skia';
import { useListen } from '@storesjs/stores';
import { cloneDeep, merge } from 'lodash';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
Expand All @@ -22,7 +23,6 @@ import { useWorkletClass } from '@/hooks/reanimated/useWorkletClass';
import { useCleanup } from '@/hooks/useCleanup';
import { useOnChange } from '@/hooks/useOnChange';
import { useStableValue } from '@/hooks/useStableValue';
import { useListen } from '@/state/internal/hooks/useListen';
import { useListenerRouteGuard } from '@/state/internal/hooks/useListenerRouteGuard';
import { type DeepPartial } from '@/types/objects';
import { deepFreeze } from '@/utils/deepFreeze';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { memo, useMemo } from 'react';
import { Platform, StyleSheet, View } from 'react-native';

import { type BaseStore, type QueryStoreState } from '@storesjs/stores';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, { useAnimatedStyle, useDerivedValue, withTiming, type DerivedValue, type SharedValue } from 'react-native-reanimated';

Expand All @@ -9,8 +10,6 @@ import { easing, TIMING_CONFIGS } from '@/components/animations/animationConfigs
import { EasingGradient } from '@/components/easing-gradient/EasingGradient';
import { AnimatedText, Text, TextShadow, useColorMode } from '@/design-system';
import { opacity } from '@/framework/ui/utils/opacity';
import { type StoreState } from '@/state/internal/queryStore/types';
import { type BaseRainbowStore } from '@/state/internal/types';
import { THICKER_BORDER_WIDTH } from '@/styles/constants';
import { formatTimestamp, type FormatTimestampOptions } from '@/worklets/dates';

Expand All @@ -36,9 +35,9 @@ const TIME_FORMAT_OPTIONS: FormatTimestampOptions = Object.freeze({ useTodayYest

// ============ Types ========================================================== //

type ChartsStoreType = BaseRainbowStore<{
type ChartsStoreType = BaseStore<{
getData: () => PolymarketChartData;
getStatus: StoreState<unknown, Record<string, unknown>>['getStatus'];
getStatus: QueryStoreState<unknown, Record<string, unknown>>['getStatus'];
}>;

type LegendEntry = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createQueryStore } from '@storesjs/stores';

import { type ResponseByTheme } from '@/__swaps__/utils/swaps';
import { POLYMARKET_SPORTS_MARKET_TYPE } from '@/features/polymarket/constants';
import { usePolymarketEventStore } from '@/features/polymarket/stores/polymarketEventStore';
import { getOutcomeTeamColor } from '@/features/polymarket/utils/getOutcomeTeam';
import { isThreeWayMoneyline } from '@/features/polymarket/utils/marketClassification';
import { isDrawMarket } from '@/features/polymarket/utils/sports';
import { createQueryStore } from '@/state/internal/createQueryStore';
import { time } from '@/utils/time';

import { fetchPriceHistory } from '../api/clobClient';
Expand Down
5 changes: 2 additions & 3 deletions src/features/charts/polymarket/stores/polymarketStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { createStoreActions } from '@/state/internal/utils/createStoreActions';
import { createBaseStore, createStoreActions } from '@storesjs/stores';

import { type MarketFilter, type PolymarketInterval } from '../types';

Expand All @@ -19,7 +18,7 @@ export type PolymarketStoreState = {

// ============ Store ========================================================== //

export const usePolymarketStore = createRainbowStore<PolymarketStoreState>(
export const usePolymarketStore = createBaseStore<PolymarketStoreState>(
set => ({
chartInterval: '1d',
highlightedSeriesId: null,
Expand Down
4 changes: 1 addition & 3 deletions src/features/charts/stores/candlestickStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createQueryStore, createStoreActions, getQueryKey, type CacheEntry, type SetDataParams } from '@storesjs/stores';
import qs from 'qs';

import type { NativeCurrencyKey } from '@/entities/nativeCurrencyTypes';
Expand All @@ -13,9 +14,6 @@ import { ensureError } from '@/logger';
import { getPlatformClient } from '@/resources/platform/client';
import { type ExpandedSheetParamAsset } from '@/screens/expandedAssetSheet/context/ExpandedAssetSheetContext';
import { userAssetsStoreManager } from '@/state/assets/userAssetsStoreManager';
import { createQueryStore, getQueryKey } from '@/state/internal/createQueryStore';
import { type CacheEntry, type SetDataParams } from '@/state/internal/queryStore/types';
import { createStoreActions } from '@/state/internal/utils/createStoreActions';
import { type Exact } from '@/types/objects';
import { time } from '@/utils/time';

Expand Down
6 changes: 3 additions & 3 deletions src/features/charts/stores/chartsStore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createBaseStore, createStoreActions } from '@storesjs/stores';

import { CANDLESTICK_CHARTS } from '@/config/experimental';
import useExperimentalFlag from '@/config/experimentalHooks';
import { isHyperliquidToken } from '@/features/charts/utils';
import { useRemoteConfig } from '@/model/remoteConfig';
import { createRainbowStore } from '@/state/internal/createRainbowStore';
import { createStoreActions } from '@/state/internal/utils/createStoreActions';
import { type Exact } from '@/types/objects';

import { CandleResolution, ChartType, LineChartTimePeriod, type HyperliquidSymbol, type Token } from '../types';
Expand Down Expand Up @@ -31,7 +31,7 @@ export type ChartsState = {
togglePerpsIndicators: () => boolean;
};

export const useChartsStore = createRainbowStore<ChartsState>(
export const useChartsStore = createBaseStore<ChartsState>(
(set, get) => ({
candleResolution: CandleResolution.H1,
chartType: ChartType.Candlestick,
Expand Down
5 changes: 3 additions & 2 deletions src/features/charts/stores/derived/useCandlestickPrice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createDerivedStore } from '@storesjs/stores';

import { type Price } from '@/features/charts/candlestick/types';
import { arePricesEqual } from '@/features/charts/candlestick/utils';
import { createDerivedStore } from '@/state/internal/createDerivedStore';

import { calculatePercentChange, getTokenId, useCandlestickStore } from '../candlestickStore';
import { useChartsStore } from '../chartsStore';
Expand All @@ -19,5 +20,5 @@ export const useCandlestickPrice = createDerivedStore<CandlestickPrice | undefin
return { percentChange: calculatePercentChange(candles), price };
},

{ equalityFn: arePricesEqual, fastMode: true }
{ equalityFn: arePricesEqual, lockDependencies: true }
);
Loading
Loading