Skip to content

refactor: migrate RevokeDelegationPanel to meteorology gas + parallel multi-chain revoke#7171

Open
DanielSinclair wants to merge 5 commits intodevelopfrom
@daniel/7702-revoke-gas
Open

refactor: migrate RevokeDelegationPanel to meteorology gas + parallel multi-chain revoke#7171
DanielSinclair wants to merge 5 commits intodevelopfrom
@daniel/7702-revoke-gas

Conversation

@DanielSinclair
Copy link
Contributor

@DanielSinclair DanielSinclair commented Feb 24, 2026

What changed

Splits RevokeDelegationPanel into a presentation layer + two hooks, and migrates gas from the legacy useGas polling hook to meteorology.

  • RevokeDelegationPanel.tsx is now a pure presenter. All tx execution, state management, and gas logic moved out.
  • useRevokeDelegation (new) handles the full lifecycle: per-chain status tracking, native-balance preflight (skips chains with no gas instead of failing), parallel Promise.allSettled execution across all chains, single wallet load (one biometric prompt), and retry of only failed chains.
  • useRevokeDelegationGas (new) estimates fees across all chains using useMeteorologySuggestionMultichain + estimateGas, sums them into one display value with the same gas buffer the delegation SDK applies.
  • meteorology.ts adds useMeteorologyMultichain, useMeteorologySuggestionMultichain, and getMeteorologyCachedData for multi-chain and non-React cache reads.
  • types.ts (new) extracts RevokeReason, RevokeStatus, and ChainRevokeStatus so they're importable without pulling in the panel.
  • DevSection passes real delegations to the simulated revoke panel per reason (vulnerability/bug use Rainbow delegations, third-party uses third-party, etc.) instead of hardcoded { chainId: mainnet } fallbacks.
  • New i18n keys for multi-network gas display, insufficient gas, and estimating state.

Why

The old panel executed chains one at a time via component-level index state, prompted biometrics per chain, used the legacy gas hook, and had no concept of insufficient gas. This was slow for multi-chain revokes and had poor error UX.

Benefit

  • Parallel multi-chain revoke with a single biometric prompt
  • insufficientGas as a first-class state with clear messaging
  • Gas estimates match what the delegation SDK actually builds
  • Clean separation makes the panel trivially testable

@DanielSinclair
Copy link
Contributor Author

DanielSinclair commented Feb 24, 2026

@DanielSinclair DanielSinclair changed the title fix: pass real delegations to simulated revoke when possible refactor: migrate RevokeDelegationPanel to meteorology gas + parallel multi-chain revoke Feb 24, 2026
@DanielSinclair DanielSinclair marked this pull request as ready for review February 24, 2026 09:05
@DanielSinclair
Copy link
Contributor Author

/e2e

@github-actions
Copy link

Launch in simulator or device for 816e203

@github-actions
Copy link

Launch in simulator or device for 6875f0a

@github-actions
Copy link

Launch in simulator or device for a983faa

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

🧪 Flashlight Performance Report (AWS Device Farm)

🔀 Commit: d015096

📎 View Artifacts

Metric Current Δ vs Baseline
Time to Interactive (TTI) 5634 ms ⚪ +18.0 ms (+0.3%)
Average FPS 55.60 🔴 -1.2 (-2.1%)
Average RAM 384.8 MB ⚪ -2.4 MB (-0.6%)

@github-actions
Copy link

Launch in simulator or device for 9e3db4a

Base automatically changed from 7702 to develop February 26, 2026 05:50
…state management

fix: useEffect revoke panel dismissal

useMeteorologySuggestionMultichain

fix: stableDelegationsToRevoke

refactor: useRevokeDelegation

Simplify setChainStatus typing

Fix revoke preflight to read balances at action time
Add focused tests for partitionChainsByGasAvailability to lock in the new gas-eligibility behavior.

The suite verifies: (1) cached zero native balance is insufficient without RPC fallback, (2) missing cache falls back to provider balance, and (3) balance lookup failures stay actionable to avoid false negatives.
Revoke preflight previously treated chains as actionable when the native asset cache entry was missing. On cold or stale caches this could skip insufficient-gas classification and proceed into avoidable revoke failures.

