|
1 | 1 | 'use client' |
2 | | -import React, { useCallback, useEffect, useState } from 'react' |
3 | | -import { useForm } from 'react-hook-form' |
4 | | -import { useAccount } from 'wagmi' |
| 2 | +import React, { useCallback, useEffect, useEffectEvent, useState } from 'react' |
| 3 | +import { FormProvider, useForm, useWatch } from 'react-hook-form' |
| 4 | +import { useConnection } from 'wagmi' |
5 | 5 | import './i18initializer' |
6 | 6 |
|
7 | 7 | import { BridgeFormValues } from '@/components/bridge-input' |
8 | 8 | import { Header } from '@/components/header' |
9 | 9 | import { MainComponent } from '@/components/main' |
10 | 10 | import { MainModal } from '@/components/modals/main-modal' |
11 | 11 | import { TransactionDetailsModal } from '@/components/modals/transaction-details-modal' |
12 | | -import { Network } from '@/components/network-box' |
13 | 12 | import { useBridgeFees } from '@/hooks/use-bridge-fees' |
14 | 13 | import { useBridgeToEthereum } from '@/hooks/use-bridge-to-ethereum' |
15 | | -import { useBridgeToTari } from '@/hooks/use-bridge-to-tari' |
| 14 | + |
16 | 15 | import { useBridgeTransaction } from '@/hooks/use-bridge-transaction' |
17 | | -import useTariAccountStore from '@/store/account' |
18 | | -import { TokensUnwrappedService, UserTransactionDTO } from '@tari-project/wxtm-bridge-backend-api' |
19 | | -import { DeployedChains } from '@tari-project/wxtm-bridge-contracts/deployments' |
| 16 | +import { setDetailedTx, setLastOngoingBridgeTx, useTariAccountStore } from '@/store/account' |
| 17 | +import { UserTransactionDTO } from '@tari-project/wxtm-bridge-backend-api' |
20 | 18 | import { FooterText } from '@/components/main/footer-text' |
21 | | -import useBridgeStore from '@/store/bridge' |
22 | | -import { microXtmToXtm } from '@/utils/parse-wxtm-token-amount' |
| 19 | +import { setUnwrapFailed, useBridgeStore } from '@/store/bridge' |
| 20 | +import { useFetchDailyLimit } from '@/hooks/use-fetch-daily-limit' |
| 21 | +import { setIsModalOpen, setModalStep, useModalStore } from '@/store/modal' |
23 | 22 |
|
24 | | -const DAILY_LIMIT_ERROR = 'Daily wrap limit exceeded' |
25 | | -const DAILY_LIMIT_ERROR_TYPE = 'Forbidden' |
26 | | -const REFETCH_LIMIT_INTERVAL = 30000 |
| 23 | +const REFETCH_LIMIT_INTERVAL = 30 * 1000 // 30 sec |
27 | 24 |
|
28 | 25 | export default function Home() { |
29 | | - const { isConnected, chain, address: ethAddress } = useAccount() |
30 | | - const [hasFetchedParams, setHasFetchedParams] = useState(false) |
31 | | - const [modalOpen, setModalOpen] = useState(false) |
32 | | - const [modalStep, setModalStep] = useState<number>(1) |
33 | | - const [fromNetwork, setFromNetwork] = useState<Network>({ |
34 | | - name: 'Tari', |
35 | | - icon: '/icons/tari.png', |
36 | | - }) |
37 | | - const [toNetwork, setToNetwork] = useState<Network>({ |
38 | | - name: 'Ethereum', |
39 | | - icon: '/icons/eth.png', |
40 | | - }) |
41 | | - const [isUnwrapping, setIsUnwrapping] = useState(false) |
42 | | - const [isUnwrappingFailed, setIsUnwrappingFailed] = useState(false) |
43 | | - const [remainingDailyLimit, setRemainingDailyLimit] = useState<number | undefined>(undefined) |
44 | | - |
45 | | - const chainId = (chain?.id ?? 1) as DeployedChains |
46 | | - |
47 | | - const { bridgeToEthereum, getBridgeTxParams } = useBridgeToEthereum() |
48 | | - const { bridgeToTari, isPending, isSuccess, isError, error } = useBridgeToTari(ethAddress || '0x', chainId) |
49 | | - const { getUserBackendBridgeTxs } = useBridgeTransaction() |
50 | | - const tariAccount = useTariAccountStore((s) => s.tariAccount) |
51 | | - const setExceededDailyLimit = useTariAccountStore((s) => s.setExceededDailyLimit) |
52 | | - const setTariAccount = useTariAccountStore((s) => s.setTariAccount) |
| 26 | + const modalStep = useModalStore((s) => s.modalStep) |
| 27 | + const isModalOpen = useModalStore((s) => s.isModalOpen) |
53 | 28 | const detailedTx = useTariAccountStore((s) => s.detailedTx) |
54 | | - const setDetailedTx = useTariAccountStore((s) => s.setDetailedTx) |
| 29 | + const tariAccount = useTariAccountStore((s) => s.tariAccount) |
55 | 30 | const ongoingBridgeTx = useTariAccountStore((s) => s.ongoingBridgeTx) |
56 | | - const setLastOngoingBridgeTx = useTariAccountStore((s) => s.setLastOngoingBridgeTx) |
57 | 31 | const tariColdWalletAddress = useBridgeStore((s) => s.tariColdWalletAddress) |
58 | 32 | const wrapTokenFeePercentageBps = useBridgeStore((s) => s.wrapTokenFeePercentageBps) |
| 33 | + const fromNetwork = useBridgeStore((s) => s.fromNetwork) |
| 34 | + const isUnwrappingFailed = useBridgeStore((s) => s.unwrapFailed) |
| 35 | + const unwrapSuccess = useBridgeStore((s) => s.unwrapSuccess) |
| 36 | + |
| 37 | + const fetchDailyLimit = useFetchDailyLimit() |
| 38 | + const { isConnected, address: ethAddress } = useConnection() |
| 39 | + const { getUserBackendBridgeTxs } = useBridgeTransaction() |
| 40 | + const { getBridgeTxParams } = useBridgeToEthereum() |
| 41 | + const methods = useForm<BridgeFormValues>({ |
| 42 | + defaultValues: { amount: '' }, |
| 43 | + mode: 'onChange', |
| 44 | + }) |
| 45 | + const { control, resetField } = methods |
| 46 | + const amount = useWatch({ control, name: 'amount' }) |
| 47 | + |
| 48 | + const [hasFetchedParams, setHasFetchedParams] = useState(false) |
| 49 | + const [remainingDailyLimit, setRemainingDailyLimit] = useState<number | undefined>(undefined) |
59 | 50 |
|
60 | 51 | // Prevent main modal from showing when transaction details modal is active |
61 | 52 | const showModalDetailedTx = !!detailedTx |
62 | 53 | const showModalOngoingTx = ongoingBridgeTx && ongoingBridgeTx.showModal |
63 | | - |
64 | 54 | const isFailed = ongoingBridgeTx?.status === UserTransactionDTO.status.TIMEOUT || isUnwrappingFailed |
65 | 55 | const isWrapSuccess = ongoingBridgeTx?.status === UserTransactionDTO.status.SUCCESS |
66 | 56 |
|
67 | | - const { |
68 | | - watch, |
69 | | - control, |
70 | | - setValue, |
71 | | - formState: { errors, isValid }, |
72 | | - resetField, |
73 | | - } = useForm<BridgeFormValues>({ |
74 | | - defaultValues: { amount: '' }, |
75 | | - mode: 'onChange', |
76 | | - }) |
77 | | - |
78 | | - const amount = watch('amount') |
79 | 57 | const decimals = fromNetwork.name === 'Tari' ? 6 : 18 |
80 | 58 | const feesData = useBridgeFees(amount, decimals) |
81 | 59 |
|
| 60 | + const fetchUserTransactions = useCallback(async () => { |
| 61 | + if (tariAccount) { |
| 62 | + try { |
| 63 | + await getUserBackendBridgeTxs() |
| 64 | + } catch (error) { |
| 65 | + console.error('[ TAPPLET-BRIDGE ] Failed to get user transactions:', error) |
| 66 | + } |
| 67 | + } |
| 68 | + }, [getUserBackendBridgeTxs, tariAccount]) |
| 69 | + |
| 70 | + const onNetworkChange = useEffectEvent(() => setRemainingDailyLimit(undefined)) |
| 71 | + const onFetchedParams = useEffectEvent((hasFetched: boolean) => setHasFetchedParams(hasFetched)) |
| 72 | + |
82 | 73 | useEffect(() => { |
83 | 74 | if (!tariAccount || hasFetchedParams) return |
84 | 75 | const fetchBridgeTxParams = async () => { |
85 | 76 | try { |
86 | 77 | await getBridgeTxParams() |
| 78 | + setHasFetchedParams(true) |
87 | 79 | } catch (error) { |
88 | 80 | console.error('[ TAPPLET-BRIDGE ] Failed to get bridge transaction params:', error) |
89 | 81 | } |
90 | 82 | } |
91 | 83 |
|
92 | | - fetchBridgeTxParams().then(() => { |
93 | | - setHasFetchedParams(true) |
94 | | - }) |
95 | | - }, [getBridgeTxParams, hasFetchedParams, tariAccount]) |
| 84 | + fetchBridgeTxParams().then( |
| 85 | + async () => await fetchUserTransactions(), //initial |
| 86 | + ) |
| 87 | + }, [fetchUserTransactions, getBridgeTxParams, hasFetchedParams, tariAccount]) |
96 | 88 |
|
97 | 89 | useEffect(() => { |
98 | | - setHasFetchedParams(!!tariColdWalletAddress?.length || !!wrapTokenFeePercentageBps) |
| 90 | + const hasFetched = Boolean(!!tariColdWalletAddress?.length || !!wrapTokenFeePercentageBps) |
| 91 | + onFetchedParams(hasFetched) |
99 | 92 | }, [tariColdWalletAddress?.length, wrapTokenFeePercentageBps]) |
100 | 93 |
|
101 | 94 | useEffect(() => { |
102 | | - if (!tariAccount) return |
103 | | - |
104 | | - const fetchUserTransactions = async () => { |
105 | | - try { |
106 | | - await getUserBackendBridgeTxs() |
107 | | - await setTariAccount() |
108 | | - } catch (error) { |
109 | | - console.error('[ TAPPLET-BRIDGE ] Failed to get user transactions:', error) |
110 | | - } |
111 | | - } |
112 | | - |
113 | | - fetchUserTransactions() |
114 | | - // Poll every 30 sec |
115 | | - const intervalId = setInterval(fetchUserTransactions, 30000) |
116 | | - |
| 95 | + // Poll every 5 min |
| 96 | + const intervalId = setInterval(fetchUserTransactions, 1000 * 60 * 5) |
117 | 97 | return () => { |
118 | 98 | clearInterval(intervalId) |
119 | 99 | } |
120 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
121 | | - }, [tariAccount?.address]) |
| 100 | + }, [fetchUserTransactions]) |
122 | 101 |
|
123 | 102 | useEffect(() => { |
124 | | - if (modalOpen && modalStep === 0 && isConnected) { |
125 | | - setModalOpen(false) |
| 103 | + if (isModalOpen && modalStep === 0 && isConnected) { |
| 104 | + setIsModalOpen(false) |
126 | 105 | setModalStep(1) |
127 | | - } else if (!showModalDetailedTx && showModalOngoingTx && (isSuccess || ongoingBridgeTx.type === 'wrap')) { |
128 | | - setModalStep(2) |
129 | | - setModalOpen(true) |
130 | | - } else if (showModalDetailedTx && modalOpen) { |
131 | | - setModalOpen(false) |
132 | | - } |
133 | | - }, [isConnected, modalOpen, modalStep, isSuccess, ongoingBridgeTx, showModalDetailedTx, showModalOngoingTx]) |
134 | | - |
135 | | - useEffect(() => { |
136 | | - if (isUnwrapping) { |
137 | | - console.debug(`[ TAPPLET-BRIDGE ] Initiating transaction...`) |
138 | | - setModalStep(3) |
139 | | - } |
140 | | - }, [isUnwrapping]) |
141 | | - |
142 | | - useEffect(() => { |
143 | | - if (isSuccess) { |
144 | | - console.debug(`[ TAPPLET-BRIDGE ] Unwrap transaction success!`) |
145 | | - setIsUnwrapping(false) |
| 106 | + } else if (!showModalDetailedTx && showModalOngoingTx && (unwrapSuccess || ongoingBridgeTx.type === 'wrap')) { |
146 | 107 | setModalStep(2) |
147 | | - } else if (isError) { |
148 | | - console.error(`[ TAPPLET-BRIDGE ] Unwrap transaction failed:`, error) |
149 | | - setIsUnwrapping(false) |
150 | | - setIsUnwrappingFailed(true) |
| 108 | + setIsModalOpen(true) |
| 109 | + } else if (showModalDetailedTx && isModalOpen) { |
| 110 | + setIsModalOpen(false) |
151 | 111 | } |
152 | | - }, [isPending, isSuccess, isError, error]) |
| 112 | + }, [isConnected, isModalOpen, modalStep, ongoingBridgeTx, showModalDetailedTx, showModalOngoingTx, unwrapSuccess]) |
153 | 113 |
|
154 | 114 | useEffect(() => { |
155 | 115 | if (fromNetwork.name === 'Tari') { |
156 | | - setRemainingDailyLimit(undefined) |
| 116 | + onNetworkChange() |
157 | 117 | return |
158 | 118 | } |
159 | | - |
160 | | - const fetchDailyLimit = async () => { |
161 | | - try { |
162 | | - const limitMicro = await TokensUnwrappedService.getRemainingDailyLimit() |
163 | | - const limitXtm = microXtmToXtm(limitMicro) |
164 | | - setRemainingDailyLimit(limitXtm) |
165 | | - } catch (error) { |
166 | | - console.error('[ TAPPLET-BRIDGE ] Failed to fetch daily limit:', error) |
167 | | - } |
168 | | - } |
169 | | - |
170 | | - fetchDailyLimit() |
171 | | - |
172 | 119 | const interval = setInterval(fetchDailyLimit, REFETCH_LIMIT_INTERVAL) |
173 | 120 | return () => clearInterval(interval) |
174 | | - }, [fromNetwork.name]) |
| 121 | + }, [fetchDailyLimit, fromNetwork.name]) |
175 | 122 |
|
176 | | - const handleConnectClick = () => { |
177 | | - if (!isConnected) { |
178 | | - setModalStep(0) |
179 | | - setModalOpen(true) |
180 | | - } |
181 | | - } |
182 | | - |
183 | | - const handleContinueClick = () => { |
184 | | - setModalStep(1) |
185 | | - setModalOpen(true) |
186 | | - } |
187 | 123 | const handleSetOngoingModalOpen = (open: boolean) => { |
188 | | - setModalOpen(open) |
| 124 | + setIsModalOpen(open) |
189 | 125 | if (ongoingBridgeTx) setLastOngoingBridgeTx({ ...ongoingBridgeTx, showModal: false }) |
190 | 126 | } |
191 | | - |
192 | | - const handleBridgeToEthereum = useCallback(() => { |
193 | | - if (!amount || !ethAddress) { |
194 | | - return |
195 | | - } |
196 | | - |
197 | | - bridgeToEthereum({ |
198 | | - amount, |
199 | | - ethAddress: ethAddress, |
200 | | - amountAfterFee: feesData.amountAfterFee, |
201 | | - }) |
202 | | - .then(async () => { |
203 | | - await getUserBackendBridgeTxs() |
204 | | - setExceededDailyLimit(false) |
205 | | - }) |
206 | | - .catch((e) => { |
207 | | - console.error('[ TAPPLET-BRIDGE ] Bridge operation failed:', e) |
208 | | - const error = e as Error |
209 | | - const isLimitError = |
210 | | - error?.message?.includes(DAILY_LIMIT_ERROR_TYPE) || error?.message?.includes(DAILY_LIMIT_ERROR) |
211 | | - setExceededDailyLimit(isLimitError) |
212 | | - if (isLimitError) { |
213 | | - setModalOpen(false) |
214 | | - } |
215 | | - }) |
216 | | - }, [amount, ethAddress, bridgeToEthereum, feesData.amountAfterFee, getUserBackendBridgeTxs, setExceededDailyLimit]) |
217 | | - |
218 | | - const handleBridgeToTari = useCallback(async () => { |
219 | | - if (!amount || !ethAddress || !tariAccount?.address) { |
220 | | - return |
221 | | - } |
222 | | - |
223 | | - setIsUnwrapping(true) |
224 | | - const success = await bridgeToTari(amount, ethAddress, tariAccount.address) |
225 | | - if (success) { |
226 | | - setIsUnwrapping(false) |
227 | | - } else { |
228 | | - setIsUnwrapping(false) |
229 | | - setIsUnwrappingFailed(true) |
230 | | - } |
231 | | - }, [amount, ethAddress, tariAccount?.address, bridgeToTari]) |
232 | | - |
233 | 127 | const handleCloseModal = () => { |
234 | 128 | resetField('amount', { defaultValue: '' }) |
235 | | - setIsUnwrappingFailed(false) |
| 129 | + setUnwrapFailed(false) |
236 | 130 | handleSetOngoingModalOpen(false) |
237 | 131 | setModalStep(1) |
238 | 132 | } |
| 133 | + |
239 | 134 | return ( |
240 | 135 | <main className="relative min-h-screen w-full flex flex-col pl-(--tu-padding-left) pr-8 items-center justify-center"> |
241 | | - <Header onConnectClickAction={handleConnectClick} /> |
242 | | - |
243 | | - <MainComponent |
244 | | - onConnectClick={handleConnectClick} |
245 | | - onContinueClick={handleContinueClick} |
246 | | - control={control} |
247 | | - errors={errors} |
248 | | - setValue={setValue} |
249 | | - isValid={isValid} |
250 | | - fromNetwork={fromNetwork} |
251 | | - setFromNetwork={setFromNetwork} |
252 | | - toNetwork={toNetwork} |
253 | | - setToNetwork={setToNetwork} |
254 | | - remainingDailyLimit={remainingDailyLimit} |
255 | | - /> |
256 | | - |
257 | | - {detailedTx && <TransactionDetailsModal transaction={detailedTx} closeModal={() => setDetailedTx(null)} />} |
258 | | - |
259 | | - {modalOpen && !showModalDetailedTx && ( |
260 | | - <MainModal |
261 | | - success={isWrapSuccess} |
262 | | - failed={isFailed} |
263 | | - step={modalStep} |
264 | | - handleBridgeToEthereum={handleBridgeToEthereum} |
265 | | - handleBridgeToTari={handleBridgeToTari} |
266 | | - amount={amount} |
267 | | - ethereumAddress={ethAddress} |
268 | | - tariWalletAddress={tariAccount?.address} |
269 | | - fromNetwork={fromNetwork} |
270 | | - toNetwork={toNetwork} |
271 | | - feesData={feesData} |
272 | | - closeModal={handleCloseModal} |
273 | | - type={ongoingBridgeTx?.type || 'wrap'} |
274 | | - /> |
275 | | - )} |
| 136 | + <Header /> |
| 137 | + <FormProvider {...methods}> |
| 138 | + <MainComponent remainingDailyLimit={remainingDailyLimit} /> |
| 139 | + {isModalOpen && !showModalDetailedTx && ( |
| 140 | + <MainModal |
| 141 | + success={isWrapSuccess} |
| 142 | + failed={isFailed} |
| 143 | + step={modalStep} |
| 144 | + amount={amount} |
| 145 | + ethereumAddress={ethAddress} |
| 146 | + tariWalletAddress={tariAccount?.address} |
| 147 | + feesData={feesData} |
| 148 | + closeModalAction={handleCloseModal} |
| 149 | + type={ongoingBridgeTx?.type || 'wrap'} |
| 150 | + /> |
| 151 | + )} |
| 152 | + </FormProvider> |
| 153 | + {detailedTx && <TransactionDetailsModal transaction={detailedTx} closeModalAction={() => setDetailedTx(null)} />} |
276 | 154 | <FooterText /> |
277 | 155 | </main> |
278 | 156 | ) |
|
0 commit comments