Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/lazy-bridges-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

⚡️ defer lifi sdk initialization
5 changes: 5 additions & 0 deletions .changeset/light-chains-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

⚡️ remove startup-blocking viem/chains import
17 changes: 0 additions & 17 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ import { channel, checkForUpdateAsync, fetchUpdateAsync, reloadAsync } from "exp

import { ToastProvider } from "@tamagui/toast";

import { optimism } from "@account-kit/infra";
import { createConfig, EVM } from "@lifi/sdk";
import {
ErrorBoundary,
feedbackIntegration,
Expand All @@ -28,12 +26,9 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { reconnect } from "@wagmi/core";
import { use as configI18n } from "i18next";
import { anvil } from "viem/chains";
import { WagmiProvider } from "wagmi";

import alchemyAPIKey from "@exactly/common/alchemyAPIKey";
import domain from "@exactly/common/domain";
import chain from "@exactly/common/generated/chain";

import BDOGroteskDemiBold from "../assets/fonts/BDOGrotesk-DemiBold.otf";
import BDOGroteskRegular from "../assets/fonts/BDOGrotesk-Regular.otf";
Expand All @@ -45,7 +40,6 @@ import release from "../generated/release";
import en from "../i18n/en.json";
import es from "../i18n/es.json";
import e2e from "../utils/e2e";
import publicClient from "../utils/publicClient";
import queryClient, { persister } from "../utils/queryClient";
import reportError from "../utils/reportError";
import exaConfig from "../utils/wagmi/exa";
Expand Down Expand Up @@ -124,17 +118,6 @@ const useServerFonts = typeof window === "undefined" ? useFonts : () => undefine
const useServerAssets = typeof window === "undefined" ? useAssets : () => undefined;
const useLayoutEffect = typeof window === "undefined" ? () => undefined : useClientLayoutEffect;
const devtools = !!JSON.parse(process.env.EXPO_PUBLIC_DEVTOOLS ?? String(Platform.OS === "web" && __DEV__));
if (!chain.testnet && chain.id !== anvil.id && typeof window !== "undefined") {
createConfig({
integrator: "exa_app",
apiKey: "4bdb54aa-4f28-4c61-992a-a2fdc87b0a0b.251e33ad-ef5e-40cb-9b0f-52d634b99e8f",
providers: [EVM({ getWalletClient: () => Promise.resolve(publicClient) })],
rpcUrls: {
[optimism.id]: [`${optimism.rpcUrls.alchemy?.http[0]}/${alchemyAPIKey}`],
[chain.id]: [publicClient.transport.alchemyRpcUrl],
},
});
}

export default wrap(function RootLayout() {
const navigationContainer = useNavigationContainerRef();
Expand Down
15 changes: 7 additions & 8 deletions src/components/activity/PendingProposals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ import {
} from "@tamagui/lucide-icons";
import { XStack, YStack } from "tamagui";

import { extractChain, type Chain } from "viem";
import * as chains from "viem/chains";

import chain from "@exactly/common/generated/chain";
import ProposalType, {
decodeBorrowAtMaturity,
Expand All @@ -30,14 +27,15 @@ import ProposalType, {
import shortenHex from "@exactly/common/shortenHex";

import { presentArticle } from "../../utils/intercom";
import queryClient from "../../utils/queryClient";
import reportError from "../../utils/reportError";
import useAsset from "../../utils/useAsset";
import usePendingOperations from "../../utils/usePendingOperations";
import SafeView from "../shared/SafeView";
import Text from "../shared/Text";
import View from "../shared/View";

import type { RouteFrom } from "../../utils/lifi";
import type { BridgeSources, RouteFrom } from "../../utils/lifi";
import type { MutationState } from "@tanstack/react-query";
import type { TFunction } from "i18next";

Expand Down Expand Up @@ -300,10 +298,11 @@ function ProposalItem({ proposal }: { proposal: Proposal }) {

function MutationItem({ mutation }: { mutation: MutationState<unknown, Error, RouteFrom> & { id: number } }) {
const { t } = useTranslation();
const { name: sourceChainName } = extractChain({
chains: Object.values(chains) as unknown as readonly [Chain, ...Chain[]],
id: mutation.variables?.chainId ?? 0,
});
const chainId = mutation.variables?.chainId ?? 0;
const bridgeSources = queryClient.getQueriesData<BridgeSources>({ queryKey: ["bridge", "sources"] });
const sourceChainName =
bridgeSources.flatMap(([, data]) => data?.chains ?? []).find((c) => c.id === chainId)?.name ??
t("Chain {{id}}", { id: chainId });
Comment on lines +301 to +305
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/activity/PendingProposals.tsx | head -320 | tail -40

Repository: exactly/exa

Length of output: 1822


🏁 Script executed:

# Get more context around the problematic lines
sed -n '290,320p' src/components/activity/PendingProposals.tsx

Repository: exactly/exa

Length of output: 1120


🏁 Script executed:

# Check the component structure and imports
head -50 src/components/activity/PendingProposals.tsx

Repository: exactly/exa

Length of output: 1455


🌐 Web query:

TanStack Query v5 subscribe to query cache updates non-reactive getQueriesData

💡 Result:

queryClient.getQueriesData() is synchronous and returns a snapshot; it won’t “react” to cache changes by itself. To get updates, subscribe to the QueryCache, and on each event re-read getQueriesData() (optionally batching the callback). [1] [2]

import { notifyManager } from '@tanstack/query-core' // or '@tanstack/react-query'
import type { QueryClient } from '@tanstack/query-core'

export function subscribeQueriesData<TData = unknown>(
  queryClient: QueryClient,
  filters: Parameters<QueryClient['getQueriesData']>[0],
  onChange: (tuples: Array<[unknown, TData | undefined]>) => void,
) {
  const queryCache = queryClient.getQueryCache()

  const emit = notifyManager.batchCalls(() => {
    onChange(queryClient.getQueriesData<TData>(filters) as any)
  })

  // initial snapshot
  emit()

  // update on any cache event (you can additionally filter by event.query?.queryKey if desired)
  const unsubscribe = queryCache.subscribe(() => emit())

  return unsubscribe
}

// usage:
const unsubscribe = subscribeQueriesData(client, { queryKey: ['todos'], exact: false }, (tuples) => {
  console.log('latest matching cached data:', tuples)
})

Sources: QueryCache subscription API [1], QueryClient getQueriesData() and getQueryCache() [2], notifyManager.batchCalls() [3].

References: [1] [2] [3]


getQueriesData is non-reactive; chain names won't update when bridge sources are cached.

queryClient.getQueriesData(...) returns a snapshot and won't trigger re-renders when the cache updates, leaving the UI stuck with the "Chain {id}" fallback even after bridge sources arrive. Use useQuery to subscribe reactively to bridge sources instead of querying the cache directly.

🤖 Prompt for AI Agents
In `@src/components/activity/PendingProposals.tsx` around lines 301 - 305, The
code uses queryClient.getQueriesData (involving BridgeSources and
mutation.variables?.chainId) which is non-reactive and causes sourceChainName to
not update; replace the snapshot access with a reactive useQuery hook that
subscribes to the bridge sources (e.g., call useQuery for the
["bridge","sources"] key or the equivalent fetcher used elsewhere) and derive
sourceChainName from that reactive data so the component re-renders when bridge
sources arrive; keep the same fallback t("Chain {{id}}", { id: chainId }) when
no matching chain is found and ensure you reference mutation.variables?.chainId,
BridgeSources, and sourceChainName when moving the logic into the component
body.

// TODO map values to other supported mutations
return (
<XStack gap="$s4" paddingVertical="$s3">
Expand Down
19 changes: 18 additions & 1 deletion src/components/add-funds/Bridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ScrollView, Spinner, Square, XStack, YStack } from "tamagui";
import { useMutation, useQuery } from "@tanstack/react-query";
import { switchChain, waitForTransactionReceipt } from "@wagmi/core";
import {
defineChain,
encodeFunctionData,
erc20Abi,
formatUnits,
Expand All @@ -37,7 +38,7 @@ import openBrowser from "../../utils/openBrowser";
import queryClient from "../../utils/queryClient";
import reportError from "../../utils/reportError";
import useAccount from "../../utils/useAccount";
import ownerConfig from "../../utils/wagmi/owner";
import ownerConfig, { addChains } from "../../utils/wagmi/owner";
import AssetLogo from "../shared/AssetLogo";
import GradientScrollView from "../shared/GradientScrollView";
import SafeView from "../shared/SafeView";
Expand Down Expand Up @@ -99,6 +100,22 @@ export default function Bridge() {
refetchIntervalInBackground: true,
});

useEffect(() => {
if (!bridge?.chains) return;
addChains(
bridge.chains
.filter((c): c is typeof c & { metamask: { rpcUrls: [string, ...string[]] } } => Boolean(c.metamask.rpcUrls[0]))
.map(({ id, name, metamask }) =>
defineChain({
id,
name,
nativeCurrency: metamask.nativeCurrency,
rpcUrls: { default: { http: [metamask.rpcUrls[0]] } },
}),
),
);
}, [bridge?.chains]);

const chains = bridge?.chains;
const ownerAssetsByChain = bridge?.ownerAssetsByChain;
const usdByToken = bridge?.usdByToken;
Expand Down
26 changes: 24 additions & 2 deletions src/utils/lifi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { optimism } from "@account-kit/infra";
import {
ChainType,
config,
createConfig as createLifiConfig,
EVM,
getChains,
getQuote,
getToken,
Expand All @@ -12,15 +15,29 @@ import {
type TokenAmount,
} from "@lifi/sdk";
import { parse } from "valibot";
import { encodeFunctionData, formatUnits } from "viem";
import { encodeFunctionData, formatUnits, type Address } from "viem";
import { anvil } from "viem/chains";

import alchemyAPIKey from "@exactly/common/alchemyAPIKey";
import chain, { mockSwapperAbi, swapperAddress } from "@exactly/common/generated/chain";
import { Address as AddressSchema, Hex } from "@exactly/common/validation";

import publicClient from "./publicClient";

import type { Address } from "viem";
let configured = false;
function ensureConfig() {
if (configured || chain.testnet || chain.id === anvil.id) return;
createLifiConfig({
integrator: "exa_app",
apiKey: "4bdb54aa-4f28-4c61-992a-a2fdc87b0a0b.251e33ad-ef5e-40cb-9b0f-52d634b99e8f",
providers: [EVM({ getWalletClient: () => Promise.resolve(publicClient) })],
rpcUrls: {
[optimism.id]: [`${optimism.rpcUrls.alchemy?.http[0]}/${alchemyAPIKey}`],
[chain.id]: [publicClient.transport.alchemyRpcUrl],
},
});
configured = true;
}

export async function getRoute(
fromToken: Hex,
Expand All @@ -30,6 +47,7 @@ export async function getRoute(
receiver: Hex,
denyExchanges?: Record<string, boolean>,
) {
ensureConfig();
if (chain.testnet || chain.id === anvil.id) {
const fromAmount = await publicClient.readContract({
abi: mockSwapperAbi,
Expand Down Expand Up @@ -90,6 +108,7 @@ export async function getRoute(
}

async function getAllTokens(): Promise<Token[]> {
ensureConfig();
if (chain.testnet || chain.id === anvil.id) return [];
const response = await getTokens({ chains: [chain.id] });
const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B");
Expand Down Expand Up @@ -152,6 +171,7 @@ const allowList = new Set([
]);

export async function getAllowTokens() {
ensureConfig();
if (chain.testnet || chain.id === anvil.id) return [];
const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B");
const { tokens } = await getTokens({ chains: [chain.id] });
Expand Down Expand Up @@ -192,6 +212,7 @@ export async function getRouteFrom({
toChainId?: number;
toTokenAddress: string;
}): Promise<RouteFrom> {
ensureConfig();
config.set({ integrator: "exa_app", userId: fromAddress });
const { estimate, transactionRequest, tool } = await getQuote({
fee: 0.0025,
Expand Down Expand Up @@ -242,6 +263,7 @@ export type BridgeSources = {
};

export async function getBridgeSources(account?: string, protocolSymbols: string[] = []): Promise<BridgeSources> {
ensureConfig();
if (!account) throw new Error("account is required");
const bridgeTokenSymbols = new Set(protocolSymbols);
if (bridgeTokenSymbols.size === 0) throw new Error("protocol symbols is required");
Expand Down
18 changes: 12 additions & 6 deletions src/utils/wagmi/owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,31 @@ import AsyncStorage from "@react-native-async-storage/async-storage";

import { sdk } from "@farcaster/miniapp-sdk";
import { farcasterMiniApp as miniAppConnector } from "@farcaster/miniapp-wagmi-connector";
import { http } from "viem";
import * as chains from "viem/chains";
import { createClient, http, type Chain } from "viem";
import { createConfig, createStorage, custom, injected } from "wagmi";

import chain from "@exactly/common/generated/chain";

import publicClient from "../publicClient";

const config = createConfig({
chains: [chain, ...Object.values(chains)],
chains: [chain],
connectors: [miniAppConnector(), injected()],
transports: {
...Object.fromEntries(Object.values(chains).map((c) => [c.id, http()])),
[chain.id]: custom(publicClient),
client({ chain: c }) {
return createClient({ chain: c, transport: c.id === chain.id ? custom(publicClient) : http() });
},
storage: createStorage({ key: "wagmi.owner", storage: AsyncStorage }),
});
export default config;

export function addChains(newChains: readonly Chain[]) {
const current = config.chains;
const ids = new Set(current.map((c) => c.id));
const toAdd = newChains.filter((c) => !ids.has(c.id));
if (toAdd.length === 0) return;
config._internal.chains.setState([...current, ...toAdd]);
}
Comment on lines +22 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/utils/wagmi/owner.ts

Repository: exactly/exa

Length of output: 1981


🏁 Script executed:

# Search for wagmi config initialization and imports in the project
rg "config\s*=" src/utils/wagmi/ -A 3 -B 3

Repository: exactly/exa

Length of output: 397


🌐 Web query:

wagmi dynamic chain configuration addChains public API

💡 Result:

wagmi does not expose a public addChains (or “dynamically add chains to an existing config”) API.

  • In wagmi, the supported chains are defined when you create the config (createConfig({ chains: [...] })) and that list is what wagmi uses for validation/routing. If a chain isn’t in config.chains, you’ll hit ChainNotConfiguredError. [1], [2]
  • The older configureChains(...) helper similarly returns a static chains array to pass into config/connectors; it’s not a runtime “registry” you can append to. [3]
  • switchChain has an addEthereumChainParameter option, but that’s for prompting the wallet to add a chain (EIP-3085 / wallet_addEthereumChain)—it does not add the chain to wagmi’s configured chains list. [4]

Practical pattern: include all chains you may need up-front in createConfig, or create a new config (and re-mount WagmiProvider) when your chain set must change (e.g., per environment). [1], [2]

Sources:
[1] https://wagmi.sh/core/api/createConfig
[2] wevm/wagmi#3425
[3] https://1.x.wagmi.sh/core/providers/configuring-chains
[4] https://wagmi.sh/core/api/actions/switchChain


🌐 Web query:

wagmi config internal API _internal chains setState stability

💡 Result:

In wagmi v2+, anything on config._internal (including patterns like config._internal.chains.setState) should be treated as private implementation detail and not a stable/public API.

  • The documented/stable surface is what’s on the Config returned by createConfig (e.g. config.chains is defined as a readonly list, and config.setState is the supported way to update Config state, not the chain list) [1].
  • config._internal is used by wagmi’s own internals/types (e.g. actions referencing config['_internal']['transports']), which is a strong signal it’s meant for internal wiring rather than consumer use [2].
  • Wagmi emphasizes semver/stability for its supported APIs, but that guarantee does not typically extend to underscored/internal members [3].

Practical takeaway: don’t rely on _internal.*.setState for runtime chain mutation. If you need a different supported chain set, create a new config with a different chains array; if you just need to change the active chain, use the supported chain-switching APIs (e.g. useSwitchChain in React) rather than mutating internal chain stores [1].

Sources: [1] createConfig API docs (Config surface, chains readonly, setState) https://wagmi.sh/core/api/createConfig, [2] example of _internal usage in wagmi core source/types (UNPKG) https://app.unpkg.com/%40wagmi/core%402.16.5/files/src/actions/watchPendingTransactions.ts, [3] wagmi “Stability” statement https://wagmi.sh/core/why


Remove use of wagmi's private _internal.chains.setState API.

The addChains function uses config._internal.chains.setState, which is an undocumented private implementation detail. Wagmi provides no public API for dynamically adding chains to an existing config. The config.chains property is explicitly readonly, and wagmi's stability guarantees do not extend to _internal members—this pattern may break silently on any wagmi upgrade.

If dynamic chain support is required, either pre-configure all chains upfront in the createConfig call, or create a new config (and re-mount the WagmiProvider) when the chain set must change.

🤖 Prompt for AI Agents
In `@src/utils/wagmi/owner.ts` around lines 22 - 28, The addChains function
currently mutates wagmi internals via config._internal.chains.setState which
relies on private API; remove that call and stop mutating config.chains
directly. Replace usage of addChains by either (A) pre-configuring all required
Chain entries in createConfig so config.chains is complete at startup, or (B)
when chains must change at runtime, create a new wagmi config (via createConfig)
with the updated chains array and re-mount the WagmiProvider with that new
config instead of attempting to mutate config._internal.chains; also remove or
deprecate the addChains helper to avoid future use. Ensure references to
addChains in the codebase are updated to the chosen approach (recreate
config/WagmiProvider or supply full list at creation).


export async function getConnector() {
const miniApp = await sdk.isInMiniApp();
const connector = miniApp
Expand Down