This change keeps existing cached-balance behavior, but when native asset data is unavailable it now falls back to provider.getBalance(address) per chain before partitioning actionable vs insufficientGas chains.

The hook now runs this partition asynchronously and passes address into the preflight helper, so behavior stays minimal and localized to gas eligibility checks.
@DanielSinclair DanielSinclair force-pushed the @daniel/7702-revoke-gas branch from 028ebcb to a91caff Compare February 26, 2026 20:01
@github-actions
Copy link

Launch in simulator or device for d015096

Copy link
Contributor

@maxbbb maxbbb left a comment

Choose a reason for hiding this comment

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

Looks good, just a few small things / questions.

Comment on lines +114 to +116
staleTime: staleTime ?? 12_000, // 12 seconds
cacheTime: cacheTime ?? 36_000, // 36 seconds
refetchInterval: refetchInterval ?? 12_000, // 12 seconds
Copy link
Contributor

Choose a reason for hiding this comment

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

super nit: would pull these into constants at top of function defined with time util from utils/time since they're shared between this and the regular useMeteorology

Comment on lines +24 to +62
export async function partitionChainsByGasAvailability({
chains,
address,
getNativeAssetBalance,
getChainBalance,
}: PartitionGasChainsArgs): Promise<{ insufficientGasChainIds: number[]; actionableChains: ChainWithId[] }> {
const insufficientGasChainIds: number[] = [];
const actionableChains: ChainWithId[] = [];

for (const d of chains) {
const chainId = d.chainId as ChainId;
const nativeAssetBalance = getNativeAssetBalance(chainId);

if (nativeAssetBalance != null && Number(nativeAssetBalance) <= 0) {
insufficientGasChainIds.push(d.chainId);
continue;
}
if (nativeAssetBalance != null && Number(nativeAssetBalance) > 0) {
actionableChains.push(d);
continue;
}

if (!address) {
actionableChains.push(d);
continue;
}

try {
const onchainBalance = await getChainBalance(chainId, address);
if (onchainBalance <= 0n) insufficientGasChainIds.push(d.chainId);
else actionableChains.push(d);
} catch {
// If balance fetch fails, keep chain actionable and let execution return the real failure reason.
actionableChains.push(d);
}
}

return { insufficientGasChainIds, actionableChains };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

a few nits:

  • Only thing this effects is the fallback case, so not that consequential, but ideally this is done in a Promise.all or Promise.allSettled so that you're not doing sequential fetches.
  • I'm not sure of other places in the app where we do a fallback direct chain lookup for balance, it might be harmless, but is it possible there are downstream consequences when this case is hit?
  • Not sure there is a good reason to have one return type as number[] and the other ChainWithId[], vs both just being one or the other.

Comment on lines +105 to +117
setChainStatuses(prev => {
const next = { ...prev };
for (const cid of chainIds) {
if (status === 'insufficientGas' && next[cid] === 'success') continue;
next[cid] = status as ChainRevokeStatus;
}
return next;
});
}, []);

const mergeChainStatuses = useCallback((results: Record<number, ChainRevokeStatus>) => {
setChainStatuses(prev => ({ ...prev, ...results }));
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

These are doing basically the same thing, apart from the if (status === 'insufficientGas' && next[cid] === 'success') continue;, could they be combined?


const handleRetry = useCallback(async () => {
const failedChains = delegationsToRevoke.filter(d => chainStatuses[d.chainId] === 'error');
if (failedChains.length === 0) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

this condition is handled by revokeChains, do we need to also handle it here?

Comment on lines +269 to +275
const onSuccessRef = useRef(onSuccess);
onSuccessRef.current = onSuccess;
useEffect(() => {
if (revokeStatus === 'success') {
onSuccessRef.current?.();
}
}, [revokeStatus]);
Copy link
Contributor

Choose a reason for hiding this comment

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

You could just use onSuccess directly and add // eslint-disable-next-line react-hooks/exhaustive-deps

chainStatuses[cid] = 'error';
anyFailed = true;
logger.error(new RainbowError('Failed to revoke delegation'), {
error: r.reason,
Copy link
Contributor

Choose a reason for hiding this comment

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

r.reason should be passed as the second argument to RainbowError. Error in the object goes to Sentry as metadata and it often gets dropped, plus we lose the stack trace.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants