Skip to content

✨ app: add support for multi-chain asset recovery#970

Open
dieguezguille wants to merge 12 commits intotransportfrom
recover
Open

✨ app: add support for multi-chain asset recovery#970
dieguezguille wants to merge 12 commits intotransportfrom
recover

Conversation

@dieguezguille
Copy link
Copy Markdown
Member

@dieguezguille dieguezguille commented Apr 16, 2026

closes #854 , closes #649

Summary by CodeRabbit

  • New Features

    • Cross-chain account support, multi-chain balance fetching, and non-collateral/external assets views; recovery flow for assets on unsupported networks; portfolio pending-state UI.
  • Bug Fixes

    • Improved error status extraction, USD display for very small balances, swap/transfer validation, provider access behavior, and proposal timing; aligned close buttons.
  • Refactors

    • Bridge/source fetching and query exclusion logic simplified.
  • Localization

    • Spanish and Portuguese strings added/updated for new UX.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 16, 2026

🦋 Changeset detected

Latest commit: 7b68045

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds cross-chain asset discovery and execution, portfolio restructuring, new UI sheets for external/collateral/unsupported networks, cross-chain account client routing, multiple query helpers for deployed/wallet balances, i18n updates, and assorted UI/layout tweaks. Includes many small changesets for patch releases.

Changes

Cohort / File(s) Summary
Changeset Metadata
.changeset/better-feet-arrive.md, .changeset/brisk-otters-sing.md, .changeset/easy-rockets-reply.md, .changeset/fuzzy-icons-push.md, .changeset/open-coats-battle.md, .changeset/smooth-plants-grab.md, .changeset/solid-snails-add.md, .changeset/tough-ducks-end.md, .changeset/wicked-buses-go.md, .changeset/wide-laws-travel.md, .changeset/wild-geckos-remain.md, .changeset/soft-ferrets-tap.md, .changeset/strong-kings-pay.md, .changeset/twelve-points-sniff.md
Added multiple patch-level changeset entries for @exactly/mobile documenting fixes, refactors, and small features.
Bridge & Add-Funds
src/components/add-funds/Bridge.tsx
Switched to useLocalSearchParams, introduced sender/transfer/swap/recovery classification, replaced markets with portfolio query helpers, adjusted quote/simulation gating to isTransfer, changed execution branching for cross-chain/native flows, removed shared handleError, and updated UI/deployment gating.
Home: Assets & Sheets
src/components/home/Home.tsx, src/components/home/AssetList.tsx, src/components/home/Portfolio.tsx, src/components/home/PortfolioSummary.tsx, src/components/home/CollateralAssetsSheet.tsx, src/components/home/ExternalAssets.tsx, src/components/home/ExternalAssetsSheet.tsx, src/components/home/UnsupportedNetworksSheet.tsx
Refactored portfolio to expose allAssets, added ExternalAssets and sheets (CollateralAssetsSheet, ExternalAssetsSheet, UnsupportedNetworksSheet) for non-collateral/cross-chain UX, added pending skeletons, asset grouping by chain, deployment checks, Intercom recovery flow, and small UI/press adjustments.
Portfolio Hook & LiFi / Balances
src/utils/usePortfolio.ts, src/utils/lifi.ts, src/utils/deployedOptions.ts
Split same-chain vs cross-chain balance queries, added walletBalancesOptions and bridgeSourcesOptions, introduced deployedOptions, added OwnerAsset + tokenAmountsToAssets, added protocolSymbols, added allAssets/crossChainAssets and updated USD totals and pending flags.
Account Client & Cross-Chain Routing
src/utils/accountClient.ts, src/utils/alchemyChains.ts
Extracted signUserOperationHash, added shared dummySignatureMiddleware, implemented cached getCrossChainClient(targetChainId), adjusted wallet_sendCalls to route UserOperations via cross-chain client with composite ids, and updated wallet_getCallsStatus to decode chain from ids.
Connector & Wagmi Config
src/utils/alchemyConnector.ts, src/utils/wagmi/exa.ts
Removed optional chainId param/validation from getProvider(), expanded Wagmi config to include multiple viem chains with per-chain transports, and added alchemyChains map file.
UI Consistency & Small Components
src/components/shared/Failure.tsx, src/components/shared/Pending.tsx, src/components/shared/Success.tsx, src/components/send-funds/Amount.tsx, src/components/loans/Review.tsx, src/components/swaps/Failure.tsx, src/components/swaps/Pending.tsx, src/components/swaps/Success.tsx, src/components/shared/AmountSelector.tsx, src/components/shared/AssetSelector.tsx, src/components/home/CollateralAssetsSheet.tsx, src/components/defi/ConnectionSheet.tsx, src/components/defi/IntroSheet.tsx
Applied alignSelf="flex-start" to various close buttons, adjusted flex/shrink behavior in some sheets, changed AmountSelector to store formatted USD string directly, added isSameChainPending gating in AssetSelector, and minor swaps/insufficient-balance logic tweaks.
Error Handling & Query Utilities
src/utils/reportError.ts, src/utils/queryClient.ts, src/utils/useSimulateProposal.ts
Refined passkey error classification to use codes and regexes, changed status extraction to traverse error cause chain for numeric codes, consolidated excluded query keys and suppressed messages into Sets, and bumped simulation block override timestamps by +1 unit.
Internationalization
src/i18n/es.json, src/i18n/pt.json
Added Spanish and Portuguese strings for collateral vs non-collateral guidance, unsupported-network recovery templates (with placeholders), swap CTA, support contact prompt; renamed "Collateral Assets""Collateral assets" and removed "Other Assets".
Config Typing
tamagui.config.ts
Added portfolio pseudo-group to Tamagui config and extended type augmentation to include "portfolio" in group names.

