diff --git a/package-lock.json b/package-lock.json index d322bcb6..0242ab56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-query": "^3.39.3", "react-redux": "^8.1.3", "react-youtube": "^10.1.0", "redux": "^4.2.1", @@ -470,8 +471,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.4", - "license": "MIT", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3515,6 +3517,14 @@ "version": "1.1.4", "license": "MIT" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/bip174": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", @@ -3566,6 +3576,21 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "node_modules/brorand": { "version": "1.1.0", "license": "MIT" @@ -3920,6 +3945,11 @@ "node": ">=6" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, "node_modules/detect-node-es": { "version": "1.1.0", "license": "MIT" @@ -5559,6 +5589,15 @@ "node": ">=12" } }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "license": "MIT", @@ -5588,6 +5627,11 @@ "node": ">=8.6" } }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "license": "ISC" @@ -5627,6 +5671,14 @@ "version": "2.1.2", "license": "MIT" }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "dependencies": { + "big-integer": "^1.6.16" + } + }, "node_modules/nanoid": { "version": "3.3.7", "funding": [ @@ -5752,6 +5804,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "node_modules/once": { "version": "1.4.0", "license": "ISC", @@ -6121,6 +6178,31 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/react-query": { + "version": "3.39.3", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", + "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-redux": { "version": "8.1.3", "license": "MIT", @@ -6345,6 +6427,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/require-directory": { "version": "2.1.1", "license": "MIT", @@ -6984,6 +7071,15 @@ "version": "5.26.5", "license": "MIT" }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "dev": true, diff --git a/package.json b/package.json index 605dc358..15c8b6cd 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-query": "^3.39.3", "react-redux": "^8.1.3", "react-youtube": "^10.1.0", "redux": "^4.2.1", diff --git a/src/app/app.tsx b/src/app/app.tsx index 79583d98..aebf098f 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -1,9 +1,11 @@ +import { QueryClient, QueryClientProvider } from 'react-query'; import { Route } from 'react-router-dom'; import { AppLayout } from '@components/app.layout'; import { MyVaults } from '@pages/my-vaults/my-vaults'; import { ProofOfReservePage } from '@pages/proof-of-reserve/proof-of-reserve-page'; import { BalanceContextProvider } from '@providers/balance-context-provider'; +import { BlockchainHeightContextProvider } from '@providers/bitcoin-query-provider'; import { EthereumObserverProvider } from '@providers/ethereum-observer-provider'; import { About } from './pages/about/about'; @@ -11,20 +13,26 @@ import { Dashboard } from './pages/dashboard/dashboard'; import { EthereumContextProvider } from './providers/ethereum-context-provider'; import { VaultContextProvider } from './providers/vault-context-provider'; +const queryClient = new QueryClient(); + export function App(): React.JSX.Element { return ( - - - - } /> - } /> - } /> - } /> - - - + + + + + + } /> + } /> + } /> + } /> + + + + + ); diff --git a/src/app/components/mint-unmint/mint-unmint.tsx b/src/app/components/mint-unmint/mint-unmint.tsx index dfe95e35..682e4441 100644 --- a/src/app/components/mint-unmint/mint-unmint.tsx +++ b/src/app/components/mint-unmint/mint-unmint.tsx @@ -25,6 +25,7 @@ export function MintUnmint({ address }: MintUnmintContainerProps): React.JSX.Ele setTimeout(() => { setAnimate(false); }, 1000); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [mintStep, unmintStep]); function handleTabsChange(index: number) { diff --git a/src/app/components/proof-of-reserve/proof-of-reserve.tsx b/src/app/components/proof-of-reserve/proof-of-reserve.tsx index ae6151b9..f481306c 100644 --- a/src/app/components/proof-of-reserve/proof-of-reserve.tsx +++ b/src/app/components/proof-of-reserve/proof-of-reserve.tsx @@ -14,6 +14,7 @@ export function ProofOfReserve(): React.JSX.Element { setContent(data); }) .catch(error => { + // eslint-disable-next-line no-console console.error('Error fetching data:', error); }); }, []); diff --git a/src/app/hooks/use-blockchain-height-query.ts b/src/app/hooks/use-blockchain-height-query.ts new file mode 100644 index 00000000..a122a936 --- /dev/null +++ b/src/app/hooks/use-blockchain-height-query.ts @@ -0,0 +1,18 @@ +import { useQuery } from 'react-query'; + +export function useBlockchainHeightQuery(): number | undefined { + const bitcoinBlockchainAPIURL = import.meta.env.VITE_BITCOIN_BLOCKCHAIN_API_URL; + const bitcoinExplorerHeightURL = `${bitcoinBlockchainAPIURL}/blocks/tip/height`; + + async function getBlockchainHeight() { + const response = await fetch(bitcoinExplorerHeightURL); + if (!response.ok) throw new Error('Network response was not ok'); + return response.json(); + } + + const { data: blockHeight } = useQuery('blockHeight', () => getBlockchainHeight(), { + refetchInterval: 10000, + }); + + return blockHeight; +} diff --git a/src/app/hooks/use-confirmation-checker.ts b/src/app/hooks/use-confirmation-checker.ts index cbdb513d..f3e37c35 100644 --- a/src/app/hooks/use-confirmation-checker.ts +++ b/src/app/hooks/use-confirmation-checker.ts @@ -1,63 +1,69 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; import { Vault, VaultState } from '@models/vault'; +import { BlockchainHeightContext } from '@providers/bitcoin-query-provider'; export function useConfirmationChecker(vault?: Vault): number { - const txID = vault?.state === VaultState.FUNDING ? vault?.fundingTX : vault?.closingTX; + const { blockHeight } = useContext(BlockchainHeightContext); + + let txID; + switch (vault?.state) { + case VaultState.FUNDING: + txID = vault?.fundingTX; + break; + case VaultState.CLOSED: + txID = vault?.closingTX; + break; + default: + txID = undefined; + } + + const [blockHeightAtBroadcast, setBlockHeightAtBroadcast] = useState( + undefined + ); + const [transactionProgress, setTransactionProgress] = useState(0); const bitcoinBlockchainAPIURL = import.meta.env.VITE_BITCOIN_BLOCKCHAIN_API_URL; - const bitcoinExplorerTXURL = `${bitcoinBlockchainAPIURL}/tx/${txID}`; - const bitcoinExplorerHeightURL = `${bitcoinBlockchainAPIURL}/blocks/tip/height`; - const fetchInterval = useRef(undefined); - - const [transactionProgress, setTransactionProgress] = useState(0); - const memoizedTransactionProgress = useMemo(() => transactionProgress, [transactionProgress]); - - const fetchTransactionDetails = async () => { - if (!txID || (vault?.state && ![VaultState.FUNDING, VaultState.CLOSED].includes(vault.state))) { - clearInterval(fetchInterval.current); - return; + async function fetchTransactionDetails() { + const response = await fetch(bitcoinExplorerTXURL); + if (!response.ok) throw new Error('Network response was not ok'); + const transactionDetails = await response.json(); + return transactionDetails.status.block_height; + } + + const { data: txBlockHeightAtBroadcast } = useQuery( + ['transactionDetails', txID], + () => fetchTransactionDetails(), + { + enabled: + !!txID && + (vault?.state === VaultState.FUNDING || vault?.state === VaultState.CLOSED) && + !blockHeightAtBroadcast, + refetchInterval: 10000, } + ); - let bitcoinCurrentBlockHeight; - try { - const response = await fetch(bitcoinExplorerHeightURL, { - headers: { Accept: 'application/json' }, - }); - bitcoinCurrentBlockHeight = await response.json(); - } catch (error) { - console.error(error); - } - - let bitcoinTransactionBlockHeight; - - try { - const response = await fetch(bitcoinExplorerTXURL, { - headers: { Accept: 'application/json' }, - }); - const bitcoinTransactionDetails = await response.json(); - bitcoinTransactionBlockHeight = bitcoinTransactionDetails.status.block_height; - } catch (error) { - console.error(error); + useEffect(() => { + if (txBlockHeightAtBroadcast && typeof txBlockHeightAtBroadcast === 'number') { + setBlockHeightAtBroadcast(txBlockHeightAtBroadcast); } + }, [txBlockHeightAtBroadcast]); - const difference = bitcoinCurrentBlockHeight - bitcoinTransactionBlockHeight; - - setTransactionProgress(difference > 0 ? difference : 0); + useEffect(() => { + if ( + (vault?.state != VaultState.FUNDING && vault?.state != VaultState.CLOSED) || + transactionProgress > 6 + ) + return; - if (difference > 6) { - clearInterval(fetchInterval.current); + const blockHeightDifference = (blockHeight as number) - (blockHeightAtBroadcast as number); + if (typeof blockHeightDifference === 'number') { + setTransactionProgress(blockHeightDifference); } - }; - - fetchTransactionDetails(); - - useEffect(() => { - fetchInterval.current = setInterval(fetchTransactionDetails, 10000) as unknown as number; // Cleanup the interval when the component unmounts - return () => clearInterval(fetchInterval.current); - }, [vault?.state, txID]); + }, [blockHeightAtBroadcast, blockHeight, vault?.state, transactionProgress]); - return memoizedTransactionProgress; + return transactionProgress; } diff --git a/src/app/hooks/use-ethereum-contracts.ts b/src/app/hooks/use-ethereum-contracts.ts index 41ad3ea5..69336d44 100644 --- a/src/app/hooks/use-ethereum-contracts.ts +++ b/src/app/hooks/use-ethereum-contracts.ts @@ -76,6 +76,7 @@ export function useEthereumContracts(): UseEthereumContractsReturnType { const contractVersion = import.meta.env.VITE_ETHEREUM_DEPLOYMENT_VERSION; const deploymentPlanURL = `https://raw.githubusercontent.com/DLC-link/dlc-solidity/${branchName}/deploymentFiles/${ethereumNetwork.name.toLowerCase()}/v${contractVersion}/${contractName}.json`; + // eslint-disable-next-line no-console console.log( `Fetching deployment info for ${contractName} on ${ethereumNetwork.name} from dlc-solidity/${branchName}` ); diff --git a/src/app/hooks/use-ethereum-observer.ts b/src/app/hooks/use-ethereum-observer.ts index 8899abf3..61a9afc5 100644 --- a/src/app/hooks/use-ethereum-observer.ts +++ b/src/app/hooks/use-ethereum-observer.ts @@ -92,5 +92,6 @@ export function useEthereumObserver(): void { ); }); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [protocolContract, dlcBTCContract, network]); } diff --git a/src/app/hooks/use-ethereum.ts b/src/app/hooks/use-ethereum.ts index df3efb6d..42d83a82 100644 --- a/src/app/hooks/use-ethereum.ts +++ b/src/app/hooks/use-ethereum.ts @@ -24,6 +24,7 @@ interface UseEthereumReturnType { closeVault: (vaultUUID: string) => Promise; } +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function throwEthereumError(message: string, error: any): void { if (error.code === Logger.errors.CALL_EXCEPTION) { throw new EthereumError( diff --git a/src/app/hooks/use-vaults.ts b/src/app/hooks/use-vaults.ts index c98fac06..0344cad9 100644 --- a/src/app/hooks/use-vaults.ts +++ b/src/app/hooks/use-vaults.ts @@ -35,7 +35,9 @@ export function useVaults(): UseVaultsReturnType { }; useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises fetchVaultsIfReady(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [contractsLoaded]); const allVaults = useMemo( diff --git a/src/app/providers/bitcoin-query-provider.tsx b/src/app/providers/bitcoin-query-provider.tsx new file mode 100644 index 00000000..97a56fa4 --- /dev/null +++ b/src/app/providers/bitcoin-query-provider.tsx @@ -0,0 +1,22 @@ +import { createContext } from 'react'; + +import { useBlockchainHeightQuery } from '@hooks/use-blockchain-height-query'; +import { HasChildren } from '@models/has-children'; + +interface BlockchainHeightContextProviderType { + blockHeight: number | undefined; +} + +export const BlockchainHeightContext = createContext({ + blockHeight: undefined, +}); + +export function BlockchainHeightContextProvider({ children }: HasChildren): React.JSX.Element { + const blockHeight = useBlockchainHeightQuery(); + + return ( + + {children} + + ); +} diff --git a/src/app/providers/ethereum-observer-provider.tsx b/src/app/providers/ethereum-observer-provider.tsx index 33ea511a..3308b552 100644 --- a/src/app/providers/ethereum-observer-provider.tsx +++ b/src/app/providers/ethereum-observer-provider.tsx @@ -4,6 +4,5 @@ import { HasChildren } from '@models/has-children'; export function EthereumObserverProvider({ children }: HasChildren): React.JSX.Element { useEthereumObserver(); - return {children}; }