Skip to content

Commit fda9c01

Browse files
authored
Merge pull request #4 from agglayer/asam/fix-claim-flow
Update claim flow
2 parents 2d91ff8 + bd29856 commit fda9c01

10 files changed

Lines changed: 214 additions & 57 deletions

File tree

app/components/bridge/bridge-transaction-modal/bridge-success-view.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation';
66
import { Button } from '@/app/components/ui/button';
77
import { CopyText } from '@/app/components/copyText';
88
import { ROUTES } from '@/app/constants/routes';
9+
import { useRefetch } from '@/app/context/refetch';
910
import { shortenAddress } from '@/app/utils/address';
1011

1112
interface BridgeSuccessViewProps {
@@ -16,11 +17,13 @@ interface BridgeSuccessViewProps {
1617

1718
export const BridgeSuccessView = ({ hash, explorerUrl, onClose }: BridgeSuccessViewProps) => {
1819
const router = useRouter();
20+
const { triggerAggressiveRefetch } = useRefetch();
1921

2022
const handleGoToTransactions = useCallback(() => {
23+
triggerAggressiveRefetch();
2124
onClose();
2225
router.push(ROUTES.TRANSACTIONS);
23-
}, [onClose, router]);
26+
}, [onClose, router, triggerAggressiveRefetch]);
2427

2528
return (
2629
<div className="flex flex-col gap-3">

app/components/mode-switch.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ export const ModeSwitch = () => {
2121
.map((value) => ({
2222
value,
2323
label: APP_MODE_CONFIG[value].label,
24-
}))
25-
.slice(0, 2),
24+
})),
2625
[enabledModes, mode],
2726
);
2827

app/components/transactions/transactions-details-modal/transaction-details-modal-header.tsx