Sequence Diagram

sequenceDiagram
    participant User
    participant Bridge as Bridge Component
    participant Portfolio as Portfolio Screen
    participant QueryClient as Query Client
    participant LiFi as LiFi API
    participant Deployed as Deployed Check

    User->>Portfolio: Open portfolio
    Portfolio->>QueryClient: fetch markets, same-chain & wallet balances
    QueryClient->>LiFi: request tokenBalances & walletBalances
    LiFi-->>QueryClient: return balances per chain
    QueryClient->>Deployed: check contract bytecode per chain
    Deployed-->>Portfolio: deployed status
    Portfolio->>User: render collateral + external assets grouped by chain
    User->>Bridge: select external asset -> navigate with sender, sourceChain, sourceToken
    Bridge->>Bridge: classify mode (isTransfer/isSwap/isRecovery)
    alt isTransfer (same-chain)
        Bridge->>Wallet: send native transaction
        Wallet->>Network: broadcast tx
    else cross-chain / recovery
        Bridge->>AccountClient: request cross-chain client for targetChain
        AccountClient->>CrossChainClient: build/sign UserOperation
        CrossChainClient->>Network: submit UserOperation
        Network-->>CrossChainClient: execution/result
        CrossChainClient-->>Bridge: return composite id/result
    end
    Bridge->>User: show success/failure
Loading
sequenceDiagram
    participant User
    participant ExternalAssets as ExternalAssets Component
    participant QueryClient as Query Client
    participant Deployed as Deployed Check
    participant Unsupported as UnsupportedNetworksSheet

    User->>ExternalAssets: Tap non-collateral asset
    ExternalAssets->>QueryClient: ensure deployedOptions(account, chainId) queries
    QueryClient->>Deployed: return deployed/not-deployed
    alt not supported or not deployed
        ExternalAssets->>Unsupported: open UnsupportedNetworksSheet with formatted recovery message
        Unsupported->>User: show contact support flow (Intercom)
    else supported & deployed
        ExternalAssets->>Bridge: navigate to Bridge with asset params
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding multi-chain asset recovery support with clear scope and specificity.
Linked Issues check ✅ Passed The PR implements cross-chain asset recovery by adding external asset display, bridging support, and unsupported network handling as required by #649 and #854.
Out of Scope Changes check ✅ Passed All changes align with multi-chain recovery objectives: UI for external assets, bridge support, cross-chain queries, error handling, and layout refinements for the new features.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch recover

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 32.09549% with 256 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.74%. Comparing base (381d1b1) to head (7b68045).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/utils/lifi.ts 2.63% 74 Missing ⚠️
src/components/home/ExternalAssets.tsx 31.39% 59 Missing ⚠️
src/utils/accountClient.ts 12.50% 49 Missing ⚠️
src/utils/usePortfolio.ts 51.16% 21 Missing ⚠️
src/components/home/UnsupportedNetworksSheet.tsx 0.00% 19 Missing ⚠️
src/components/home/Portfolio.tsx 29.41% 12 Missing ⚠️
src/components/home/AssetList.tsx 75.86% 7 Missing ⚠️
src/utils/deployedOptions.ts 0.00% 6 Missing ⚠️
src/components/home/ExternalAssetsSheet.tsx 0.00% 3 Missing ⚠️
src/components/send-funds/Receiver.tsx 60.00% 2 Missing ⚠️
... and 3 more
Additional details and impacted files
@@              Coverage Diff              @@
##           transport     #970      +/-   ##
=============================================
- Coverage      71.91%   70.74%   -1.17%     
=============================================
  Files            229      236       +7     
  Lines           8441     8656     +215     
  Branches        2710     2771      +61     
=============================================
+ Hits            6070     6124      +54     
- Misses          2140     2302     +162     
+ Partials         231      230       -1     
Flag Coverage Δ
e2e 70.73% <32.09%> (-1.18%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@dieguezguille dieguezguille marked this pull request as ready for review April 20, 2026 14:10
sentry[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

sentry[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 14 additional findings in Devin Review.

Open in Devin Review

Comment thread src/utils/lifi.ts
Comment on lines +454 to +503
async function getWalletBalances(account: Address) {
const balances: Record<number, TokenAmount[]> = {};
const lifiConfig = config.get();
let offset: string | undefined;
do {
const url = new URL(`${lifiConfig.apiUrl}/wallets/${account}/balances`);
url.searchParams.set("extended", "true");
url.searchParams.set("limit", "1000");
if (offset) url.searchParams.set("offset", offset);
const response = await fetch(url, {
headers: {
...(lifiConfig.apiKey && { "x-lifi-api-key": lifiConfig.apiKey }),
...(lifiConfig.integrator && { "x-lifi-integrator": lifiConfig.integrator }),
},
});
if (!response.ok) throw new Error("wallet balances request failed");
const json = parse(
object({
balances: optional(
record(
string(),
array(
looseObject({
chainId: number(),
address: string(),
symbol: string(),
decimals: number(),
name: string(),
priceUSD: optional(string(), "0"),
logoURI: optional(string()),
amount: pipe(string(), regex(/^\d+$/)),
}),
),
),
),
offset: optional(string()),
}),
await response.json(),
);
for (const [chainId, tokens] of Object.entries(json.balances ?? {})) {
const id = Number(chainId);
if (!Number.isInteger(id)) continue;
balances[id] = [
...(balances[id] ?? []),
...tokens.map(({ amount, ...token }) => ({ ...token, amount: BigInt(amount) })),
];
}
offset = json.offset;
} while (offset);
return balances;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Wallet balances API may return tokens LI.FI cannot route

The new getWalletBalances function (src/utils/lifi.ts:454-503) fetches ALL token balances for a wallet via GET /wallets/{address}/balances, replacing the old approach of passing a curated token list to getTokenBalancesByChain. This means the bridge asset selector in Bridge.tsx may now show tokens that LI.FI has no swap/bridge route for. A user selecting such a token would get a 'unable to fetch bridge quote' error after entering an amount. The old code only showed tokens LI.FI explicitly supported. This behavioral change is likely intentional (broader asset visibility for recovery flows) but worth confirming.

Open in Devin Review

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2db4dd6a65

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

error: deploymentError,
isLoading: isCheckingDeployment,
} = useQuery({ ...deployedOptions(senderAddress, source?.chain), enabled: isRecovery });
const notDeployed = isRecovery && deployed === false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove undeployed-account block from recovery submit path

This marks recovery as unavailable whenever deployed === false, which blocks users whose exa account has balances on a supported chain but has never been deployed there yet. In this same commit, createAccountClient builds cross-chain accounts with getAccountInitCode and sends user operations on target chains, so first-use deployment is expected to be possible; treating notDeployed as a hard blocker prevents that first recovery transaction from ever being submitted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +438 to +441
if (classifyError(error).authKnown) {
setBridgePreview(undefined);
resetBridgeMutation();
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Auth-known bridge/transfer errors silently swallowed instead of reported to Sentry

The onError handlers for both executeBridge and executeTransfer mutations use classifyError(error).authKnown to detect auth-related errors and return early without calling reportError. Every other mutation error handler in the codebase (Repay.tsx:439, Repay.tsx:510, Swaps.tsx:371, Amount.tsx:153, Review.tsx:160, RollDebt.tsx:295, UpgradeAccount.tsx:106) uses the established pattern reportError(error).authKnown, which first reports the error to Sentry (as a warning-level event for auth-known errors) and then checks the classification. The new Bridge pattern skips Sentry reporting entirely for auth-known errors. This violates the codebase's established error-handling convention and creates an observability gap where passkey cancellations, wallet rejections, and bundle cancellations during bridge/transfer operations are invisible to monitoring.

established pattern vs new pattern

Established pattern (7 other files):

onError(error) {
  if (reportError(error).authKnown) resetMutation();
}

New Bridge pattern (2 handlers):

onError(error) {
  if (classifyError(error).authKnown) {
    setBridgePreview(undefined);
    resetBridgeMutation();
    return; // error NOT reported to Sentry
  }
  reportError(error);
  ...
}
Prompt for agents
The onError handlers for executeBridge (line 438) and executeTransfer (line 497) in Bridge.tsx use classifyError(error).authKnown which only classifies the error without reporting it to Sentry. Every other mutation error handler in the codebase uses reportError(error).authKnown which both reports to Sentry AND returns the classification. This creates an observability gap for auth-known errors (passkey cancellations, wallet rejections) during bridge and transfer operations.

To fix, replace classifyError(error).authKnown with reportError(error).authKnown in both the executeBridge onError (line 438) and executeTransfer onError (line 497). Keep the additional setBridgePreview(undefined) and reset calls which are a good UX improvement, but ensure the error still reaches Sentry as a warning-level event consistent with the rest of the codebase.
Open in Devin Review

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 003024cc80

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

export default function Portfolio() {
const { address } = useAccount();
const { averageRate, portfolio, totalBalanceUSD } = usePortfolio();
const { averageRate, portfolio, totalBalanceUSD, isPending } = usePortfolio();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep portfolio loading state until balances are loaded

This screen now renders the non-skeleton view as soon as isPending is false, but isPending from usePortfolio no longer includes the LI.FI balances fetch; if markets resolve first, the page shows a portfolio total and asset sections computed without external/cross-chain balances, which can temporarily underreport holdings and present incomplete data as final. Include isBalancesPending in the loading gate (or fold it back into isPending) so the first rendered totals are not partial.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 12 additional findings in Devin Review.

Open in Devin Review

address !== undefined && chainId !== undefined
? async () => !!(await getBytecode(exaConfig, { address, chainId }))
: skipToken,
staleTime: (query) => (query.state.data === undefined ? 0 : Infinity),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 staleTime: Infinity for deployed === false permanently caches "not deployed" status

The deployedOptions query sets staleTime: Infinity whenever data is not undefined — including when data === false. Combined with gcTime: Infinity and the fact that the "deployed" query key is NOT in the persistence exclusion list (queryClient.ts:77), a false result (account not yet deployed on a chain) is persisted to disk and never considered stale. If the user's account is later deployed on that chain, the app continues reading the cached false and shows "Non-supported network" permanently until the user clears app data.

Staleness logic detail

The condition query.state.data === undefined ? 0 : Infinity treats both true and false identically with Infinity staleTime. Only true (deployed) is a permanent state — false can change to true when the account is deployed on a new chain. The staleTime for false should be finite (or 0) so the query refetches periodically.

Suggested change
staleTime: (query) => (query.state.data === undefined ? 0 : Infinity),
staleTime: (query) => (query.state.data === true ? Infinity : 0),
Open in Devin Review

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

Comment thread src/utils/lifi.ts
})
: undefined,
]);
if (exa) balances[chain.id] = [exa, ...(balances[chain.id] ?? [])];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 EXA token may appear as duplicate in same-chain balances on Optimism

In balancesOptions (lifi.ts:93), the EXA token balance is explicitly fetched via getTokenBalancesByChain and prepended to balances[chain.id]. If the wallet balances API (getWalletBalances) also returns the EXA token, it would appear twice in the same-chain list. This could cause duplicate entries in externalAssets (usePortfolio) and sameChainAssets (Bridge). In practice, the LiFi wallet balances API may not include the EXA token (it's explicitly added because it's a smaller token), so duplicates may not manifest. If they do, the impact is cosmetic (duplicate rows in asset lists).

Open in Devin Review

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants