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/bright-llamas-provision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/mobile": patch
---

✨ provision alchemy rpc urls across lifi chains
31 changes: 25 additions & 6 deletions src/utils/lifi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { optimism } from "@account-kit/infra";
import * as infra from "@account-kit/infra";
import {
ChainType,
config,
Expand Down Expand Up @@ -51,7 +51,7 @@ export const lifiTokensOptions = queryOptions({
try {
const { tokens } = await getTokens({ chainTypes: [ChainType.EVM] });
const allTokens = Object.values(tokens).flat();
if (chain.id !== optimism.id) return allTokens;
if (chain.id !== infra.optimism.id) return allTokens;
const exa = await getToken(chain.id, "0x1e925De1c68ef83bD98eE3E130eF14a50309C01B").catch((error: unknown) => {
reportError(error);
});
Expand Down Expand Up @@ -91,15 +91,34 @@ export function tokenBalancesOptions(account: Address | undefined) {
let configured = false;
function ensureConfig() {
if (configured || chain.testnet || chain.id === anvil.id) return;
const rpcUrls = {
...Object.values(infra).reduce<Record<number, string[]>>((result, item) => {
if (typeof item !== "object" || !("id" in item) || !("rpcUrls" in item)) return result;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The check typeof item !== "object" is not sufficient to guard against null, because typeof null is 'object'. If item is null, the expression !("id" in item) will throw a TypeError. You should add a check for item === null.

Suggested change
if (typeof item !== "object" || !("id" in item) || !("rpcUrls" in item)) return result;
if (typeof item !== "object" || item === null || !("id" in item) || !("rpcUrls" in item)) return result;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

null slips past the typeof item !== "object" guard.

typeof null === "object" in JavaScript, so null values are not filtered by the typeof check. The subsequent "id" in item call on null throws a TypeError at runtime. While @account-kit/infra is unlikely to export null today, this is fragile.

🛡️ Proposed fix
-      if (typeof item !== "object" || !("id" in item) || !("rpcUrls" in item)) return result;
+      if (!item || typeof item !== "object" || !("id" in item) || !("rpcUrls" in item)) return result;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof item !== "object" || !("id" in item) || !("rpcUrls" in item)) return result;
if (!item || typeof item !== "object" || !("id" in item) || !("rpcUrls" in item)) return result;

const candidate = item as { id: number; rpcUrls: { alchemy?: { http?: string[] } } };
const alchemyRpcUrl = candidate.rpcUrls.alchemy?.http?.[0];
if (!alchemyRpcUrl) return result;
result[candidate.id] = [`${alchemyRpcUrl}/${alchemyAPIKey}`];
return result;
}, {}),
[chain.id]: [publicClient.transport.alchemyRpcUrl],
Copy link
Copy Markdown

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:

# First, find and examine the lifi.ts file around line 103
fd -e ts -path "*utils/lifi*" --exclude "*.test.*"

Repository: exactly/exa

Length of output: 223


🏁 Script executed:

# Find publicClient definition
fd -e ts "publicClient" --exclude "*.test.*" --exclude node_modules

Repository: exactly/exa

Length of output: 109


🏁 Script executed:

# Search for how alchemyRpcUrl is defined or used
rg "alchemyRpcUrl" --type ts

Repository: exactly/exa

Length of output: 357


🏁 Script executed:

# Read the lifi.ts file to see full context around line 103
cat -n src/utils/lifi.ts | head -120 | tail -30

Repository: exactly/exa

Length of output: 1596


🏁 Script executed:

# Read publicClient.ts to understand the transport type
cat -n src/utils/publicClient.ts

Repository: exactly/exa

Length of output: 487


🏁 Script executed:

# Check if there's any guard or conditional before line 103
cat -n src/utils/lifi.ts | sed -n '90,110p'

Repository: exactly/exa

Length of output: 1132


🏁 Script executed:

# Search for alchemy transport typing and alchemyRpcUrl property
rg "alchemyRpcUrl" -B 2 -A 2 --type-list

Repository: exactly/exa

Length of output: 444


🏁 Script executed:

# Check what `@account-kit/infra` exports and how alchemy transport is typed
rg "@account-kit/infra" src/utils/publicClient.ts

Repository: exactly/exa

Length of output: 130


🏁 Script executed:

# Check how alchemyAPIKey is imported and what its default value is
rg "alchemyAPIKey" src/utils/lifi.ts -B 2 -A 2

Repository: exactly/exa

Length of output: 527


🏁 Script executed:

# Find the alchemyAPIKey definition/export
fd -e ts -e js "alchemyAPIKey" --exclude node_modules

Repository: exactly/exa

Length of output: 78


🏁 Script executed:

# Read the alchemyAPIKey definition
cat -n common/alchemyAPIKey.ts

Repository: exactly/exa

Length of output: 1036


🏁 Script executed:

# Also check for type definitions in publicClient.ts to see if alchemyRpcUrl is properly typed
rg "transport" src/utils/publicClient.ts -B 2 -A 2

Repository: exactly/exa

Length of output: 198


🏁 Script executed:

# Check if line 103 execution is guarded for anvil chain
cat -n src/utils/lifi.ts | sed -n '92,104p'

Repository: exactly/exa

Length of output: 795


Add a guard to ensure publicClient.transport.alchemyRpcUrl is defined before using it.

If the alchemy transport doesn't expose the alchemyRpcUrl property or it's undefined, the code will add an undefined value to the RPC URLs array, silently breaking LiFi's RPC configuration for the active chain. Consider adding a check like:

[chain.id]: publicClient.transport.alchemyRpcUrl ? [publicClient.transport.alchemyRpcUrl] : [],

Or retrieve the URL from the same infra object used above to maintain consistency.

};
Comment on lines +94 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

rpcUrls is used in a single place — consider inlining per project guidelines.

The rpcUrls variable is only consumed in the createLifiConfig call on line 110. As per coding guidelines, single-use values should be kept inline rather than extracted into a named variable.

♻️ Proposed inline refactor
-  const rpcUrls = {
-    ...Object.values(infra).reduce<Record<number, string[]>>((result, item) => {
-      if (typeof item !== "object" || !("id" in item) || !("rpcUrls" in item)) return result;
-      const candidate = item as { id: number; rpcUrls: { alchemy?: { http?: string[] } } };
-      const alchemyRpcUrl = candidate.rpcUrls.alchemy?.http?.[0];
-      if (!alchemyRpcUrl) return result;
-      result[candidate.id] = [`${alchemyRpcUrl}/${alchemyAPIKey}`];
-      return result;
-    }, {}),
-    [chain.id]: [publicClient.transport.alchemyRpcUrl],
-  };
   createLifiConfig({
     integrator: "exa_app",
     apiKey: "4bdb54aa-4f28-4c61-992a-a2fdc87b0a0b.251e33ad-ef5e-40cb-9b0f-52d634b99e8f",
     preloadChains: false,
     providers: [EVM({ getWalletClient: () => Promise.resolve(publicClient) })],
-    rpcUrls,
+    rpcUrls: {
+      ...Object.values(infra).reduce<Record<number, string[]>>((result, item) => {
+        if (typeof item !== "object" || !item || !("id" in item) || !("rpcUrls" in item)) return result;
+        const candidate = item as { id: number; rpcUrls: { alchemy?: { http?: string[] } } };
+        const alchemyRpcUrl = candidate.rpcUrls.alchemy?.http?.[0];
+        if (!alchemyRpcUrl) return result;
+        result[candidate.id] = [`${alchemyRpcUrl}/${alchemyAPIKey}`];
+        return result;
+      }, {}),
+      [chain.id]: [publicClient.transport.alchemyRpcUrl],
+    },
   });

Note: the !item guard is included to prevent a TypeError when "id" in item is evaluated against a null value (see below).

As per coding guidelines: "Do not extract a value into a variable or logic into a function unless it is used in two or more places; keep single-use values and functions inline."

createLifiConfig({
integrator: "exa_app",
apiKey: "4bdb54aa-4f28-4c61-992a-a2fdc87b0a0b.251e33ad-ef5e-40cb-9b0f-52d634b99e8f",
preloadChains: false,
providers: [EVM({ getWalletClient: () => Promise.resolve(publicClient) })],
rpcUrls: {
[optimism.id]: [`${optimism.rpcUrls.alchemy?.http[0]}/${alchemyAPIKey}`],
[chain.id]: [publicClient.transport.alchemyRpcUrl],
},
rpcUrls,
});
config.loading = getChains({ chainTypes: [ChainType.EVM] })
.then((availableChains) => {
const rpcs = config.get().rpcUrls as Partial<Record<number, readonly string[]>>;
config.setChains(
availableChains.map((c) => (rpcs[c.id]?.length ? { ...c, metamask: { ...c.metamask, rpcUrls: [] } } : c)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The code spreads c.metamask without checking if it exists. If c.metamask is undefined, this creates a new, incomplete metamask object, leading to incorrect LiFi SDK configuration.
Severity: MEDIUM

Suggested Fix

Conditionally update the metamask property only if it already exists on the chain object c. For example, change the logic to rpcs[c.id]?.length && c.metamask ? { ...c, metamask: { ...c.metamask, rpcUrls: [] } } : c.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/utils/lifi.ts#L116

Potential issue: When configuring chains for the LiFi SDK, the code maps over
`availableChains`. If a chain has a configured RPC, it attempts to modify the chain's
`metamask` property by spreading it: `{ ...c, metamask: { ...c.metamask, rpcUrls: [] }
}`. The `metamask` property is optional on a chain object `c`. If `c.metamask` is
`undefined`, the spread results in a new, incomplete object `{ rpcUrls: [] }`. This
creates a malformed `metamask` configuration, as it's missing other required properties
like `chainId` and `chainName`, which can cause downstream errors within the LiFi SDK.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The metamask property on ExtendedChain (the type of c) is optional. If c.metamask is undefined, the spread operator ...c.metamask will throw a TypeError. You should handle this case, for example by providing a default empty object.

Suggested change
availableChains.map((c) => (rpcs[c.id]?.length ? { ...c, metamask: { ...c.metamask, rpcUrls: [] } } : c)),
availableChains.map((c) => (rpcs[c.id]?.length ? { ...c, metamask: { ...(c.metamask ?? {}), rpcUrls: [] } } : c)),

);
})
.catch((error: unknown) => {
reportError(error);
});
configured = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

race condition: configured is set before config.loading completes. subsequent calls to functions like getRoute (line 135) or getAllTokens (line 196) won't re-run ensureConfig, but chains may not be fully loaded yet, potentially causing issues if they rely on config.setChains being complete.

queryClient.prefetchQuery(lifiTokensOptions).catch(reportError);
queryClient.prefetchQuery(lifiChainsOptions).catch(reportError);
Comment on lines +112 to 124
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Duplicate getChains call — config.loading and lifiChainsOptions both fetch the same data

The new config.loading at src/utils/lifi.ts:112 calls getChains({ chainTypes: [ChainType.EVM] }), and then src/utils/lifi.ts:124 prefetches lifiChainsOptions which also calls getChains({ chainTypes: [ChainType.EVM] }) (defined at src/utils/lifi.ts:37). The lifi SDK internally awaits config.loading before API operations, so the lifiChainsOptions prefetch will first wait for the config.loading chain fetch to complete, then make a second identical API call.

This is functionally correct but results in two sequential getChains calls on initialization — the first to configure chain RPC overrides, the second to populate the query cache. Consider reusing the chains fetched by config.loading to populate the query cache directly, avoiding the redundant network request.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Expand Down