Lines changed: 9 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,44 +17,17 @@ export const TxDetailsHeader = ({
1717
formattedAmount,
1818
}: TxDetailsHeaderProps) => {
1919
return (
20-
<div className="w-full rounded-xl border border-border bg-surface-muted px-4 py-4 shadow-xs">
21-
<div className="flex items-center gap-3">
22-
<div className="min-w-0 flex-1">
23-
<div className="text-[10px] font-semibold uppercase tracking-wide text-muted/80">From</div>
24-
<div className="mt-1 flex min-w-0 items-center gap-2">
25-
{sourceChain?.icon && (
26-
<BadgeImageFallback
27-
src={sourceChain.icon}
28-
size="sm"
29-
fallbackText={sourceChain.name}
30-
className="shrink-0"
31-
/>
32-
)}
33-
<span className="truncate font-semibold text-black">{sourceChain?.name ?? '-'}</span>
34-
</div>
35-
</div>
36-
<div className="flex shrink-0 items-center gap-2">
37-
<span className="h-2 w-2 rounded-full bg-border" />
38-
<span className="h-px w-12 bg-border" />
39-
<ArrowRight className="size-6 text-muted" />
40-
<span className="h-px w-12 bg-border" />
41-
<span className="h-2 w-2 rounded-full bg-border" />
42-
</div>
43-
<div className="min-w-0 flex-1 text-right">
44-
<div className="text-[10px] font-semibold uppercase tracking-wide text-muted/80">To</div>
45-
<div className="mt-1 flex min-w-0 items-center justify-end gap-2">
46-
<span className="truncate font-semibold text-black">{destChain?.name ?? '-'}</span>
47-
{destChain?.icon && (
48-
<BadgeImageFallback src={destChain.icon} size="sm" fallbackText={destChain.name} className="shrink-0" />
49-
)}
50-
</div>
51-
</div>
20+
<div className="w-full rounded-xl border border-border bg-surface-muted p-5 flex flex-col items-center text-center">
21+
<div className="flex items-center gap-2 text-sm">
22+
<span className="font-medium text-foreground">{sourceChain?.name ?? '-'}</span>
23+
<ArrowRight className="size-4 text-muted" />
24+
<span className="font-medium text-foreground">{destChain?.name ?? '-'}</span>
5225
</div>
53-
<div className="flex items-center justify-center gap-2">
54-
{tokenLogo && <BadgeImageFallback src={tokenLogo} size="md" fallbackText={tokenSymbol} />}
55-
<div className="text-2xl font-bold text-black">
26+
<div className="mt-4 flex items-center gap-2.5">
27+
{tokenLogo && <BadgeImageFallback src={tokenLogo} size="lg" fallbackText={tokenSymbol} />}
28+
<span className="text-3xl font-bold text-foreground">
5629
{formattedAmount} {tokenSymbol}
57-
</div>
30+
</span>
5831
</div>
5932
</div>
6033
);

app/components/transactions/transactions-view.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useCallback, useMemo, useState } from 'react';
3+
import { useCallback, useEffect, useMemo, useState } from 'react';
44
import type { Hex } from 'viem';
55
import { useQueryClient } from '@tanstack/react-query';
66
import { AlertTriangle, Plug, RotateCw } from 'lucide-react';
@@ -9,11 +9,12 @@ import { Button } from '@/app/components/ui/button';
99
import { TransactionFilters } from '@/app/components/transactions/transaction-filters';
1010
import { TransactionList } from '@/app/components/transactions/transaction-list';
1111
import { ClaimResultModal } from '@/app/components/transactions/claim-result-modal';
12-
import { useTransactions } from '@/app/hooks/useTransactions';
12+
import { TOTAL_REFETCH_TIME, useTransactions } from '@/app/hooks/useTransactions';
1313
import { useClaimExecution } from '@/app/hooks/useClaimExecution';
1414
import { useEnforceCorrectChain } from '@/app/hooks/useEnforceCorrectChain';
1515
import { useWallet } from '@/app/context/wallet';
1616
import { useAppMode } from '@/app/context/app-mode';
17+
import { useRefetch } from '@/app/context/refetch';
1718
import type { TransactionStatus, Transaction } from '@/app/types/transaction';
1819
import { getTransactionInitialStatus } from '@/app/components/transactions/intialStatus';
1920
import { TransactionDetailsModal } from '@/app/components/transactions/transactions-details-modal/transaction-details-modal';
@@ -23,6 +24,7 @@ import { cn } from '@/app/utils/common';
2324
export const TransactionsView = () => {
2425
const { address, status, chainId, connect } = useWallet();
2526
const { defaultFromChainId, chains, bridgeAddress } = useAppMode();
27+
const { aggressiveRefetch, triggerAggressiveRefetch, clearAggressiveRefetch } = useRefetch();
2628
const queryClient = useQueryClient();
2729
const initialStatus = getTransactionInitialStatus();
2830
const [filters, setFilters] = useState<{ status?: TransactionStatus; updatedSince?: number }>(() => ({
@@ -51,12 +53,21 @@ export const TransactionsView = () => {
5153
chainId: effectiveChainId,
5254
filters: queryFilters,
5355
enabled: isConnected,
56+
aggressiveRefetch,
5457
});
5558

59+
// Auto-disable aggressive mode after burst completes
60+
useEffect(() => {
61+
if (!aggressiveRefetch) return;
62+
const timeout = setTimeout(clearAggressiveRefetch, TOTAL_REFETCH_TIME);
63+
return () => clearTimeout(timeout);
64+
}, [aggressiveRefetch, clearAggressiveRefetch]);
65+
5666
const handleClaimComplete = useCallback(() => {
67+
triggerAggressiveRefetch();
5768
refetch();
5869
queryClient.invalidateQueries({ queryKey: ['ready-to-claim-count'] });
59-
}, [queryClient, refetch]);
70+
}, [queryClient, refetch, triggerAggressiveRefetch]);
6071

6172
const ensureCorrectChain = useEnforceCorrectChain();
6273
const claimExecution = useClaimExecution({

app/context/refetch.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client';
2+
3+
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react';
4+
5+
type RefetchContextValue = {
6+
aggressiveRefetch: boolean;
7+
triggerAggressiveRefetch: () => void;
8+
clearAggressiveRefetch: () => void;
9+
};
10+
11+
const RefetchContext = createContext<RefetchContextValue | null>(null);
12+
13+
export const RefetchProvider = ({ children }: { children: ReactNode }) => {
14+
const [aggressiveRefetch, setAggressiveRefetch] = useState(false);
15+
16+
const triggerAggressiveRefetch = useCallback(() => {
17+
setAggressiveRefetch(true);
18+
}, []);
19+
20+
const clearAggressiveRefetch = useCallback(() => {
21+
setAggressiveRefetch(false);
22+
}, []);
23+
24+
return (
25+
<RefetchContext.Provider value={{ aggressiveRefetch, triggerAggressiveRefetch, clearAggressiveRefetch }}>
26+
{children}
27+
</RefetchContext.Provider>
28+
);
29+
};
30+
31+
export const useRefetch = () => {
32+
const context = useContext(RefetchContext);
33+
if (!context) throw new Error('useRefetch must be used within RefetchProvider');
34+
return context;
35+
};

app/hooks/useClaimExecution.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import type { Hex } from 'viem';
55
import { useConfig, useSendTransaction } from 'wagmi';
66
import { getPublicClient } from '@wagmi/core';
77
import { useAggNative } from '@/app/context/aggLayerSdk';
8+
import { useAppMode } from '@/app/context/app-mode';
9+
import { fetchClaimProof } from '@/app/services/claim-proof';
810
import type { ClaimExecutionResult, ClaimExecutionState, Transaction } from '@/app/types/transaction';
911
import type { AppChain } from '@/app/types/app-mode';
10-
import { mapTransactionRequest, resolveLeafIndex } from '@/app/utils/transaction';
12+
import { buildClaimAssetParams, mapTransactionRequest, resolveLeafIndex } from '@/app/utils/transaction';
1113

1214
interface UseClaimExecutionParams {
1315
bridgeAddress?: string;
@@ -20,6 +22,7 @@ export const useClaimExecution = (params: UseClaimExecutionParams) => {
2022
const { bridgeAddress, address, onComplete } = params;
2123
const native = useAggNative();
2224
const config = useConfig();
25+
const { mode } = useAppMode();
2326
const { sendTransactionAsync } = useSendTransaction();
2427

2528
const [state, setState] = useState<ClaimExecutionState>({
@@ -85,13 +88,15 @@ export const useClaimExecution = (params: UseClaimExecutionParams) => {
8588
return;
8689
}
8790

88-
const claimTx = await bridge.buildClaimAssetFromHash(
89-
transaction.transactionHash,
90-
transaction.sourceNetwork,
91+
const proof = await fetchClaimProof({
92+
mode,
93+
sourceNetworkId: transaction.sourceNetwork,
9194
leafIndex,
92-
0,
93-
address,
94-
);
95+
depositCount: transaction.depositCount,
96+
});
97+
98+
const claimParams = buildClaimAssetParams({ transaction, proof });
99+
const claimTx = await bridge.buildClaimAsset(claimParams, address);
95100

96101
localClaimHash = await sendTransactionAsync({
97102
...mapTransactionRequest(claimTx, address),
@@ -160,7 +165,7 @@ export const useClaimExecution = (params: UseClaimExecutionParams) => {
160165
});
161166
}
162167
},
163-
[address, bridgeAddress, native, config, sendTransactionAsync, onComplete],
168+
[address, bridgeAddress, native, config, sendTransactionAsync, onComplete, mode],
164169
);
165170

166171
const reset = useCallback(() => {

app/hooks/useTransactions.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
11
'use client';
22

3-
import { useMemo } from 'react';
3+
import { useEffect, useMemo, useRef } from 'react';
44
import { type InfiniteData, useInfiniteQuery } from '@tanstack/react-query';
55
import { fetchTransactions } from '@/app/services/transactions';
66
import type { TransactionFilters, TransactionsResponse } from '@/app/types/transaction';
77
import { useAppMode } from '@/app/context/app-mode';
88

9-
export const useTransactions = (params: { chainId?: number; filters?: TransactionFilters; enabled?: boolean }) => {
10-
const { chainId, filters = {}, enabled = true } = params;
9+
const REFETCH_INTERVALS = [500, 1000, 2000, 3000];
10+
export const TOTAL_REFETCH_TIME = REFETCH_INTERVALS.reduce((acc, curr) => acc + curr, 0);
11+
12+
export const useTransactions = (params: {
13+
chainId?: number;
14+
filters?: TransactionFilters;
15+
enabled?: boolean;
16+
aggressiveRefetch?: boolean;
17+
}) => {
18+
const { chainId, filters = {}, enabled = true, aggressiveRefetch = false } = params;
1119
const { mode } = useAppMode();
1220
const filtersKey = useMemo(() => JSON.stringify(filters ?? {}), [filters]);
1321

22+
const fetchCountRef = useRef(0);
23+
const prevAggressiveRef = useRef(aggressiveRefetch);
24+
25+
// Reset counter when aggressiveRefetch transitions from false -> true
26+
useEffect(() => {
27+
if (aggressiveRefetch && !prevAggressiveRef.current) {
28+
fetchCountRef.current = 0;
29+
}
30+
prevAggressiveRef.current = aggressiveRefetch;
31+
}, [aggressiveRefetch]);
32+
1433
return useInfiniteQuery<
1534
TransactionsResponse,
1635
Error,
@@ -22,16 +41,30 @@ export const useTransactions = (params: { chainId?: number; filters?: Transactio
2241
enabled: enabled && Boolean(chainId),
2342
queryFn: async ({ pageParam }) => {
2443
if (!chainId) throw new Error('MISSING_CHAIN_ID');
25-
return fetchTransactions({
44+
const data = await fetchTransactions({
2645
mode,
2746
filters: {
2847
...filters,
2948
startAfter: pageParam,
3049
},
3150
});
51+
// only increment fetch count on initial load pages not on subsequent pages
52+
if (!pageParam && aggressiveRefetch) {
53+
fetchCountRef.current++;
54+
}
55+
return data;
3256
},
3357
getNextPageParam: (lastPage) => lastPage.pagination.nextStartAfterCursor,
3458
initialPageParam: undefined,
35-
staleTime: 30 * 1000, // 30 seconds
59+
staleTime: 30 * 1000,
60+
refetchInterval: (query) => {
61+
if (!aggressiveRefetch) return false;
62+
if (query.state.status === 'error') return false;
63+
64+
const count = fetchCountRef.current;
65+
if (count >= REFETCH_INTERVALS.length) return false;
66+
67+
return REFETCH_INTERVALS[count];
68+
},
3669
});
3770
};

app/providers.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ReactNode } from 'react';
22
import { AggLayerSDKProvider } from '@/app/context/aggLayerSdk';
33
import { AppModeProvider } from '@/app/context/app-mode';
4+
import { RefetchProvider } from '@/app/context/refetch';
45
import { TokenProvider } from '@/app/context/token';
56
import { WalletProvider } from '@/app/context/wallet';
67

@@ -9,7 +10,9 @@ export const Providers = ({ children }: { children: ReactNode }) => {
910
<AppModeProvider>
1011
<WalletProvider>
1112
<AggLayerSDKProvider>
12-
<TokenProvider>{children}</TokenProvider>
13+
<RefetchProvider>
14+
<TokenProvider>{children}</TokenProvider>
15+
</RefetchProvider>
1316
</AggLayerSDKProvider>
1417
</WalletProvider>
1518
</AppModeProvider>

app/services/claim-proof.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { Hex } from 'viem';
2+
import { AppMode } from '@/app/types/app-mode';
3+
import { getBridgeHubApiBaseUrl } from '@/app/utils/app-mode';
4+
5+
export type ClaimProof = {
6+
proof_local_exit_root: Hex[];
7+
proof_rollup_exit_root: Hex[];
8+
l1_info_tree_leaf: {
9+
block_num: number;
10+
block_pos: number;
11+
l1_info_tree_index: number;
12+
previous_block_hash: string;
13+
timestamp: number;
14+
mainnet_exit_root: Hex;
15+
rollup_exit_root: Hex;
16+
global_exit_root: Hex;
17+
hash: Hex;
18+
};
19+
};
20+
21+
type ClaimProofResponse = {
22+
status: 'success' | 'error';
23+
data?: ClaimProof;
24+
error?: string;
25+
};
26+
27+
const buildClaimProofUrl = (params: {
28+
mode: AppMode;
29+
sourceNetworkId: number;
30+
leafIndex: number;
31+
depositCount: number;
32+
}): string => {
33+
const url = new URL(`${getBridgeHubApiBaseUrl(params.mode)}/claim-proof`);
34+
url.searchParams.set('sourceNetworkId', params.sourceNetworkId.toString());
35+
url.searchParams.set('leafIndex', params.leafIndex.toString());
36+
url.searchParams.set('depositCount', params.depositCount.toString());
37+
return url.toString();
38+
};
39+
40+
export const fetchClaimProof = async (params: {
41+
mode: AppMode;
42+
sourceNetworkId: number;
43+
leafIndex: number;
44+
depositCount: number;
45+
}): Promise<ClaimProof> => {
46+
const url = buildClaimProofUrl(params);
47+
const res = await fetch(url, {
48+
headers: { accept: 'application/json' },
49+
});
50+
51+
if (!res.ok) {
52+
const msg = await res.text().catch(() => '');
53+
throw new Error(`CLAIM_PROOF_${res.status}: ${msg || 'Request failed'}`);
54+
}
55+
56+
const json: ClaimProofResponse = await res.json();
57+
if (json.status !== 'success' || !json.data) {
58+
throw new Error(json.error || 'CLAIM_PROOF_INVALID_RESPONSE');
59+
}
60+
61+
return json.data;
62+
};

0 commit comments

Comments
 (0)