From bd46c1c0b8725fc0ea968603b0188d72cf6d1633 Mon Sep 17 00:00:00 2001 From: zzq0826 <770166635@qq.com> Date: Thu, 25 Jan 2024 10:46:16 +0800 Subject: [PATCH] Merge sepolia into mainnet (#933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Auto squash #914  [bot] * upgrade: node 16.x -> node 20.x * upgrade: browserslist from >0.2% to >0.5% * doc: update readme.MD * Fix warning (#907) * fix warning * Fix warning * Fix warning * Change tag name * Update sepolia banner * Bridge history refactor (#896) * Transaction history is changed to be obtained from the backend Update * Only show unclaimed txs in claim page * Only show unclaimed txs in claim page * Rename var * fix typo * Update transation storagekey * Update bridge history api fields (#917) * Update bridge history api fields * fix apis error * Auto squash #921  [bot] * fix: support long list * fix: measure only when getting the final height * feat: change bridge-history backend api to v2 (#922) * fix typos (#925) * fix: custom navbar bg color * Auto squash #930  [bot] * Bridge costs optimization (#916) * Contract upgrade: Bridge cost optimization * Update netlify.toml * Auto squash #932  [bot] * FailedRelayed tx support retry feature (#924) * Revert "Bridge costs optimization (#916)" This reverts commit f61221aaa020372bffaab29ce82d5f39d36a29d7. --------- Co-authored-by: github-actions Co-authored-by: holybasil Co-authored-by: colin <102356659+colinlyguo@users.noreply.github.com> --- .gitignore | 5 + README.md | 2 +- netlify.toml | 8 +- package.json | 13 +- .../homepage/blog/scrollOriginsNFT/cubic.svg | 6 +- .../blog/scrollOriginsNFT/quartic.svg | 6 +- .../blog/scrollOriginsNFT/quintic.svg | 6 +- .../scrollOriginsNFT/rainbow-background.svg | 6 +- .../blog/scrollOriginsNFT/rainbow-stroke.svg | 6 +- public/imgs/nft/placeholder.svg | 6 +- src/apis/bridge.ts | 4 +- src/assets/abis/L1MessageQueue.json | 585 ++++++++++++++++++ src/assets/abis/L2GasPriceOracle.json | 198 +++--- src/components/Header/announcement.tsx | 14 +- src/components/Header/constants.ts | 5 +- src/components/Header/desktop_header.tsx | 26 +- src/components/Header/mobile_header.tsx | 30 +- .../Header/useCheckCustomNavBarBg.tsx | 29 + src/components/Header/useCheckNoBg.tsx | 28 - src/components/LinesEllipsis/index.tsx | 165 +++++ src/constants/brandKit.ts | 2 + src/constants/storageKey.ts | 4 + src/constants/transaction.ts | 24 +- src/contexts/BridgeContextProvider.tsx | 12 +- src/contexts/PriceFeeProvider.tsx | 4 +- src/hooks/useAddToken.ts | 4 +- src/hooks/useApprove.ts | 4 +- src/hooks/useBalance.tsx | 4 +- src/hooks/useCheckClaimStatus.ts | 47 -- src/hooks/useClaim.ts | 126 ++-- src/hooks/useClaimHistory.ts | 80 +++ src/hooks/useEstimateSendTransaction.ts | 4 +- src/hooks/useGasFee.ts | 4 +- src/hooks/useLastFinalizedBatchIndex.tsx | 4 +- src/hooks/useRetry.ts | 71 +++ src/hooks/useSendTransaction.ts | 43 +- src/hooks/useSufficientBalance.ts | 4 +- src/hooks/useTokenInfo.ts | 4 +- src/hooks/useTxHistory.ts | 33 +- src/pages/brand-kit/Assets/AssetCard.tsx | 8 +- src/pages/brand-kit/Assets/index.tsx | 6 +- src/pages/brand-kit/index.tsx | 4 +- src/pages/bridge/Send/Claim.tsx | 29 +- .../Send/SendTransaction/ApprovalDialog.tsx | 5 +- .../SendTransaction/InfoTooltip/DetailRow.tsx | 1 - .../SendTransaction/TransactionSummary.tsx | 36 +- .../bridge/Send/SendTransaction/index.tsx | 8 +- .../bridge/TxHistoryDialog/TxHistoryTable.tsx | 4 +- src/pages/bridge/TxHistoryDialog/index.tsx | 15 + .../components/ClaimTable/ClaimButton.tsx | 256 -------- .../bridge/components/ClaimTable/index.tsx | 250 -------- .../components/TxTable/ActiveButton.tsx | 118 ++++ .../components/TxTable/TxStatusButton.tsx | 85 +-- src/pages/bridge/components/TxTable/index.tsx | 99 ++- src/pages/career/Perks/index.tsx | 2 +- src/pages/ecosystem/Protocols/Category.tsx | 2 +- .../Protocols/ProtocolList/ProtocolCard.tsx | 33 +- .../Protocols/ProtocolList/index.tsx | 68 +- src/pages/ourStory/Initail/InlineAvatar.tsx | 4 +- src/pages/ourStory/TechPrinciple/index.tsx | 2 +- src/pages/portal/WalletConfig.tsx | 10 +- src/pages/rollup/batch/index.tsx | 2 +- src/pages/rollup/chunk/detail.tsx | 2 +- src/stores/claimStore.ts | 198 ++---- src/stores/txStore.ts | 444 ++----------- src/stores/utils.ts | 134 ++++ src/theme/options.ts | 2 + src/types/index.d.ts | 4 + src/utils/common.ts | 7 + src/utils/format.ts | 8 +- tsconfig.json | 2 +- yarn.lock | 62 +- 72 files changed, 1881 insertions(+), 1655 deletions(-) create mode 100644 src/assets/abis/L1MessageQueue.json create mode 100644 src/components/Header/useCheckCustomNavBarBg.tsx delete mode 100644 src/components/Header/useCheckNoBg.tsx create mode 100644 src/components/LinesEllipsis/index.tsx delete mode 100644 src/hooks/useCheckClaimStatus.ts create mode 100644 src/hooks/useClaimHistory.ts create mode 100644 src/hooks/useRetry.ts delete mode 100644 src/pages/bridge/components/ClaimTable/ClaimButton.tsx delete mode 100644 src/pages/bridge/components/ClaimTable/index.tsx create mode 100644 src/pages/bridge/components/TxTable/ActiveButton.tsx create mode 100644 src/stores/utils.ts diff --git a/.gitignore b/.gitignore index b718753cc..f3740d083 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,8 @@ yarn-error.log* .vscode .env +.tool-versions + + +.next +next-env.d.ts \ No newline at end of file diff --git a/README.md b/README.md index f7daf9bdb..577e00f76 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ If you encounter bugs or have feature ideas, feel free to [create an issue](/../ ## Node Version -Tested with v16.20.1 (npm v8.19.4). +Tested with v20.10.0 (npm v10.2.3). ## Available Scripts diff --git a/netlify.toml b/netlify.toml index 49fd68d6f..025ed33d2 100644 --- a/netlify.toml +++ b/netlify.toml @@ -32,7 +32,7 @@ REACT_APP_NFT_API_URI="https://nft.scroll.io" [context.deploy-preview.environment] REACT_APP_SCROLL_ENVIRONMENT = "Staging" REACT_APP_API_BASE_URI = "https://sepolia-api.scroll.io" -REACT_APP_BRIDGE_API_URI = "https://sepolia-api-bridge.scroll.io/api" +REACT_APP_BRIDGE_API_URI = "https://sepolia-api-bridge-v2.scroll.io/api" REACT_APP_ROLLUPSCAN_API_URI = "https://sepolia-api-re.scroll.io/api" REACT_APP_CHAIN_ID_L1 = "11155111" REACT_APP_CHAIN_ID_L2 = "534351" @@ -72,7 +72,7 @@ REACT_APP_SCROLL_ORIGINS_NFT_V2="0xDd7d857F570B0C211abfe05cd914A85BefEC2464" [context.staging.environment] REACT_APP_SCROLL_ENVIRONMENT = "Staging" REACT_APP_API_BASE_URI = "https://sepolia-api.scroll.io" -REACT_APP_BRIDGE_API_URI = "https://sepolia-api-bridge.scroll.io/api" +REACT_APP_BRIDGE_API_URI = "https://sepolia-api-bridge-v2.scroll.io/api" REACT_APP_ROLLUPSCAN_API_URI = "https://sepolia-api-re.scroll.io/api" REACT_APP_CHAIN_ID_L1 = "11155111" REACT_APP_CHAIN_ID_L2 = "534351" @@ -112,7 +112,7 @@ REACT_APP_SCROLL_ORIGINS_NFT_V2="0xDd7d857F570B0C211abfe05cd914A85BefEC2464" [context.sepolia.environment] REACT_APP_SCROLL_ENVIRONMENT = "Sepolia" REACT_APP_API_BASE_URI = "https://sepolia-api.scroll.io" -REACT_APP_BRIDGE_API_URI = "https://sepolia-api-bridge.scroll.io/api" +REACT_APP_BRIDGE_API_URI = "https://sepolia-api-bridge-v2.scroll.io/api" REACT_APP_ROLLUPSCAN_API_URI = "https://sepolia-api-re.scroll.io/api" REACT_APP_CHAIN_ID_L1 = "11155111" REACT_APP_CHAIN_ID_L2 = "534351" @@ -152,7 +152,7 @@ REACT_APP_SCROLL_ORIGINS_NFT_V2="0xDd7d857F570B0C211abfe05cd914A85BefEC2464" [context.mainnet.environment] REACT_APP_SCROLL_ENVIRONMENT = "Mainnet" REACT_APP_API_BASE_URI = "https://mainnet-api.scroll.io" -REACT_APP_BRIDGE_API_URI = "https://mainnet-api-bridge.scroll.io/api" +REACT_APP_BRIDGE_API_URI = "https://mainnet-api-bridge-v2.scroll.io/api" REACT_APP_ROLLUPSCAN_API_URI = "https://mainnet-api-re.scroll.io/api" REACT_APP_CHAIN_ID_L1 = "1" REACT_APP_CHAIN_ID_L2 = "534352" diff --git a/package.json b/package.json index a498573d6..787a4f7f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scroll.io", - "version": "5.0.63", + "version": "5.0.67", "private": false, "license": "MIT", "scripts": { @@ -21,7 +21,7 @@ }, "browserslist": { "production": [ - ">0.2%", + ">0.5%", "not dead", "not op_mini all" ], @@ -45,7 +45,7 @@ "@sentry/react": "^7.43.0", "@sentry/tracing": "^7.43.0", "@types/jest": "^27.5.2", - "@types/node": "^16.11.43", + "@types/node": "^20.10.6", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@web3-onboard/injected-wallets": "^2.2.4", @@ -63,12 +63,12 @@ "react-dom": "^18.2.0", "react-ga4": "^1.4.1", "react-helmet-async": "^1.3.0", - "react-lines-ellipsis": "^0.15.4", "react-mailchimp-subscribe": "^2.1.3", "react-markdown": "^8.0.3", "react-query": "^3.39.2", "react-scripts": "5.0.1", "react-use": "^17.4.0", + "react-virtualized": "^9.22.5", "rehype-katex": "^6.0.2", "rehype-raw": "^6.1.1", "remark-gfm": "^3.0.1", @@ -112,7 +112,7 @@ "postcss": "^8.4.17", "prettier": "^2.8.1", "process": "^0.11.10", - "react-device-detect": "^1.6.2", + "react-device-detect": "^2.2.3", "react-redux": "^7.2.0", "react-router-dom": "^6.3.0", "stream": "^0.0.2", @@ -127,5 +127,8 @@ "@typescript-eslint/eslint-plugin": "^6.5.0", "@typescript-eslint/parser": "^6.5.0" } + }, + "engines": { + "node": ">=20.10.0" } } diff --git a/public/imgs/homepage/blog/scrollOriginsNFT/cubic.svg b/public/imgs/homepage/blog/scrollOriginsNFT/cubic.svg index 97a07767b..c381c0548 100644 --- a/public/imgs/homepage/blog/scrollOriginsNFT/cubic.svg +++ b/public/imgs/homepage/blog/scrollOriginsNFT/cubic.svg @@ -9,13 +9,13 @@ .st45{fill:#EE5132;stroke:#050308;stroke-width:3;stroke-miterlimit:10;} .row{margin:0;} .row span{display:inline-block;height:14px;line-height:14px;background:#050308;padding:0 3px;border:1px solid #EE5132;} - .row span:nth-child(even){ + .row span:nth-of-type(even){ border-left:none; } - .row:nth-child(n+2) span{ + .row:nth-of-type(n+2) span{ margin-top: -0.9px } - .row:nth-child(n+2) span:nth-child(1){ + .row:nth-of-type(n+2) span:nth-of-type(1){ width: 55.8px } .st61{ diff --git a/public/imgs/homepage/blog/scrollOriginsNFT/quartic.svg b/public/imgs/homepage/blog/scrollOriginsNFT/quartic.svg index ca7efdfa6..a5f35d75c 100644 --- a/public/imgs/homepage/blog/scrollOriginsNFT/quartic.svg +++ b/public/imgs/homepage/blog/scrollOriginsNFT/quartic.svg @@ -9,13 +9,13 @@ .st45{fill:#62E6D4;stroke:#050308;stroke-width:3;stroke-miterlimit:10;} .row{margin:0;} .row span{display:inline-block;height:14px;line-height:14px;background:#050308;padding:0 3px;border:1px solid #62E6D4;} - .row span:nth-child(even){ + .row span:nth-of-type(even){ border-left:none; } - .row:nth-child(n+2) span{ + .row:nth-of-type(n+2) span{ margin-top: -0.9px } - .row:nth-child(n+2) span:nth-child(1){ + .row:nth-of-type(n+2) span:nth-of-type(1){ width: 55.8px } .st61{ diff --git a/public/imgs/homepage/blog/scrollOriginsNFT/quintic.svg b/public/imgs/homepage/blog/scrollOriginsNFT/quintic.svg index 95a4ec7ba..4fbbbab8f 100644 --- a/public/imgs/homepage/blog/scrollOriginsNFT/quintic.svg +++ b/public/imgs/homepage/blog/scrollOriginsNFT/quintic.svg @@ -9,13 +9,13 @@ .st45{fill:#EBC28E;stroke:#050308;stroke-width:3;stroke-miterlimit:10;} .row{margin:0;} .row span{display:inline-block;height:14px;line-height:14px;background:#050308;padding:0 3px;border:1px solid #EBC28E;} - .row span:nth-child(even){ + .row span:nth-of-type(even){ border-left:none; } - .row:nth-child(n+2) span{ + .row:nth-of-type(n+2) span{ margin-top: -0.9px } - .row:nth-child(n+2) span:nth-child(1){ + .row:nth-of-type(n+2) span:nth-of-type(1){ width: 55.8px } .st61{ diff --git a/public/imgs/homepage/blog/scrollOriginsNFT/rainbow-background.svg b/public/imgs/homepage/blog/scrollOriginsNFT/rainbow-background.svg index 5239ddd21..66559b807 100644 --- a/public/imgs/homepage/blog/scrollOriginsNFT/rainbow-background.svg +++ b/public/imgs/homepage/blog/scrollOriginsNFT/rainbow-background.svg @@ -9,13 +9,13 @@ .st45{stroke:#050308;stroke-width:3;stroke-miterlimit:10;} .row{margin:0;} .row span{display:inline-block;height:14px;line-height:14px;background:#050308;padding:0 3px;border:1px solid #EBC28E;} - .row span:nth-child(even){ + .row span:nth-of-type(even){ border-left:none; } - .row:nth-child(n+2) span{ + .row:nth-of-type(n+2) span{ margin-top: -0.9px } - .row:nth-child(n+2) span:nth-child(1){ + .row:nth-of-type(n+2) span:nth-of-type(1){ width: 55.8px } .st61{ diff --git a/public/imgs/homepage/blog/scrollOriginsNFT/rainbow-stroke.svg b/public/imgs/homepage/blog/scrollOriginsNFT/rainbow-stroke.svg index 6be1a5662..ff4174593 100644 --- a/public/imgs/homepage/blog/scrollOriginsNFT/rainbow-stroke.svg +++ b/public/imgs/homepage/blog/scrollOriginsNFT/rainbow-stroke.svg @@ -9,13 +9,13 @@ .st45{fill:#62E6D4;stroke:#050308;stroke-width:3;stroke-miterlimit:10;} .row{margin:0;} .row span{display:inline-block;height:14px;line-height:14px;background:#050308;padding:0 3px;border:1px solid #62E6D4;} - .row span:nth-child(even){ + .row span:nth-of-type(even){ border-left:none; } - .row:nth-child(n+2) span{ + .row:nth-of-type(n+2) span{ margin-top: -0.9px } - .row:nth-child(n+2) span:nth-child(1){ + .row:nth-of-type(n+2) span:nth-of-type(1){ width: 55.8px } .st61{ diff --git a/public/imgs/nft/placeholder.svg b/public/imgs/nft/placeholder.svg index cbea8838b..eaa3fd1b0 100644 --- a/public/imgs/nft/placeholder.svg +++ b/public/imgs/nft/placeholder.svg @@ -10,13 +10,13 @@ .st45{fill:#EBC28E;stroke:#050308;stroke-width:3;stroke-miterlimit:10;} .row{margin:0;} .row span{display:inline-block;height:14px;line-height:14px;background:#050308;padding:0 3px;border:1px solid #EBC28E;} - .row span:nth-child(even){ + .row span:nth-of-type(even){ border-left:none; } - .row:nth-child(n+2) span{ + .row:nth-of-type(n+2) span{ margin-top: -0.9px } - .row:nth-child(n+2) span:nth-child(1){ + .row:nth-of-type(n+2) span:nth-of-type(1){ width: 55.8px } .st61{ diff --git a/src/apis/bridge.ts b/src/apis/bridge.ts index 652db67bc..cdd353989 100644 --- a/src/apis/bridge.ts +++ b/src/apis/bridge.ts @@ -4,6 +4,8 @@ const baseUrl = requireEnv("REACT_APP_BRIDGE_API_URI") export const fetchTxByHashUrl = `${baseUrl}/txsbyhashes` +export const fetchWithdrawalListUrl = `${baseUrl}/l2/withdrawals` + export const fetchTxListUrl = `${baseUrl}/txs` -export const fetchClaimableTxListUrl = `${baseUrl}/claimable` +export const fetchClaimableTxListUrl = `${baseUrl}/l2/unclaimed/withdrawals` diff --git a/src/assets/abis/L1MessageQueue.json b/src/assets/abis/L1MessageQueue.json new file mode 100644 index 000000000..127fc2b59 --- /dev/null +++ b/src/assets/abis/L1MessageQueue.json @@ -0,0 +1,585 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "startIndex", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "count", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "skippedBitmap", + "type": "uint256" + } + ], + "name": "DequeueTransaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "DropTransaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "queueIndex", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "QueueTransaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_oldGateway", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_newGateway", + "type": "address" + } + ], + "name": "UpdateEnforcedTxGateway", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_oldGasOracle", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "_newGasOracle", + "type": "address" + } + ], + "name": "UpdateGasOracle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "_oldMaxGasLimit", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_newMaxGasLimit", + "type": "uint256" + } + ], + "name": "UpdateMaxGasLimit", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "appendCrossDomainMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "appendEnforcedTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_calldata", + "type": "bytes" + } + ], + "name": "calculateIntrinsicGasFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_queueIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "computeTransactionHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_index", + "type": "uint256" + } + ], + "name": "dropCrossDomainMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "enforcedTxGateway", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_gasLimit", + "type": "uint256" + } + ], + "name": "estimateCrossDomainMessageFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasOracle", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_queueIndex", + "type": "uint256" + } + ], + "name": "getCrossDomainMessage", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_messenger", + "type": "address" + }, + { + "internalType": "address", + "name": "_scrollChain", + "type": "address" + }, + { + "internalType": "address", + "name": "_enforcedTxGateway", + "type": "address" + }, + { + "internalType": "address", + "name": "_gasOracle", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_maxGasLimit", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "maxGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "messageQueue", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "messenger", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextCrossDomainMessageIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingQueueIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_startIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_skippedBitmap", + "type": "uint256" + } + ], + "name": "popCrossDomainMessage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "scrollChain", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newGateway", + "type": "address" + } + ], + "name": "updateEnforcedTxGateway", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newGasOracle", + "type": "address" + } + ], + "name": "updateGasOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_newMaxGasLimit", + "type": "uint256" + } + ], + "name": "updateMaxGasLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/assets/abis/L2GasPriceOracle.json b/src/assets/abis/L2GasPriceOracle.json index e9b334a4f..cd9696e1d 100644 --- a/src/assets/abis/L2GasPriceOracle.json +++ b/src/assets/abis/L2GasPriceOracle.json @@ -1,15 +1,51 @@ [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, { "anonymous": false, "inputs": [ { "indexed": false, "internalType": "uint256", - "name": "l2BaseFee", + "name": "txGas", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "txGasContractCreation", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "zeroGas", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "nonZeroGas", "type": "uint256" } ], - "name": "L2BaseFeeUpdated", + "name": "IntrinsicParamsUpdated", "type": "event" }, { @@ -18,11 +54,17 @@ { "indexed": false, "internalType": "uint256", - "name": "overhead", + "name": "oldL2BaseFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newL2BaseFee", "type": "uint256" } ], - "name": "OverheadUpdated", + "name": "L2BaseFeeUpdated", "type": "event" }, { @@ -44,19 +86,6 @@ "name": "OwnershipTransferred", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "scalar", - "type": "uint256" - } - ], - "name": "ScalarUpdated", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -78,28 +107,13 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - }, - { - "internalType": "address", - "name": "", - "type": "address" - }, { "internalType": "bytes", "name": "_message", "type": "bytes" - }, - { - "internalType": "uint256", - "name": "_gasLimit", - "type": "uint256" } ], - "name": "estimateCrossDomainMessageFee", + "name": "calculateIntrinsicGasFee", "outputs": [ { "internalType": "uint256", @@ -113,12 +127,12 @@ { "inputs": [ { - "internalType": "bytes", - "name": "_data", - "type": "bytes" + "internalType": "uint256", + "name": "_gasLimit", + "type": "uint256" } ], - "name": "getL1GasUsed", + "name": "estimateCrossDomainMessageFee", "outputs": [ { "internalType": "uint256", @@ -130,7 +144,28 @@ "type": "function" }, { - "inputs": [], + "inputs": [ + { + "internalType": "uint64", + "name": "_txGas", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "_txGasContractCreation", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "_zeroGas", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "_nonZeroGas", + "type": "uint64" + } + ], "name": "initialize", "outputs": [], "stateMutability": "nonpayable", @@ -138,25 +173,27 @@ }, { "inputs": [], - "name": "l1BaseFee", + "name": "intrinsicParams", "outputs": [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "l2BaseFee", - "outputs": [ + "internalType": "uint64", + "name": "txGas", + "type": "uint64" + }, { - "internalType": "uint256", - "name": "", - "type": "uint256" + "internalType": "uint64", + "name": "txGasContractCreation", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "zeroGas", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "nonZeroGas", + "type": "uint64" } ], "stateMutability": "view", @@ -164,7 +201,7 @@ }, { "inputs": [], - "name": "overhead", + "name": "l2BaseFee", "outputs": [ { "internalType": "uint256", @@ -195,41 +232,30 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [], - "name": "scalar", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { - "internalType": "uint256", - "name": "_l2BaseFee", - "type": "uint256" - } - ], - "name": "setL2BaseFee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ + "internalType": "uint64", + "name": "_txGas", + "type": "uint64" + }, { - "internalType": "uint256", - "name": "_overhead", - "type": "uint256" + "internalType": "uint64", + "name": "_txGasContractCreation", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "_zeroGas", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "_nonZeroGas", + "type": "uint64" } ], - "name": "setOverhead", + "name": "setIntrinsicParams", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -238,11 +264,11 @@ "inputs": [ { "internalType": "uint256", - "name": "_scalar", + "name": "_newL2BaseFee", "type": "uint256" } ], - "name": "setScalar", + "name": "setL2BaseFee", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/src/components/Header/announcement.tsx b/src/components/Header/announcement.tsx index 1e3c326f3..98d4bf294 100644 --- a/src/components/Header/announcement.tsx +++ b/src/components/Header/announcement.tsx @@ -4,7 +4,6 @@ import { useMatch } from "react-router-dom" import { Stack } from "@mui/material" import { styled } from "@mui/system" -import Link from "@/components/Link" import { isProduction, requireEnv } from "@/utils" const AnnouncementStack = styled(Stack, { shouldForwardProp: prop => prop !== "production" })(({ theme, production }) => ({ @@ -21,12 +20,6 @@ const AnnouncementStack = styled(Stack, { shouldForwardProp: prop => prop ! }, })) -const ReadMoreLink = styled("a")( - ({ theme }) => ` - font-weight: 700; - `, -) - const Announcement = () => { const isHome = useMatch("/") const isPortal = useMatch("/portal") @@ -35,16 +28,13 @@ const Announcement = () => { if (isProduction && (isHome || isPortal)) { return ( <> - Scroll {requireEnv("REACT_APP_SCROLL_ENVIRONMENT")} is now live. Try it! + Scroll {requireEnv("REACT_APP_SCROLL_ENVIRONMENT")} is now live. Try it! ) } else if (!isProduction) { return ( <> - You are on the Scroll {requireEnv("REACT_APP_SCROLL_ENVIRONMENT")} Testnet website. Return to{" "} - - Mainnet - + You are on the Scroll {requireEnv("REACT_APP_SCROLL_ENVIRONMENT")} Testnet website. Return to Mainnet ) } diff --git a/src/components/Header/constants.ts b/src/components/Header/constants.ts index 7248aff44..247be83a4 100644 --- a/src/components/Header/constants.ts +++ b/src/components/Header/constants.ts @@ -36,7 +36,7 @@ const sepoliaNavigations = [ }, { label: "Bug Bounty", - key: "status", + key: "bug-bounty", href: "https://immunefi.com/bounty/scroll/", isExternal: true, }, @@ -126,7 +126,7 @@ const mainnetNavigations = [ }, { label: "Bug Bounty", - key: "status", + key: "bug-bounty", href: "https://immunefi.com/bounty/scroll/", isExternal: true, }, @@ -184,6 +184,7 @@ const mainnetNavigations = [ label: "Brand Kit", key: "brand kit", href: "/brand-kit", + rootKey: "resources", }, { label: "Forum", diff --git a/src/components/Header/desktop_header.tsx b/src/components/Header/desktop_header.tsx index e53dcb7ce..ff54958d6 100644 --- a/src/components/Header/desktop_header.tsx +++ b/src/components/Header/desktop_header.tsx @@ -13,21 +13,22 @@ import useShowWalletConnector from "@/hooks/useShowWalletToolkit" import Announcement from "./announcement" import { navigations } from "./constants" -import useCheckNoBg from "./useCheckNoBg" +import useCheckCustomNavBarBg from "./useCheckCustomNavBarBg" import useCheckTheme from "./useCheckTheme" -const StyledBox = styled(Stack, { shouldForwardProp: prop => prop !== "dark" })(({ theme, transparent, dark }) => ({ +const StyledBox = styled(Stack, { shouldForwardProp: prop => prop !== "dark" && prop !== "bgColor" })(({ theme, bgColor, dark }) => ({ position: "sticky", top: 0, width: "100%", zIndex: 10, - backgroundColor: transparent ? "transparent" : dark ? theme.palette.themeBackground.dark : theme.palette.themeBackground.light, + backgroundColor: bgColor ? theme.palette.themeBackground[bgColor] : dark ? theme.palette.themeBackground.dark : theme.palette.themeBackground.light, })) -const StyledPopper = styled(Popper, { shouldForwardProp: prop => prop !== "dark" })(({ theme, transparent, dark }) => ({ - backgroundColor: transparent ? "transparent" : dark ? theme.palette.themeBackground.dark : theme.palette.themeBackground.light, +const StyledPopper = styled(Popper, { shouldForwardProp: prop => prop !== "dark" && prop !== "bgColor" })(({ theme, bgColor, dark }) => ({ + backgroundColor: bgColor ? theme.palette.themeBackground[bgColor] : dark ? theme.palette.themeBackground.dark : theme.palette.themeBackground.light, padding: "0 2rem 1rem", marginLeft: "-2rem !important", + zIndex: theme.zIndex.appBar, })) const HeaderContainer = styled(Box)(({ theme }) => ({ @@ -157,9 +158,8 @@ const LinkStyledSubButton = styled(NavLink, { shouldForwardProp: prop => pr const App = ({ currentMenu }) => { const { cx } = useStyles() - const noBg = useCheckNoBg() + const navbarBg = useCheckCustomNavBarBg() const { isDesktop } = useCheckViewport() - const dark = useCheckTheme() const [checked, setChecked] = useState("") @@ -179,9 +179,11 @@ const App = ({ currentMenu }) => { } const renderSubMenuList = children => { - return children.map(section => ( - - {section.label} + return children.map((section, idx) => ( + + + {section.label} + {section.children // only show sub menu item when the href is set ?.filter(subItem => subItem.href) @@ -230,7 +232,7 @@ const App = ({ currentMenu }) => { inheritViewBox > {item.key === checked && ( - + {({ TransitionProps }) => ( {renderSubMenuList(item.children)} @@ -266,7 +268,7 @@ const App = ({ currentMenu }) => { } return ( - + diff --git a/src/components/Header/mobile_header.tsx b/src/components/Header/mobile_header.tsx index 4b1419484..f33d8a91b 100644 --- a/src/components/Header/mobile_header.tsx +++ b/src/components/Header/mobile_header.tsx @@ -11,7 +11,7 @@ import useShowWalletConnector from "@/hooks/useShowWalletToolkit" import Logo from "../ScrollLogo" import Announcement from "./announcement" import { navigations } from "./constants" -import useCheckNoBg from "./useCheckNoBg" +import useCheckCustomNavBarBg from "./useCheckCustomNavBarBg" import useCheckTheme from "./useCheckTheme" const NavStack = styled(Stack)(({ theme }) => ({ @@ -33,7 +33,7 @@ const Menu = styled("div")(({ theme }) => ({ }, })) -const Bar = styled("div")(({ theme, dark }) => ({ +const Bar = styled("div", { shouldForwardProp: prop => prop !== "dark" })(({ theme, dark }) => ({ width: "2rem", height: ".2rem", backgroundColor: dark ? theme.palette.primary.contrastText : theme.palette.text.primary, @@ -41,12 +41,12 @@ const Bar = styled("div")(({ theme, dark }) => ({ transition: "0.4s", })) -const MenuContent = styled(Box)(({ theme, dark }) => ({ +const MenuContent = styled(Box, { shouldForwardProp: prop => prop !== "dark" })(({ theme, dark }) => ({ margin: "0.5rem 1.6rem 0", background: dark ? theme.palette.themeBackground.dark : theme.palette.themeBackground.light, })) -const ListItem = styled(ListItemButton)(({ theme, dark }) => ({ +const ListItem = styled(ListItemButton, { shouldForwardProp: prop => prop !== "dark" })(({ theme, dark }) => ({ fontWeight: 600, fontSize: "2rem", height: "5.5rem", @@ -65,7 +65,7 @@ const ListItem = styled(ListItemButton)(({ theme, dark }) => ({ }, })) -const MenuLinkStyledButton = styled(NavLink)(({ theme, dark }) => ({ +const MenuLinkStyledButton = styled(NavLink, { shouldForwardProp: prop => prop !== "dark" })(({ theme, dark }) => ({ fontWeight: 600, fontSize: "2rem", height: "5.5rem", @@ -86,7 +86,7 @@ const SubListItem = styled(ListItemButton)(({ theme }) => ({ padding: "0 !important", })) -const LinkStyledButton = styled(NavLink)(({ theme, dark }) => ({ +const LinkStyledButton = styled(NavLink, { shouldForwardProp: prop => prop !== "dark" })(({ theme, dark }) => ({ fontWeight: 500, fontSize: "1.8rem", height: "4rem", @@ -99,7 +99,7 @@ const LinkStyledButton = styled(NavLink)(({ theme, dark }) => ({ }, })) -const ExternalLink = styled(Link)(({ theme, dark }) => ({ +const ExternalLink = styled(Link, { shouldForwardProp: prop => prop !== "dark" })(({ theme, dark }) => ({ fontWeight: 500, fontSize: "1.8rem", height: "4rem", @@ -110,11 +110,11 @@ const ExternalLink = styled(Link)(({ theme, dark }) => ({ width: "100%", })) -const SectionList = styled("div")(({ theme, dark }) => ({ +const SectionList = styled("div", { shouldForwardProp: prop => prop !== "dark" })(({ theme, dark }) => ({ "&:last-of-type": { paddingBottom: "2.5rem", }, - "&:nth-last-child(-n+2)": { + "&:nth-last-of-type(-n+2)": { paddingBottom: "1.6rem", }, "&:nth-of-type(n+2)": { @@ -131,7 +131,7 @@ const ExpandMoreIcon = styled(ExpandMore)(({ theme }) => ({ })) const App = ({ currentMenu }) => { - const noBg = useCheckNoBg() + const navbarBg = useCheckCustomNavBarBg() const showWalletConnector = useShowWalletConnector() const dark = useCheckTheme() @@ -183,17 +183,17 @@ const App = ({ currentMenu }) => { )} - + - {item.children?.map(section => ( - + {item.children?.map((section, idx) => ( + {section.label} {section.children // only show sub items with href ?.filter(subItem => subItem.href) .map(subItem => subItem.isExternal ? ( - toggleDrawer(false)} sx={{ mx: 4 }} key={subItem.key}> + toggleDrawer(false)} sx={{ mx: 4 }} key={subItem.key + idx}> {subItem.label} { return ( diff --git a/src/components/Header/useCheckCustomNavBarBg.tsx b/src/components/Header/useCheckCustomNavBarBg.tsx new file mode 100644 index 000000000..f21fb9b0f --- /dev/null +++ b/src/components/Header/useCheckCustomNavBarBg.tsx @@ -0,0 +1,29 @@ +import { useMemo } from "react" +import { useLocation } from "react-router-dom" + +import useScrollTrigger from "@mui/material/useScrollTrigger" + +const TRANSPARENT_BG_PAGE_LIST = ["/story"] +// themeBackground +const CUSTOM_BG_PAGE_MAP = { + "/brand-kit": "brand", + "/join-us": "normal", +} + +const useCheckCustomNavBarBg = () => { + const isScrolling = useScrollTrigger({ disableHysteresis: true, threshold: 10 }) + + const { pathname } = useLocation() + const isNoBgPage = useMemo(() => TRANSPARENT_BG_PAGE_LIST.includes(pathname), [pathname]) + + const navbarBg = useMemo(() => { + if (isNoBgPage) { + return isScrolling ? "" : "transparent" + } + return CUSTOM_BG_PAGE_MAP[pathname] || "" + }, [isNoBgPage, isScrolling, pathname]) + + return navbarBg +} + +export default useCheckCustomNavBarBg diff --git a/src/components/Header/useCheckNoBg.tsx b/src/components/Header/useCheckNoBg.tsx deleted file mode 100644 index ffaafd099..000000000 --- a/src/components/Header/useCheckNoBg.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect, useMemo, useState } from "react" -import { useLocation } from "react-router-dom" - -const useCheckNoBg = () => { - const { pathname } = useLocation() - const isNoBgPage = useMemo(() => ["/story", "/join-us", "/brand-kit"].includes(pathname), [pathname]) - const [isNoBgSection, setIsNoBgSection] = useState(isNoBgPage) - - useEffect(() => { - setIsNoBgSection(isNoBgPage) - if (isNoBgPage) { - const element = document.documentElement - const elementListener = window - const handleScroll = () => { - const scrollTop = element.scrollTop - setIsNoBgSection(scrollTop < 10) - } - elementListener.addEventListener("scroll", handleScroll) - return () => { - setIsNoBgSection(true) - elementListener.removeEventListener("scroll", handleScroll) - } - } - }, [isNoBgPage]) - return isNoBgPage && isNoBgSection ? "transparent" : "" -} - -export default useCheckNoBg diff --git a/src/components/LinesEllipsis/index.tsx b/src/components/LinesEllipsis/index.tsx new file mode 100644 index 000000000..bc6a5d712 --- /dev/null +++ b/src/components/LinesEllipsis/index.tsx @@ -0,0 +1,165 @@ +// @ts-nocheck +import React, { useEffect, useRef, useState } from "react" + +const agentStyle = { + position: "absolute", + bottom: 0, + left: 0, + height: 0, + overflow: "hidden", + paddingTop: 0, + paddingBottom: 0, + border: "none", +} + +const mirrorProps = [ + "box-sizing", + "width", + "font-size", + "font-weight", + "font-family", + "font-style", + "letter-spacing", + "text-indent", + "white-space", + "word-break", + "overflow-wrap", + "padding-left", + "padding-right", +] + +function prevSibling(node, count) { + while (node && count--) { + node = node.previousElementSibling + } + return node +} + +const LinesEllipsis = props => { + const { component: Component = "div", ellipsis, trimRight = true, basedOn, maxLine = 1, text, className, onReflow, ...rest } = props + + const [displayedText, setDisplayedText] = useState(text) + const [clamped, setClamped] = useState(false) + + const units = useRef([]) + const shadowRef = useRef() + const targetRef = useRef() + const ellipsisRef = useRef() + + useEffect(() => { + const handleSizeChanged = entries => { + if (targetRef.current) { + copyStyleToShadow() + reflow({ basedOn, text, maxLine }) + } + } + const resizeObserver = new ResizeObserver(handleSizeChanged) + resizeObserver.observe(targetRef.current) + + return () => { + if (targetRef.current) { + resizeObserver && resizeObserver.unobserve(targetRef.current) + } + } + }, [basedOn, text, maxLine]) + + const copyStyleToShadow = () => { + const targetStyle = window.getComputedStyle(targetRef.current) + mirrorProps.forEach(key => { + shadowRef.current.style[key] = targetStyle[key] + }) + } + + const reflow = props => { + /* eslint-disable no-control-regex */ + const basedOn = props.basedOn || (/^[\x00-\x7F]+$/.test(props.text) ? "words" : "letters") + + if (basedOn === "words") { + units.current = props.text.split(/\b|(?=\W)/) + } else if (basedOn === "letters") { + units.current = Array.from(props.text) + } else { + // default + units.current = props.text.split(/\b|(?=\W)/) + } + shadowRef.current.innerHTML = units.current + .map(c => { + return `${c}` + }) + .join("") + const ellipsisIndex = putEllipsis(calcIndexes()) + const nextClamped = ellipsisIndex > -1 + const nextDisplayedText = nextClamped ? units.current.slice(0, ellipsisIndex).join("") : props.text + setClamped(nextClamped) + setDisplayedText(nextDisplayedText) + onReflow({ clamped: nextClamped, text: nextDisplayedText }) + } + + // return the index of the first letter/word of each line + // row count: maxLine + 1 + const calcIndexes = () => { + const indexes = [0] + let spanNode = shadowRef.current.firstElementChild + if (!spanNode) return indexes + + let index = 0 + let line = 1 + let offsetTop = spanNode.offsetTop + while ((spanNode = spanNode.nextElementSibling)) { + if (spanNode.offsetTop > offsetTop) { + line++ + indexes.push(index) + offsetTop = spanNode.offsetTop + } + index++ + if (line > maxLine) { + break + } + } + return indexes + } + + const putEllipsis = indexes => { + // no ellipsis + if (indexes.length <= maxLine) return -1 + const lastIndex = indexes[maxLine] + const truncatedUnits = units.current.slice(0, lastIndex) + + // the first letter/word of maxLine + 1 row + const maxOffsetTop = shadowRef.current.children[lastIndex].offsetTop + shadowRef.current.innerHTML = + truncatedUnits + .map((c, i) => { + return `${c}` + }) + .join("") + `${ellipsisRef.current.innerHTML}` + const ellipsisNode = shadowRef.current.lastElementChild + let lastTextNode = prevSibling(ellipsisNode, 1) + while ( + lastTextNode && + (ellipsisNode.offsetTop > maxOffsetTop || + ellipsisNode.offsetHeight > lastTextNode.offsetHeight || + ellipsisNode.offsetTop > lastTextNode.offsetTop) + ) { + shadowRef.current.removeChild(lastTextNode) + lastTextNode = prevSibling(ellipsisNode, 1) + truncatedUnits.pop() + } + return truncatedUnits.length + } + + return ( + <> + + {trimRight ? displayedText.trimRight() : displayedText} + {clamped && {ellipsis}} + +
+ + {ellipsis} + + + ) +} + +export default LinesEllipsis diff --git a/src/constants/brandKit.ts b/src/constants/brandKit.ts index c6231064a..155da4164 100644 --- a/src/constants/brandKit.ts +++ b/src/constants/brandKit.ts @@ -29,6 +29,7 @@ export const brandAssets = [ versions: [ { title: "Full coloured logo on light background", + type: "light", cover: FullColouredLogoSvg, coverClass: "LogoDemo", formats: { @@ -39,6 +40,7 @@ export const brandAssets = [ }, { title: "White logo on dark background", + type: "dark", cover: InvertedLogoSvg, coverClass: "LogoDemo", formats: { diff --git a/src/constants/storageKey.ts b/src/constants/storageKey.ts index 50bdc3ca0..8a680ea57 100644 --- a/src/constants/storageKey.ts +++ b/src/constants/storageKey.ts @@ -11,6 +11,10 @@ export const BRIDGE_TRANSACTIONS = "bridgeTransactions" export const CLAIM_TRANSACTIONS = "claimTransactions" +export const BRIDGE_TRANSACTIONS_V2 = "bridgeTransactionsV2" + +export const CLAIM_TRANSACTIONS_V2 = "claimTransactions2" + export const APP_VERSION = "appVersion" export const BLOCK_NUMBERS = "blockNumbers" diff --git a/src/constants/transaction.ts b/src/constants/transaction.ts index 8ed9e8b09..1cd441c09 100644 --- a/src/constants/transaction.ts +++ b/src/constants/transaction.ts @@ -2,14 +2,26 @@ export const WAIT_CONFIRMATIONS = 10 export const BRIDGE_PAGE_SIZE = 3 -export const CLAIM_TABEL_PAGE_SIZE = 5 +export const WITHDRAW_TABLE_PAGE_SIZE = 5 + +export const CLAIM_TABLE_PAGE_SIZE = 5 export const MAX_CACHE_NUMBER = 18 +export enum TX_TYPE { + ALL, + DEPOSIT, + WITHDRAW, + CLAIM, +} + export enum TX_STATUS { - success = "Success", - pending = "Pending", - failed = "Failed", - cancelled = "Cancelled", - empty = "N/A", + Unknown = -1, + Sent, + SentFailed, + Relayed, + FailedRelayed, + RelayedReverted, + Skipped, + Dropped, } diff --git a/src/contexts/BridgeContextProvider.tsx b/src/contexts/BridgeContextProvider.tsx index 8abf70fe1..e79df7c60 100644 --- a/src/contexts/BridgeContextProvider.tsx +++ b/src/contexts/BridgeContextProvider.tsx @@ -12,7 +12,7 @@ import { CHAIN_ID, ETH_SYMBOL, GATEWAY_ROUTE_PROXY_ADDR, NATIVE_TOKEN_LIST, RPC_ import { BLOCK_NUMBERS, BRIDGE_TOKEN_SYMBOL, USER_TOKEN_LIST } from "@/constants/storageKey" import { useRainbowContext } from "@/contexts/RainbowProvider" import useBlockNumbers from "@/hooks/useBlockNumbers" -import useClaim from "@/hooks/useClaim" +import useClaimHistory from "@/hooks/useClaimHistory" import useTokenPrice from "@/hooks/useTokenPrice" import useTxHistory, { TxHistory } from "@/hooks/useTxHistory" import { loadState } from "@/utils/localStorage" @@ -32,7 +32,7 @@ type BridgeContextProps = { txHistory: TxHistory blockNumbers: number[] tokenList: Token[] - claim: any + claimHistory: TxHistory tokenPrice: Prices refreshTokenList: () => void } @@ -54,7 +54,7 @@ const BridgeContextProvider = ({ children }: any) => { const [fetchTokenListError, setFetchTokenListError] = useState("") const txHistory = useTxHistory() - const claim = useClaim() + const claimHistory = useClaimHistory() // TODO: need refactoring inspired by publicClient and walletClient const update = async (walletProvider: BrowserProvider, address: string) => { @@ -161,7 +161,7 @@ const BridgeContextProvider = ({ children }: any) => { networksAndSigners, txHistory, blockNumbers, - claim, + claimHistory, tokenList: tokenList ?? NATIVE_TOKEN_LIST, refreshTokenList, tokenPrice, @@ -192,10 +192,10 @@ const BridgeContextProvider = ({ children }: any) => { ) } -export function useBrigeContext() { +export function useBridgeContext() { const ctx = useContext(BridgeContext) if (!ctx) { - throw new Error("useBrigeContext must be used within BridgeContextProvider") + throw new Error("useBridgeContext must be used within BridgeContextProvider") } return ctx } diff --git a/src/contexts/PriceFeeProvider.tsx b/src/contexts/PriceFeeProvider.tsx index c09a84d34..8f6487715 100644 --- a/src/contexts/PriceFeeProvider.tsx +++ b/src/contexts/PriceFeeProvider.tsx @@ -5,7 +5,7 @@ import { useBlockNumber } from "wagmi" import { CHAIN_ID, ETH_SYMBOL } from "@/constants" import { BRIDGE_TOKEN_SYMBOL } from "@/constants/storageKey" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" import { requireEnv, trimErrorMessage } from "@/utils" @@ -98,7 +98,7 @@ export const usePriceFeeContext = () => { export const PriceFeeProvider = ({ children }) => { const { walletCurrentAddress, chainId } = useRainbowContext() const [tokenSymbol] = useStorage(localStorage, BRIDGE_TOKEN_SYMBOL, ETH_SYMBOL) - const { networksAndSigners, tokenList } = useBrigeContext() + const { networksAndSigners, tokenList } = useBridgeContext() const [gasLimit, setGasLimit] = useState(BigInt(0)) const [gasPrice, setGasPrice] = useState(BigInt(0)) const [errorMessage, setErrorMessage] = useState("") diff --git a/src/hooks/useAddToken.ts b/src/hooks/useAddToken.ts index 767612d7b..bb3e25204 100644 --- a/src/hooks/useAddToken.ts +++ b/src/hooks/useAddToken.ts @@ -6,7 +6,7 @@ import L1_erc20ABI from "@/assets/abis/L1_erc20ABI.json" import L2_GATEWAY_ROUTER_PROXY_ABI from "@/assets/abis/L2_GATEWAY_ROUTER_PROXY_ADDR.json" import { CHAIN_ID, GATEWAY_ROUTE_PROXY_ADDR } from "@/constants" import { USER_TOKEN_LIST } from "@/constants/storageKey" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { loadState, saveState } from "@/utils/localStorage" export enum TOKEN_LEVEL { @@ -16,7 +16,7 @@ export enum TOKEN_LEVEL { } const useAddToken = () => { - const { networksAndSigners, refreshTokenList } = useBrigeContext() + const { networksAndSigners, refreshTokenList } = useBridgeContext() const [isLoading, setIsLoading] = useState(false) const getProvider = chainId => networksAndSigners[chainId].provider diff --git a/src/hooks/useApprove.ts b/src/hooks/useApprove.ts index 49d51f20b..b46464314 100644 --- a/src/hooks/useApprove.ts +++ b/src/hooks/useApprove.ts @@ -4,13 +4,13 @@ import { useCallback, useEffect, useMemo, useState } from "react" import L1_erc20ABI from "@/assets/abis/L1_erc20ABI.json" import { GATEWAY_ROUTE_PROXY_ADDR, USDC_GATEWAY_PROXY_ADDR, USDC_SYMBOL, WETH_GATEWAY_PROXY_ADDR, WETH_SYMBOL } from "@/constants" import { CHAIN_ID } from "@/constants" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" import { amountToBN } from "@/utils" const useApprove = (fromNetwork, selectedToken, amount) => { const { walletCurrentAddress, chainId } = useRainbowContext() - const { networksAndSigners } = useBrigeContext() + const { networksAndSigners } = useBridgeContext() const [isLoading, setIsLoading] = useState(false) const [isRequested, setIsRequested] = useState(false) diff --git a/src/hooks/useBalance.tsx b/src/hooks/useBalance.tsx index 1bbeee80e..33c67f59b 100644 --- a/src/hooks/useBalance.tsx +++ b/src/hooks/useBalance.tsx @@ -2,12 +2,12 @@ import { ethers } from "ethers" import useSWR from "swr" import L1_erc20ABI from "@/assets/abis/L1_erc20ABI.json" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" const useBalance = (token: any, network?: any) => { const { walletCurrentAddress } = useRainbowContext() - const { networksAndSigners } = useBrigeContext() + const { networksAndSigners } = useBridgeContext() async function fetchBalance({ provider, token, network, address }) { try { diff --git a/src/hooks/useCheckClaimStatus.ts b/src/hooks/useCheckClaimStatus.ts deleted file mode 100644 index 21469eca1..000000000 --- a/src/hooks/useCheckClaimStatus.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useMemo } from "react" - -import { ClaimStatus } from "@/stores/claimStore" -import useTxStore from "@/stores/txStore" - -import useLastFinalizedBatchIndex from "./useLastFinalizedBatchIndex" - -const useCheckClaimStatus = tx => { - const { lastFinalizedBatchIndex } = useLastFinalizedBatchIndex() - - const { estimatedTimeMap } = useTxStore() - - const claimStatus = useMemo(() => { - const { assumedStatus, toBlockNumber, claimInfo } = tx - - if (assumedStatus) { - return ClaimStatus.FAILED - } - if (toBlockNumber) { - return ClaimStatus.CLAIMED - } - // The estimated claim time will not exceed 5 minutes. - if (estimatedTimeMap[`claim_${tx.hash}`] + 1000 * 60 * 5 > Date.now()) { - return ClaimStatus.CLAIMING - } - if (+claimInfo?.batch_index && claimInfo?.batch_index <= lastFinalizedBatchIndex) { - return ClaimStatus.CLAIMABLE - } - return ClaimStatus.NOT_READY - }, [tx, lastFinalizedBatchIndex, estimatedTimeMap]) - - const claimTip = useMemo(() => { - if (claimStatus === ClaimStatus.FAILED) { - return "-" - } else if (claimStatus === ClaimStatus.CLAIMABLE) { - return "Ready to be claimed" - } else if (claimStatus === ClaimStatus.NOT_READY) { - return "Pending..." - } - - return "" - }, [claimStatus]) - - return { claimStatus, claimTip } -} - -export default useCheckClaimStatus diff --git a/src/hooks/useClaim.ts b/src/hooks/useClaim.ts index 32adc12b6..736e97b6d 100644 --- a/src/hooks/useClaim.ts +++ b/src/hooks/useClaim.ts @@ -1,84 +1,58 @@ -import { useCallback, useEffect, useMemo, useState } from "react" -import useSWR from "swr" +import { ethers } from "ethers" +import { useState } from "react" -import { fetchTxByHashUrl } from "@/apis/bridge" -import { CLAIM_TABEL_PAGE_SIZE } from "@/constants" +import L1ScrollMessenger from "@/assets/abis/L1ScrollMessenger.json" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" -import useBridgeStore from "@/stores/bridgeStore" -import useClaimStore from "@/stores/claimStore" - -export interface TxHistory { - errorMessage: string - refreshPageTransactions: (page) => void - changeErrorMessage: (value) => void -} - -const useClaim = () => { - const { walletCurrentAddress } = useRainbowContext() - const { txType, withDrawStep, historyVisible } = useBridgeStore() - - const { comboPageTransactions, pageTransactions, generateTransactions } = useClaimStore() - - const isOnClaimPage = useMemo(() => { - return !historyVisible && txType === "Withdraw" && withDrawStep === "2" - }, [historyVisible, txType, withDrawStep]) - - const [errorMessage, setErrorMessage] = useState("") - - const fetchTxList = useCallback(({ txs }) => { - return scrollRequest(fetchTxByHashUrl, { - method: "post", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ txs }), - }).then(data => data) - }, []) - - const { data } = useSWR( - () => { - const needToRefreshTransactions = pageTransactions.filter(item => !item.toHash) - if (needToRefreshTransactions.length && walletCurrentAddress && isOnClaimPage) { - const txs = needToRefreshTransactions.map(item => item.hash).filter((item, index, arr) => index === arr.indexOf(item)) - return { txs } - } - return null - }, - fetchTxList, - { - onError: (error, key) => { - setErrorMessage(`${error.status}:${error.message}`) - }, - refreshInterval: 2000, - }, - ) - - const refreshPageTransactions = useCallback( - page => { - if (walletCurrentAddress) { - comboPageTransactions(walletCurrentAddress, page, CLAIM_TABEL_PAGE_SIZE).catch(e => { - setErrorMessage(e) +import useTxStore from "@/stores/txStore" +import { CLAIM_OFFSET_TIME } from "@/stores/utils" +import { requireEnv } from "@/utils" + +export function useClaim(props) { + const { tx } = props + const { networksAndSigners } = useBridgeContext() + const [loading, setLoading] = useState(false) + const { addEstimatedTimeMap } = useTxStore() + const { chainId } = useRainbowContext() + + const relayMessageWithProof = async () => { + const contract = new ethers.Contract(requireEnv("REACT_APP_L1_SCROLL_MESSENGER"), L1ScrollMessenger, networksAndSigners[chainId as number].signer) + const { + from, + to, + value, + nonce, + message, + proof: { batch_index, merkle_proof }, + } = tx.claimInfo + + try { + setLoading(true) + addEstimatedTimeMap(`progress_${tx.hash}`, Date.now() + CLAIM_OFFSET_TIME) + const result = await contract.relayMessageWithProof(from, to, value, nonce, message, { + batchIndex: batch_index, + merkleProof: merkle_proof, + }) + result + .wait() + .then(() => { + addEstimatedTimeMap(`progress_${tx.hash}`, Date.now() + CLAIM_OFFSET_TIME) }) - } - }, - [walletCurrentAddress], - ) - - useEffect(() => { - refreshPageTransactions(1) - }, [refreshPageTransactions]) - - useEffect(() => { - if (data?.data?.result.length) { - generateTransactions(data.data.result) + .catch(error => { + // TRANSACTION_REPLACED or TIMEOUT + addEstimatedTimeMap(`progress_${tx.hash}`, 0) + }) + .finally(() => { + setLoading(false) + }) + } catch (error) { + addEstimatedTimeMap(`progress_${tx.hash}`, 0) + setLoading(false) } - }, [data]) + } return { - errorMessage, - refreshPageTransactions, - changeErrorMessage: setErrorMessage, + relayMessageWithProof, + loading, } } - -export default useClaim diff --git a/src/hooks/useClaimHistory.ts b/src/hooks/useClaimHistory.ts new file mode 100644 index 000000000..7d503072c --- /dev/null +++ b/src/hooks/useClaimHistory.ts @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useMemo, useState } from "react" +import useSWR from "swr" + +import { fetchTxByHashUrl } from "@/apis/bridge" +import { CLAIM_TABLE_PAGE_SIZE, TX_STATUS } from "@/constants" +import { useRainbowContext } from "@/contexts/RainbowProvider" +import useBridgeStore from "@/stores/bridgeStore" +import useClaimHistoryStore from "@/stores/claimStore" + +export interface TxHistory { + errorMessage: string + refreshPageTransactions: (page) => void + changeErrorMessage: (value) => void +} + +const useClaimHistory = () => { + const { walletCurrentAddress } = useRainbowContext() + const { txType, withDrawStep, historyVisible } = useBridgeStore() + + const { comboPageTransactions, pageTransactions, generateTransactions } = useClaimHistoryStore() + + const isOnClaimPage = useMemo(() => { + return !historyVisible && txType === "Withdraw" && withDrawStep === "2" + }, [historyVisible, txType, withDrawStep]) + + const [errorMessage, setErrorMessage] = useState("") + + const fetchTxList = useCallback(({ txs }) => { + return scrollRequest(fetchTxByHashUrl, { + method: "post", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ txs }), + }).then(data => data) + }, []) + + const { data } = useSWR( + () => { + const needToRefreshTransactions = pageTransactions.filter(item => item.txStatus !== TX_STATUS.Relayed) + if (needToRefreshTransactions.length && walletCurrentAddress && isOnClaimPage) { + const txs = needToRefreshTransactions.map(item => item.hash).filter((item, index, arr) => index === arr.indexOf(item)) + return { txs } + } + return null + }, + fetchTxList, + { + onError: (error, key) => { + setErrorMessage(`${error.status}:${error.message}`) + }, + refreshInterval: 2000, + }, + ) + + const refreshPageTransactions = useCallback( + page => { + if (walletCurrentAddress) { + comboPageTransactions(walletCurrentAddress, page, CLAIM_TABLE_PAGE_SIZE).catch(e => { + setErrorMessage(e) + }) + } + }, + [walletCurrentAddress], + ) + + useEffect(() => { + if (data?.data?.results.length) { + generateTransactions(walletCurrentAddress, data.data.results) + } + }, [data]) + + return { + errorMessage, + refreshPageTransactions, + changeErrorMessage: setErrorMessage, + } +} + +export default useClaimHistory diff --git a/src/hooks/useEstimateSendTransaction.ts b/src/hooks/useEstimateSendTransaction.ts index 3bb53ba32..e01144541 100644 --- a/src/hooks/useEstimateSendTransaction.ts +++ b/src/hooks/useEstimateSendTransaction.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react" import { CHAIN_ID } from "@/constants" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { usePriceFeeContext } from "@/contexts/PriceFeeProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" @@ -10,7 +10,7 @@ export function useEstimateSendTransaction(props) { const { checkConnectedChainId, walletCurrentAddress } = useRainbowContext() const { gasLimit, gasPrice } = usePriceFeeContext() - const { networksAndSigners } = useBrigeContext() + const { networksAndSigners } = useBridgeContext() const [instance, setInstance] = useState(null) diff --git a/src/hooks/useGasFee.ts b/src/hooks/useGasFee.ts index 21bb9e9e4..c694201cd 100644 --- a/src/hooks/useGasFee.ts +++ b/src/hooks/useGasFee.ts @@ -2,14 +2,14 @@ import { getPublicClient } from "@wagmi/core" import { useState } from "react" import { useBlockNumber } from "wagmi" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import useBridgeStore from "@/stores/bridgeStore" import { trimErrorMessage } from "@/utils" import { useEstimateSendTransaction } from "./useEstimateSendTransaction" const useGasFee = (selectedToken, needApproval) => { - const { networksAndSigners } = useBrigeContext() + const { networksAndSigners } = useBridgeContext() const { fromNetwork, toNetwork } = useBridgeStore() const { estimateSend } = useEstimateSendTransaction({ fromNetwork, diff --git a/src/hooks/useLastFinalizedBatchIndex.tsx b/src/hooks/useLastFinalizedBatchIndex.tsx index 0b0ad8c17..4a2e34b3c 100644 --- a/src/hooks/useLastFinalizedBatchIndex.tsx +++ b/src/hooks/useLastFinalizedBatchIndex.tsx @@ -3,11 +3,11 @@ import useSWR from "swr" import ScrollChain from "@/assets/abis/ScrollChain.json" import { CHAIN_ID } from "@/constants" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { requireEnv } from "@/utils" const useLastFinalizedBatchIndex = () => { - const { networksAndSigners } = useBrigeContext() + const { networksAndSigners } = useBridgeContext() async function fetchLastFinalizedBatchIndex() { const provider = networksAndSigners[CHAIN_ID.L1].provider diff --git a/src/hooks/useRetry.ts b/src/hooks/useRetry.ts new file mode 100644 index 000000000..349903d72 --- /dev/null +++ b/src/hooks/useRetry.ts @@ -0,0 +1,71 @@ +import { ethers } from "ethers" +import { useState } from "react" + +import L1MessageQueue from "@/assets/abis/L1MessageQueue.json" +import L1ScrollMessenger from "@/assets/abis/L1ScrollMessenger.json" +import L2GasPriceOracle from "@/assets/abis/L2GasPriceOracle.json" +import { CHAIN_ID } from "@/constants" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" +import useTxStore from "@/stores/txStore" +import { MAX_OFFSET_TIME } from "@/stores/utils" +import { requireEnv } from "@/utils" + +export function useRetry(props) { + const { hash } = props + const { networksAndSigners } = useBridgeContext() + const [loading, setLoading] = useState(false) + const { addEstimatedTimeMap } = useTxStore() + + const replayMessage = async () => { + const l2provider = networksAndSigners[CHAIN_ID.L2].provider + const deployer = networksAndSigners[CHAIN_ID.L1].signer + const queue = new ethers.Contract(requireEnv("REACT_APP_L1_MESSAGE_QUEUE"), L1MessageQueue, deployer) + const messenger = new ethers.Contract(requireEnv("REACT_APP_L1_SCROLL_MESSENGER"), L1ScrollMessenger, deployer) + const oracle = new ethers.Contract(requireEnv("REACT_APP_L2_GAS_PRICE_ORACLE"), L2GasPriceOracle, deployer) + + setLoading(true) + try { + const receipts = await networksAndSigners[CHAIN_ID.L1].provider.getTransactionReceipt(hash) + + let gasLimit = BigInt(0) + + for (const log of receipts.logs) { + if (log.topics[0] === "0x69cfcb8e6d4192b8aba9902243912587f37e550d75c1fa801491fce26717f37e") { + const event = queue.interface.decodeEventLog("QueueTransaction", log.data, log.topics) + gasLimit = await l2provider.estimateGas({ + from: event.sender, + to: event.target, + value: event.value, + data: event.data, + }) + } else if (log.topics[0] === "0x104371f3b442861a2a7b82a070afbbaab748bb13757bf47769e170e37809ec1e") { + const event = messenger.interface.decodeEventLog("SentMessage", log.data, log.topics) + const gasPirce = await oracle.estimateCrossDomainMessageFee((gasLimit * BigInt(12)) / BigInt(10)) + + await messenger.replayMessage( + event.sender, + event.target, + event.value, + event.messageNonce, + event.message, + (gasLimit * BigInt(12)) / BigInt(10), + deployer.address, + { + value: (gasPirce * BigInt(12)) / BigInt(10), + }, + ) + addEstimatedTimeMap(`progress_${hash}`, Date.now() + MAX_OFFSET_TIME) + setLoading(false) + } + } + } catch (error) { + console.error("Error replaying message:", error) + setLoading(false) + } + } + + return { + replayMessage, + loading, + } +} diff --git a/src/hooks/useSendTransaction.ts b/src/hooks/useSendTransaction.ts index 98a1a3cfb..0af03b023 100644 --- a/src/hooks/useSendTransaction.ts +++ b/src/hooks/useSendTransaction.ts @@ -1,13 +1,13 @@ import { isError } from "ethers" import { useMemo, useState } from "react" -import { CHAIN_ID, NETWORKS } from "@/constants" -import { TX_STATUS } from "@/constants" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { CHAIN_ID, NETWORKS, TX_STATUS } from "@/constants" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { usePriceFeeContext } from "@/contexts/PriceFeeProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" import useBridgeStore from "@/stores/bridgeStore" -import useTxStore, { TxDirection, TxPosition, isValidOffsetTime } from "@/stores/txStore" +import useTxStore from "@/stores/txStore" +import { isValidOffsetTime } from "@/stores/utils" import { amountToBN, isSepolia, sentryDebug } from "@/utils" import useGasFee from "./useGasFee" @@ -21,9 +21,9 @@ type TxOptions = { export function useSendTransaction(props) { const { amount: fromTokenAmount, selectedToken } = props const { walletCurrentAddress } = useRainbowContext() - const { networksAndSigners, blockNumbers } = useBrigeContext() + const { networksAndSigners, blockNumbers } = useBridgeContext() const { enlargedGasLimit: txGasLimit, maxFeePerGas, maxPriorityFeePerGas } = useGasFee(selectedToken, false) - const { addTransaction, updateTransaction, addEstimatedTimeMap, updateOrderedTxs, addAbnormalTransactions, removeFrontTransactions } = useTxStore() + const { addTransaction, addEstimatedTimeMap, removeFrontTransactions, updateTransaction } = useTxStore() const { fromNetwork, toNetwork, changeTxResult, changeWithdrawStep } = useBridgeStore() const { gasLimit, gasPrice } = usePriceFeeContext() @@ -37,7 +37,6 @@ export function useSendTransaction(props) { const send = async () => { setIsLoading(true) - const txDirection = fromNetwork.isL1 ? TxDirection.Deposit : TxDirection.Withdraw let tx let currentBlockNumber try { @@ -53,7 +52,6 @@ export function useSendTransaction(props) { tx = tx.replaceableTransaction(currentBlockNumber) handleTransaction(tx) - updateOrderedTxs(walletCurrentAddress, tx.hash, TxPosition.Frontend, txDirection) tx.wait() .then(receipt => { if (receipt?.status === 1) { @@ -78,9 +76,8 @@ export function useSendTransaction(props) { setSendError({ code: 0, message: errorMessage }) // Something failed in the EVM - updateOrderedTxs(walletCurrentAddress, tx.hash, TxPosition.Abnormal, txDirection) // EIP-658 - markTransactionAbnormal(tx, TX_STATUS.failed, errorMessage) + removeFrontTransactions(tx.hash) } }) .catch(error => { @@ -88,8 +85,7 @@ export function useSendTransaction(props) { sentryDebug(error.message) if (isError(error, "TRANSACTION_REPLACED")) { if (error.cancelled) { - markTransactionAbnormal(tx, TX_STATUS.cancelled, "transaction was cancelled") - updateOrderedTxs(walletCurrentAddress, tx.hash, TxPosition.Abnormal, txDirection) + removeFrontTransactions(tx.hash) setSendError("cancel") } else { const { blockNumber, hash: transactionHash } = error.receipt @@ -97,7 +93,6 @@ export function useSendTransaction(props) { fromBlockNumber: blockNumber, hash: transactionHash, }) - updateOrderedTxs(walletCurrentAddress, tx.hash, transactionHash, txDirection) if (fromNetwork.isL1) { const estimatedOffsetTime = (blockNumber - blockNumbers[0]) * 12 * 1000 if (isValidOffsetTime(estimatedOffsetTime)) { @@ -111,8 +106,7 @@ export function useSendTransaction(props) { } else { setSendError(error) // when the transaction execution failed (status is 0) - updateOrderedTxs(walletCurrentAddress, tx.hash, TxPosition.Abnormal, txDirection) - markTransactionAbnormal(tx, TX_STATUS.failed, error.message) + removeFrontTransactions(tx.hash) } }) .finally(() => { @@ -131,31 +125,20 @@ export function useSendTransaction(props) { const handleTransaction = (tx, updateOpts?) => { if (updateOpts) { - updateTransaction(tx.hash, updateOpts) + updateTransaction(walletCurrentAddress, tx.hash, updateOpts) return } - addTransaction({ + addTransaction(walletCurrentAddress, { hash: tx.hash, amount: parsedAmount.toString(), isL1: fromNetwork.name === NETWORKS[0].name, symbolToken: selectedToken.address, timestamp: Date.now(), + initiatedAt: Math.floor(new Date().getTime() / 1000), + txStatus: TX_STATUS.Unknown, }) } - const markTransactionAbnormal = (tx, assumedStatus, errMsg) => { - addAbnormalTransactions(walletCurrentAddress, { - hash: tx.hash, - amount: parsedAmount.toString(), - isL1: fromNetwork.name === NETWORKS[0].name, - symbolToken: selectedToken.address, - assumedStatus, - errMsg, - }) - removeFrontTransactions(tx.hash) - updateTransaction(tx.hash, { assumedStatus }) - } - const depositETH = async () => { const fee = gasPrice * gasLimit const options: TxOptions = { diff --git a/src/hooks/useSufficientBalance.ts b/src/hooks/useSufficientBalance.ts index 22549259a..f5f32655c 100644 --- a/src/hooks/useSufficientBalance.ts +++ b/src/hooks/useSufficientBalance.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" import { useIsSmartContractWallet } from "@/hooks" import useBridgeStore from "@/stores/bridgeStore" @@ -10,7 +10,7 @@ import useTransactionBuffer from "./useTransactionBuffer" function useSufficientBalance(selectedToken: any, amount?: bigint, fee?: bigint | null, tokenBalance: bigint = BigInt(0)) { const { walletCurrentAddress, chainId } = useRainbowContext() - const { networksAndSigners } = useBrigeContext() + const { networksAndSigners } = useBridgeContext() const networksAndSigner = networksAndSigners[chainId as number] const transactionBuffer = useTransactionBuffer(selectedToken) diff --git a/src/hooks/useTokenInfo.ts b/src/hooks/useTokenInfo.ts index 927574167..66c70612a 100644 --- a/src/hooks/useTokenInfo.ts +++ b/src/hooks/useTokenInfo.ts @@ -4,11 +4,11 @@ import useSWR from "swr" import L1_erc20ABI from "@/assets/abis/L1_erc20ABI.json" import { CHAIN_ID, ETH_SYMBOL } from "@/constants" import { TOKEN_INFO_MAP } from "@/constants/storageKey" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { loadState, saveState } from "@/utils/localStorage" const useTokenInfo = (address: string, isL1: boolean) => { - const { networksAndSigners } = useBrigeContext() + const { networksAndSigners } = useBridgeContext() const { data, isLoading } = useSWR( () => { const provider = networksAndSigners[isL1 ? CHAIN_ID.L1 : CHAIN_ID.L2].provider diff --git a/src/hooks/useTxHistory.ts b/src/hooks/useTxHistory.ts index 41f7117ca..872c3f2dd 100644 --- a/src/hooks/useTxHistory.ts +++ b/src/hooks/useTxHistory.ts @@ -1,8 +1,8 @@ import { useCallback, useEffect, useState } from "react" import useSWR from "swr" -import { fetchClaimableTxListUrl, fetchTxByHashUrl } from "@/apis/bridge" -import { BRIDGE_PAGE_SIZE } from "@/constants" +import { fetchTxByHashUrl } from "@/apis/bridge" +import { BRIDGE_PAGE_SIZE, TX_STATUS } from "@/constants" import { useRainbowContext } from "@/contexts/RainbowProvider" import useBridgeStore from "@/stores/bridgeStore" import useTxStore from "@/stores/txStore" @@ -15,10 +15,9 @@ export interface TxHistory { const useTxHistory = () => { const { walletCurrentAddress } = useRainbowContext() - const { pageTransactions, generateTransactions, comboPageTransactions, combineClaimableTransactions, orderedTxDB, clearTransactions } = useTxStore() + const { pageTransactions, generateTransactions, comboPageTransactions, clearTransactions } = useTxStore() const [errorMessage, setErrorMessage] = useState("") - const [claimableTx, setclaimableTx] = useState<[] | null>(null) const { historyVisible } = useBridgeStore() @@ -36,20 +35,10 @@ const useTxHistory = () => { clearTransactions() }, []) - useEffect(() => { - if (walletCurrentAddress) { - try { - scrollRequest(`${fetchClaimableTxListUrl}?address=${walletCurrentAddress}&page=1&page_size=100`).then(data => { - setclaimableTx(data.data.result) - }) - } catch (error) {} - } - }, [walletCurrentAddress]) - // fetch to hash/blockNumber from backend const { data } = useSWR( () => { - const needToRefreshTransactions = pageTransactions.filter(item => !item.toHash) + const needToRefreshTransactions = pageTransactions.filter(item => item.txStatus !== TX_STATUS.Relayed) if (needToRefreshTransactions.length && walletCurrentAddress && historyVisible) { const txs = needToRefreshTransactions.map(item => item.hash).filter((item, index, arr) => index === arr.indexOf(item)) @@ -78,21 +67,11 @@ const useTxHistory = () => { ) useEffect(() => { - refreshPageTransactions(1) - }, [refreshPageTransactions, orderedTxDB]) - - useEffect(() => { - if (data?.data?.result.length) { - generateTransactions(walletCurrentAddress, data.data.result) + if (data?.data?.results.length) { + generateTransactions(walletCurrentAddress, data.data.results) } }, [data]) - useEffect(() => { - if (claimableTx) { - combineClaimableTransactions(walletCurrentAddress, claimableTx) - } - }, [claimableTx]) - return { errorMessage, refreshPageTransactions, diff --git a/src/pages/brand-kit/Assets/AssetCard.tsx b/src/pages/brand-kit/Assets/AssetCard.tsx index ba002b645..04c30456c 100644 --- a/src/pages/brand-kit/Assets/AssetCard.tsx +++ b/src/pages/brand-kit/Assets/AssetCard.tsx @@ -146,6 +146,10 @@ const useStyles = makeStyles()((theme, { type, name }) => ({ gridArea: "cover", }, + coverdark: { + backgroundColor: theme.palette.themeBackground.dark, + }, + sampleImage0: { gridArea: "sample1", width: "100%", @@ -249,9 +253,9 @@ const AssetCard = props => { {name} {versions.map((version, index) => ( - + {version.title ? {version.title} : null} - + diff --git a/src/pages/brand-kit/Assets/index.tsx b/src/pages/brand-kit/Assets/index.tsx index 0a66d87b8..7f9f3d111 100644 --- a/src/pages/brand-kit/Assets/index.tsx +++ b/src/pages/brand-kit/Assets/index.tsx @@ -10,7 +10,7 @@ const useStyles = makeStyles()(theme => ({ float: { width: "calc(50% - 1.5rem)", float: "right", - "&:nth-child(2n)": { + "&:nth-of-type(2n)": { float: "left", }, [theme.breakpoints.down("md")]: { @@ -25,8 +25,8 @@ const Assets = props => { return ( {brandAssets.map((item, index) => ( - - + + diff --git a/src/pages/brand-kit/index.tsx b/src/pages/brand-kit/index.tsx index d3a71ba06..442d2b31b 100644 --- a/src/pages/brand-kit/index.tsx +++ b/src/pages/brand-kit/index.tsx @@ -7,10 +7,8 @@ import Header from "./Header" const useStyles = makeStyles()(theme => ({ container: { - marginTop: "-6.5rem", - paddingTop: "6.5rem", overflow: "hidden", - background: "#FFEEDA", + background: theme.palette.themeBackground.brand, }, })) diff --git a/src/pages/bridge/Send/Claim.tsx b/src/pages/bridge/Send/Claim.tsx index 3009af040..3ffbd397d 100644 --- a/src/pages/bridge/Send/Claim.tsx +++ b/src/pages/bridge/Send/Claim.tsx @@ -3,14 +3,13 @@ import { makeStyles } from "tss-react/mui" import { Box } from "@mui/material" -import { CLAIM_TABEL_PAGE_SIZE } from "@/constants" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { CLAIM_TABLE_PAGE_SIZE } from "@/constants" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" -import ClaimTable from "@/pages/bridge/components/ClaimTable" -import useBridgeStore from "@/stores/bridgeStore" import useClaimStore from "@/stores/claimStore" import NotConnected from "../components/NoConnected" +import TxTable from "../components/TxTable" const useStyles = makeStyles()(theme => ({ tableBox: { @@ -43,11 +42,10 @@ const Claim = (props: any) => { const { classes } = useStyles() const { walletCurrentAddress, chainId } = useRainbowContext() const { - claim: { refreshPageTransactions }, - } = useBrigeContext() + claimHistory: { refreshPageTransactions }, + } = useBridgeContext() - const { page, total, pageTransactions, loading, targetTransaction, orderedTxDB, setTargetTransaction, clearTransactions } = useClaimStore() - const { historyVisible } = useBridgeStore() + const { page, total, pageTransactions, loading, clearTransactions } = useClaimStore() useEffect(() => { handleChangePage(1) @@ -59,16 +57,6 @@ const Claim = (props: any) => { } }, []) - useEffect(() => { - // if targetTransaction has value, then we need to move to the target transaction - if (targetTransaction) { - const index = orderedTxDB.findIndex(tx => tx.hash === targetTransaction) - const page = Math.ceil((index + 1) / CLAIM_TABEL_PAGE_SIZE) - handleChangePage(page) - setTargetTransaction(null) - } - }, [historyVisible]) - const handleChangePage = currentPage => { refreshPageTransactions(currentPage) } @@ -76,11 +64,12 @@ const Claim = (props: any) => { return ( {chainId ? ( - ({ listItemRoot: { padding: 0, gap: 8, - "&:nth-child(n + 2)": { + "&:nth-of-type(n + 2)": { paddingTop: 8, }, }, @@ -177,6 +177,7 @@ const ApprovalDialog = props => { > {APPROVAL_OPTIONS.map(item => ( { {item.info.map(i => ( - + = props => { const { classes: styles } = useStyles() const { txType, isNetworkCorrect } = useBridgeStore() - const { tokenPrice } = useBrigeContext() + const { tokenPrice } = useBridgeContext() const { amount, feeError, selectedToken, l1GasFee, l2GasFee, l1DataFee, needApproval } = props @@ -164,21 +164,23 @@ const TransactionSummary: FC = props => { Summary - - {txType === "Deposit" && } - - {txType === "Withdraw" && } - - - - - - + + + {txType === "Deposit" && } + + {txType === "Withdraw" && } + + + + + + +
) diff --git a/src/pages/bridge/Send/SendTransaction/index.tsx b/src/pages/bridge/Send/SendTransaction/index.tsx index 19187bd5c..b3f8c3a30 100644 --- a/src/pages/bridge/Send/SendTransaction/index.tsx +++ b/src/pages/bridge/Send/SendTransaction/index.tsx @@ -8,7 +8,7 @@ import Button from "@/components/Button" import TextButton from "@/components/TextButton" import { ETH_SYMBOL } from "@/constants" import { BRIDGE_TOKEN_SYMBOL } from "@/constants/storageKey" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { usePriceFeeContext } from "@/contexts/PriceFeeProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" import { useAsyncMemo, useBalance } from "@/hooks" @@ -29,7 +29,7 @@ import TransactionSummary from "./TransactionSummary" const SendTransaction = props => { const { chainId, connect } = useRainbowContext() // TODO: extract tokenList - const { tokenList } = useBrigeContext() + const { tokenList } = useBridgeContext() const { isMobile } = useCheckViewport() const [tokenSymbol, setTokenSymbol] = useStorage(localStorage, BRIDGE_TOKEN_SYMBOL, ETH_SYMBOL) @@ -37,7 +37,7 @@ const SendTransaction = props => { const { txType, isNetworkCorrect, fromNetwork, changeTxResult } = useBridgeStore() - const [amount, setAmount] = useState() + const [amount, setAmount] = useState("") const [maxWarning, setMaxWarning] = useState() @@ -294,7 +294,7 @@ const SendTransaction = props => { component={WarningSvg} inheritViewBox >
- + {bridgeWarning} diff --git a/src/pages/bridge/TxHistoryDialog/TxHistoryTable.tsx b/src/pages/bridge/TxHistoryDialog/TxHistoryTable.tsx index 98cf4888c..1a2bc23c3 100644 --- a/src/pages/bridge/TxHistoryDialog/TxHistoryTable.tsx +++ b/src/pages/bridge/TxHistoryDialog/TxHistoryTable.tsx @@ -2,7 +2,7 @@ import { Box } from "@mui/material" import { styled } from "@mui/system" import { BRIDGE_PAGE_SIZE } from "@/constants" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" import { useRainbowContext } from "@/contexts/RainbowProvider" import useTxStore from "@/stores/txStore" @@ -23,7 +23,7 @@ const TableBox = styled(Box)(({ theme }) => ({ const TransactionsList = (props: any) => { const { txHistory: { refreshPageTransactions }, - } = useBrigeContext() + } = useBridgeContext() const { chainId } = useRainbowContext() const { page, total, pageTransactions, loading } = useTxStore() diff --git a/src/pages/bridge/TxHistoryDialog/index.tsx b/src/pages/bridge/TxHistoryDialog/index.tsx index 8211e32c4..2ab50765e 100644 --- a/src/pages/bridge/TxHistoryDialog/index.tsx +++ b/src/pages/bridge/TxHistoryDialog/index.tsx @@ -1,8 +1,11 @@ +import { useEffect } from "react" import { makeStyles } from "tss-react/mui" import { Dialog, DialogTitle, IconButton, SvgIcon, Typography } from "@mui/material" import { ReactComponent as CloseSvg } from "@/assets/svgs/bridge/close.svg" +import { useBridgeContext } from "@/contexts/BridgeContextProvider" +import { useRainbowContext } from "@/contexts/RainbowProvider" import useBridgeStore from "@/stores/bridgeStore" import TxHistoryTable from "./TxHistoryTable" @@ -21,9 +24,20 @@ const useStyles = makeStyles()(theme => ({ const TxHistoryDialog = (props: any) => { const { classes } = useStyles() const { historyVisible, changeHistoryVisible } = useBridgeStore() + const { walletCurrentAddress } = useRainbowContext() const handleClose = () => { changeHistoryVisible(false) } + const { + txHistory: { refreshPageTransactions }, + } = useBridgeContext() + + useEffect(() => { + if (historyVisible && walletCurrentAddress) { + refreshPageTransactions(1) + } + }, [historyVisible, walletCurrentAddress]) + return ( { id="customized-dialog-title" > Transaction History + diff --git a/src/pages/bridge/components/ClaimTable/ClaimButton.tsx b/src/pages/bridge/components/ClaimTable/ClaimButton.tsx deleted file mode 100644 index 966c68cf2..000000000 --- a/src/pages/bridge/components/ClaimTable/ClaimButton.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import dayjs from "dayjs" -import { ethers } from "ethers" -import { isError } from "ethers" -import { useState } from "react" -import Countdown from "react-countdown" - -import { Box, ButtonBase, Chip, CircularProgress, SvgIcon, Tooltip, alpha } from "@mui/material" -import { styled } from "@mui/system" - -import L1ScrollMessenger from "@/assets/abis/L1ScrollMessenger.json" -import { ReactComponent as InfoSvg } from "@/assets/svgs/bridge/info.svg" -import { TX_STATUS } from "@/constants" -import { CHAIN_ID } from "@/constants/common" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" -import { useRainbowContext } from "@/contexts/RainbowProvider" -import { ClaimStatus } from "@/stores/claimStore" -import useTxStore, { TxPosition, isValidOffsetTime } from "@/stores/txStore" -import { requireEnv, sentryDebug, switchNetwork } from "@/utils" - -const StyledButton = styled(ButtonBase)(({ theme }) => ({ - width: "15rem", - height: "4rem", - fontSize: "1.4rem", - fontWeight: 600, - - borderRadius: "0.5rem", - display: "flex", - justifyContent: "center", - alignItems: "center", - - color: theme.palette.primary.contrastText, - background: theme.palette.primary.main, -})) - -const StyledChip = styled(Chip)(({ theme }) => ({ - width: "15rem", - height: "4rem", - fontSize: "1.4rem", - fontWeight: 600, - padding: 0, - borderRadius: "2rem", - verticalAlign: "middle", - - ".MuiChip-label": { - display: "flex", - alignItems: "center", - }, - - "&.loading": { - borderRadius: "0.5rem", - backgroundColor: alpha(theme.palette.primary.main, 0.6), - color: theme.palette.primary.contrastText, - }, - "&.waiting": { - backgroundColor: theme.palette.themeBackground.normal, - color: "#8C591A", - }, - "&.claimed": { - backgroundColor: "#DFFCF8", - color: "#0F8E7E", - }, - "&.failed": { - backgroundColor: "#FFE1DB", - color: "#FF684B", - }, -})) - -const ClaimButton = props => { - const { tx, txStatus, loading, changeLoading } = props - const { networksAndSigners, blockNumbers } = useBrigeContext() - const { chainId, walletCurrentAddress } = useRainbowContext() - const [claimButtonLabel, setClaimButtonLabel] = useState("Claim") - const { updateTransaction, addEstimatedTimeMap, updateOrderedTxs, removeFrontTransactions } = useTxStore() - - const renderEstimatedWaitingTime = initialUTCStr => { - if (!initialUTCStr) { - return <>{tx.isL1 ? "Ready" : "Claimable"} in ... - } - const timestamp = dayjs(tx.initiatedAt).add(1, "h").valueOf() - - return - } - - const renderCountDown = ({ total, hours, minutes, seconds, completed }) => { - if (completed) { - return <>Pending - } - return ( - <> - {tx.isL1 ? "Ready" : "Claimable"} in ~{minutes ? `${minutes}m` : `${seconds}s`} - - ) - } - - const handleSwitchNetwork = async (chainId: number) => { - try { - // cancel switch network in MetaMask would not throw an error and the result is null just like successfully switched - await switchNetwork(chainId) - } catch (error) { - // when there is a switch-network popover in MetaMask and the refreshing page would throw an error - } - } - - const handleClaim = async () => { - const contract = new ethers.Contract(requireEnv("REACT_APP_L1_SCROLL_MESSENGER"), L1ScrollMessenger, networksAndSigners[chainId as number].signer) - const { from, to, value, nonce, message, proof, batch_index } = tx.claimInfo - try { - changeLoading(true) - addEstimatedTimeMap(`claim_${tx.hash}`, Date.now()) - const result = await contract.relayMessageWithProof(from, to, value, nonce, message, { - batchIndex: batch_index, - merkleProof: proof, - }) - result - .wait() - .then(receipt => { - if (receipt?.status === 1) { - const estimatedOffsetTime = (receipt.blockNumber - blockNumbers[0]) * 12 * 1000 - if (isValidOffsetTime(estimatedOffsetTime)) { - addEstimatedTimeMap(`to_${receipt.blockHash}`, Date.now() + estimatedOffsetTime) - addEstimatedTimeMap(`claim_${tx.hash}`, Date.now() + estimatedOffsetTime) - } else { - addEstimatedTimeMap(`to_${receipt.blockHash}`, 0) - addEstimatedTimeMap(`claim_${tx.hash}`, 0) - } - } else { - const errorMessage = "due to any operation that can cause the transaction or top-level call to revert" - //Something failed in the EVM - updateOrderedTxs(walletCurrentAddress, tx.hash, TxPosition.Abnormal) - //EIP - 658 - markTransactionAbnormal(tx, TX_STATUS.failed, errorMessage) - addEstimatedTimeMap(`claim_${tx.hash}`, 0) - } - }) - .catch(error => { - // TRANSACTION_REPLACED or TIMEOUT - sentryDebug(error.message) - if (isError(error, "TRANSACTION_REPLACED")) { - if (error.cancelled) { - markTransactionAbnormal(tx, TX_STATUS.cancelled, "transaction was cancelled") - updateOrderedTxs(walletCurrentAddress, tx.hash, TxPosition.Abnormal) - // setSendError("cancel") - } else { - const { blockNumber } = error.receipt - const estimatedOffsetTime = (blockNumber - blockNumbers[0]) * 12 * 1000 - if (isValidOffsetTime(estimatedOffsetTime)) { - addEstimatedTimeMap(`from_${tx.hash}`, Date.now() + estimatedOffsetTime) - } else { - addEstimatedTimeMap(`claim_${tx.hash}`, 0) - sentryDebug(`safe block number: ${blockNumbers[0]}`) - } - } - } else { - // setSendError(error) - // when the transaction execution failed (status is 0) - updateOrderedTxs(walletCurrentAddress, tx.hash, TxPosition.Abnormal) - markTransactionAbnormal(tx, TX_STATUS.failed, error.message) - addEstimatedTimeMap(`claim_${tx.hash}`, 0) - } - }) - .finally(() => { - changeLoading(false) - }) - } catch (error) { - if (isError(error, "ACTION_REJECTED")) { - addEstimatedTimeMap(`claim_${tx.hash}`, 0) - } - - changeLoading(false) - } - } - - const markTransactionAbnormal = (tx, assumedStatus, errMsg) => { - // addAbnormalTransactions(walletCurrentAddress, { - // hash: tx.hash, - // fromName: fromNetwork.name, - // toName: toNetwork.name, - // fromExplore: fromNetwork.explorer, - // toExplore: toNetwork.explorer, - // amount: parsedAmount.toString(), - // isL1: fromNetwork.name === NETWORKS[0].name, - // symbolToken: selectedToken.address, - // assumedStatus, - // errMsg, - // }) - removeFrontTransactions(tx.hash) - updateTransaction(tx.hash, { assumedStatus }) - } - - if (txStatus === ClaimStatus.FAILED) { - return ( - - - Failed - - - } - > - - ) - } else if (txStatus === ClaimStatus.CLAIMED) { - return - } else if (txStatus === ClaimStatus.CLAIMING || loading) { - return ( - - Claiming - - - } - > - ) - } else if (txStatus === ClaimStatus.CLAIMABLE) { - const isOnScrollLayer1 = chainId === CHAIN_ID.L1 - - if (isOnScrollLayer1) { - return ( - - Claim - - ) - } else { - return ( - - - setClaimButtonLabel("Switch")} - onMouseLeave={() => setClaimButtonLabel("Claim")} - onClick={() => handleSwitchNetwork(CHAIN_ID.L1)} - > - {claimButtonLabel} - - - - ) - } - } - - // ClaimStatus.NOT_READY - return ( - - {renderEstimatedWaitingTime(tx.initiatedAt)}}> - - ) -} - -export default ClaimButton diff --git a/src/pages/bridge/components/ClaimTable/index.tsx b/src/pages/bridge/components/ClaimTable/index.tsx deleted file mode 100644 index 79a5123e9..000000000 --- a/src/pages/bridge/components/ClaimTable/index.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import { useMemo, useState } from "react" -import { makeStyles } from "tss-react/mui" - -import { - Box, - CircularProgress, - Pagination, - Paper, - Skeleton, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, -} from "@mui/material" - -import Link from "@/components/Link" -import { EXPLORER_URL } from "@/constants" -import useCheckClaimStatus from "@/hooks/useCheckClaimStatus" -import useTokenInfo from "@/hooks/useTokenInfo" -import { ClaimStatus } from "@/stores/claimStore" -import { formatDate, generateExploreLink, toTokenDisplay, truncateHash } from "@/utils" - -import NoData from "../NoData" -import ClaimButton from "./ClaimButton" - -const useStyles = makeStyles()(theme => { - return { - tableContainer: { - [theme.breakpoints.down("sm")]: { - width: "100%", - }, - }, - tableWrapper: { - boxShadow: "unset", - backgroundColor: theme.palette.themeBackground.optionHightlight, - borderRadius: 0, - minHeight: "28.7rem", - [theme.breakpoints.down("sm")]: { - width: "100%", - overflowX: "auto", - }, - }, - tableTitle: { - marginTop: "2.8rem", - marginBottom: "3rem", - [theme.breakpoints.down("sm")]: { - marginTop: "1.6rem", - marginBottom: "1.6rem", - }, - }, - tableHeader: { - borderBottom: `3px solid ${theme.palette.border.main}`, - ".MuiTableCell-head": { - borderBottom: "unset", - fontWeight: 600, - fontSize: "1.6rem", - padding: "0.8rem", - color: theme.palette.text.primary, - "&:first-of-type": { - paddingLeft: 0, - }, - }, - }, - tableBody: { - minHeight: "18.3rem", - ".MuiTableCell-root": { - padding: "1.6rem 0.8rem", - [theme.breakpoints.down("sm")]: { - padding: "1.5rem 0.8rem", - }, - "*": { - fontSize: "1.4rem", - }, - "&:first-of-type": { - paddingLeft: 0, - }, - "&:last-of-type": { - paddingRight: 0, - }, - }, - }, - pagination: { - overflowX: "auto", - - ".MuiPagination-ul": { - flexWrap: "nowrap", - }, - ".MuiPaginationItem-text": { - fontSize: "1.6rem", - }, - ".MuiPaginationItem-root": {}, - ".MuiPaginationItem-root.Mui-selected": { - fontWeight: 700, - backgroundColor: "unset", - }, - ".MuiSvgIcon-root": { - fontSize: "2.4rem", - }, - }, - loadingBox: { - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "0", - background: "rgba(255,255,255,0.5)", - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - loadingIndicator: { - color: "#EB7106", - }, - } -}) - -const ClaimTable = (props: any) => { - const { data, loading, pagination } = props - const { classes } = useStyles() - - const handleChangePage = (e, newPage) => { - pagination?.onChange?.(newPage) - } - - return ( - - {data.length ? ( - <> - - - - - Claim - Amount - Initiated At - Transaction Hash - - - - <> - {data?.map((tx: any) => ( - - ))} - - -
-
- {pagination && ( - - - - )} - - ) : ( - - )} - - {loading ? ( - - - - ) : null} -
- ) -} - -const TxRow = props => { - const { tx } = props - - const { loading: tokenInfoLoading, tokenInfo } = useTokenInfo(tx.symbolToken, tx.isL1) - const [loading, setLoading] = useState(false) - - const txAmount = amount => { - return toTokenDisplay(amount, tokenInfo?.decimals ? BigInt(tokenInfo.decimals) : undefined) - } - const { claimStatus, claimTip } = useCheckClaimStatus(tx) - - const claimTipWithLoading = useMemo(() => { - if (loading || claimStatus === ClaimStatus.CLAIMING) { - return "Claiming in progress" - } - return claimTip - }, [claimStatus, claimTip, loading]) - - return ( - - - - - - - {txAmount(tx.amount)} - {tokenInfoLoading ? : {tokenInfo?.symbol}} - - - - {tx.initiatedAt ? formatDate(tx.initiatedAt, { withTime: true }) : "-"} - - - - - Scroll:{" "} - - {truncateHash(tx.hash)} - - - - - - - Ethereum:{" "} - {tx.toHash ? ( - - {truncateHash(tx.toHash)} - - ) : ( - {claimTipWithLoading} - )} - - - - - ) -} - -export default ClaimTable diff --git a/src/pages/bridge/components/TxTable/ActiveButton.tsx b/src/pages/bridge/components/TxTable/ActiveButton.tsx new file mode 100644 index 000000000..003330ec4 --- /dev/null +++ b/src/pages/bridge/components/TxTable/ActiveButton.tsx @@ -0,0 +1,118 @@ +import { useState } from "react" + +import { Box, ButtonBase, Chip, CircularProgress, Tooltip, alpha } from "@mui/material" +import { styled } from "@mui/system" + +import { CHAIN_ID } from "@/constants/common" +import { useRainbowContext } from "@/contexts/RainbowProvider" +import { useClaim } from "@/hooks/useClaim" +import { useRetry } from "@/hooks/useRetry" +import useTxStore from "@/stores/txStore" +import { switchNetwork } from "@/utils" + +const StyledButton = styled(ButtonBase)(({ theme }) => ({ + width: "15rem", + height: "4rem", + fontSize: "1.4rem", + fontWeight: 600, + + borderRadius: "0.5rem", + display: "flex", + justifyContent: "center", + alignItems: "center", + + color: theme.palette.primary.contrastText, + background: theme.palette.primary.main, +})) + +const StyledChip = styled(Chip)(({ theme }) => ({ + width: "15rem", + height: "4rem", + fontSize: "1.4rem", + fontWeight: 600, + padding: 0, + borderRadius: "2rem", + verticalAlign: "middle", + + ".MuiChip-label": { + display: "flex", + alignItems: "center", + }, + + "&.loading": { + borderRadius: "0.5rem", + backgroundColor: alpha(theme.palette.primary.main, 0.6), + color: theme.palette.primary.contrastText, + }, +})) + +const ActiveButton = props => { + const { tx, type } = props + const { chainId } = useRainbowContext() + const [activeButtonLabel, setActiveButtonLabel] = useState(type) + const { replayMessage, loading: retryLoading } = useRetry({ hash: tx.hash }) + const { relayMessageWithProof, loading: claimLoading } = useClaim({ tx }) + const { estimatedTimeMap } = useTxStore() + + const actionMap = { + Claim: { + label: "Claim", + onClick: relayMessageWithProof, + tooltip: "Please connect to the L1 network to claim.", + }, + Retry: { + label: "Retry", + onClick: replayMessage, + tooltip: "Please connect to the L1 network to retry your deposit.", + }, + } + + const handleSwitchNetwork = async (chainId: number) => { + try { + // cancel switch network in MetaMask would not throw error and the result is null just like successfully switched + await switchNetwork(chainId) + } catch (error) { + // when there is a switch-network popover in MetaMask and refreshing page would throw an error + } + } + + // The estimated retry / claim time will not exceed 30 mins. + if (claimLoading || retryLoading || estimatedTimeMap[`progress_${tx.hash}`] > Date.now()) { + return ( + + {type}ing + + + } + > + ) + } + + const isOnScrollLayer1 = chainId === CHAIN_ID.L1 + + if (isOnScrollLayer1) { + return ( + + {type} + + ) + } + return ( + + + setActiveButtonLabel("Switch")} + onMouseLeave={() => setActiveButtonLabel(type)} + onClick={() => handleSwitchNetwork(CHAIN_ID.L1)} + > + {activeButtonLabel} + + + + ) +} + +export default ActiveButton diff --git a/src/pages/bridge/components/TxTable/TxStatusButton.tsx b/src/pages/bridge/components/TxTable/TxStatusButton.tsx index e1fe85478..f08a2b4cd 100644 --- a/src/pages/bridge/components/TxTable/TxStatusButton.tsx +++ b/src/pages/bridge/components/TxTable/TxStatusButton.tsx @@ -2,14 +2,13 @@ import dayjs from "dayjs" import Countdown from "react-countdown" import { makeStyles } from "tss-react/mui" -import { ButtonBase, Chip, SvgIcon, Tooltip } from "@mui/material" +import { ButtonBase, Chip, Tooltip } from "@mui/material" -import { ReactComponent as InfoSvg } from "@/assets/svgs/bridge/info.svg" import { TX_STATUS } from "@/constants" -import useBridgeStore from "@/stores/bridgeStore" -import useClaimStore from "@/stores/claimStore" import useTxStore from "@/stores/txStore" +import ActiveButton from "./ActiveButton" + const useStyles = makeStyles()(theme => { return { chip: { @@ -71,20 +70,15 @@ const useStyles = makeStyles()(theme => { }) const TxStatus = props => { - const { toStatus, tx, fromStatus, finalizedIndex } = props + const { tx } = props const { classes, cx } = useStyles() const { estimatedTimeMap } = useTxStore() - const { changeTxType, changeWithdrawStep, changeHistoryVisible, changeTxResult } = useBridgeStore() - const { setTargetTransaction } = useClaimStore() - const renderEstimatedWaitingTime = timestamp => { - if (timestamp === 0) { - return <>Pending - } else if (timestamp) { + if (timestamp) { return } - return <>{tx.isL1 ? "Ready" : "Claimable"} in ... + return <>Pending } const renderCountDown = ({ total, hours, minutes, seconds, completed }) => { @@ -98,29 +92,33 @@ const TxStatus = props => { ) } - const moveToClaim = () => { - changeHistoryVisible(false) - changeTxResult(null) - changeTxType("Withdraw") - changeWithdrawStep("2") - setTargetTransaction(tx.hash) - } - - if (toStatus === TX_STATUS.success) { - return + if (tx.txStatus === TX_STATUS.Sent) { + if (tx.claimInfo?.claimable) { + return + } else if (tx.isL1) { + return + } else { + return ( + + + {renderEstimatedWaitingTime(tx.initiatedAt ? dayjs.unix(tx.initiatedAt).add(1, "h").valueOf() : null)} + + + ) + } } - if (tx.assumedStatus) { + if ([TX_STATUS.Dropped, TX_STATUS.SentFailed, TX_STATUS.Skipped].includes(tx.txStatus)) { return ( - + - {tx.assumedStatus} - {tx.assumedStatus === TX_STATUS.failed && ( - - )} + Failed } > @@ -128,30 +126,15 @@ const TxStatus = props => { ) } - //withdraw step2 - if (!tx.isL1 && fromStatus === TX_STATUS.success) { - // withdraw claimable - if (+tx?.claimInfo?.batch_index && tx?.claimInfo?.batch_index <= finalizedIndex) { - return ( - - Claim - - ) - } + if (tx.txStatus === TX_STATUS.Relayed) { + return + } - // withdraw not claimable - return ( - - - {renderEstimatedWaitingTime(tx.initiatedAt ? dayjs(tx.initiatedAt).add(1, "h").valueOf() : null)} - - - ) + if (tx.txStatus === TX_STATUS.RelayedReverted || tx.txStatus === TX_STATUS.FailedRelayed) { + return } - return + + return } export default TxStatus diff --git a/src/pages/bridge/components/TxTable/index.tsx b/src/pages/bridge/components/TxTable/index.tsx index 43152b8ea..1a67ccf3c 100644 --- a/src/pages/bridge/components/TxTable/index.tsx +++ b/src/pages/bridge/components/TxTable/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react" +import { useMemo } from "react" import { makeStyles } from "tss-react/mui" import { @@ -19,19 +19,18 @@ import { import Link from "@/components/Link" import { NETWORKS, TX_STATUS } from "@/constants" -import { useBrigeContext } from "@/contexts/BridgeContextProvider" -import useCheckClaimStatus from "@/hooks/useCheckClaimStatus" -import useLastFinalizedBatchIndex from "@/hooks/useLastFinalizedBatchIndex" import useTokenInfo from "@/hooks/useTokenInfo" +import useTxStore from "@/stores/txStore" import { formatDate, generateExploreLink, toTokenDisplay, truncateHash } from "@/utils" import NoData from "../NoData" import TxStatusButton from "./TxStatusButton" -const useStyles = makeStyles()(theme => { +const useStyles = makeStyles()((theme, { type }) => { return { tableContainer: { whiteSpace: "nowrap", + minHeight: "30rem", [theme.breakpoints.down("sm")]: { paddingBottom: "1.6rem", }, @@ -39,9 +38,10 @@ const useStyles = makeStyles()(theme => { tableWrapper: { boxShadow: "unset", borderRadius: "20px", + background: "transparent", }, tableMinHeight: { - minHeight: "20rem", + minHeight: type === "claim" ? "40rem" : "20rem", overflowX: "auto", }, tableTitle: { @@ -68,7 +68,7 @@ const useStyles = makeStyles()(theme => { }, tableBody: { ".MuiTableCell-root": { - padding: "2rem 1.6rem", + padding: type === "claim" ? "1.6rem 0.8rem" : "2rem 1.6rem", "*": { fontSize: "1.4rem", }, @@ -144,9 +144,8 @@ const useStyles = makeStyles()(theme => { }) const TxTable = (props: any) => { - const { data, loading, pagination } = props - const { classes } = useStyles() - const { lastFinalizedBatchIndex } = useLastFinalizedBatchIndex() + const { data, loading, pagination, type } = props + const { classes } = useStyles({ type }) const handleChangePage = (e, newPage) => { pagination?.onChange?.(newPage) @@ -163,17 +162,15 @@ const TxTable = (props: any) => { Status Amount - Action - - Initiated At - + {type !== "claim" && Action} + Initiated At Transaction Hash <> {data.map((tx: any) => ( - + ))} @@ -181,7 +178,7 @@ const TxTable = (props: any) => {
{pagination && ( -
+
{ } const TxRow = props => { - const { tx, finalizedIndex } = props + const { tx, type } = props - const { blockNumbers } = useBrigeContext() - - const txStatus = useCallback( - (blockNumber, assumedStatus, isL1, to) => { - if (assumedStatus && !to) { - return assumedStatus - } - if (assumedStatus && to) { - return TX_STATUS.empty - } - - if (blockNumber && blockNumbers) { - if (isL1) { - if ((!to && blockNumbers[0] >= blockNumber) || (to && blockNumbers[1] >= blockNumber)) { - return TX_STATUS.success - } - } else { - if ((!to && blockNumbers[1] >= blockNumber) || to) { - return TX_STATUS.success - } - } - } - return TX_STATUS.pending - }, - [blockNumbers], - ) - - const { claimTip } = useCheckClaimStatus(tx) + const { estimatedTimeMap } = useTxStore() const toTip = useMemo(() => { - if (tx.assumedStatus) { + if ([TX_STATUS.Dropped, TX_STATUS.FailedRelayed, TX_STATUS.SentFailed, TX_STATUS.Skipped].includes(tx.txStatus)) { return "-" - } else if (tx.isL1) { - return "Pending..." } - return claimTip - }, [tx, claimTip]) - const fromStatus = useMemo(() => { - return txStatus(tx.fromBlockNumber, tx.assumedStatus, tx.isL1, false) - }, [tx, txStatus]) + if (!tx.isL1) { + if (estimatedTimeMap[`progress_${tx.hash}`] > Date.now()) { + return "Claiming in progress..." + } + + if (tx.txStatus === TX_STATUS.Sent && tx.claimInfo?.claimable) { + return "Ready to be claimed" + } + } - const toStatus = useMemo(() => { - return txStatus(tx.toBlockNumber, tx.assumedStatus, tx.isL1, true) - }, [tx, txStatus]) + return "Pending..." + }, [tx, estimatedTimeMap]) const { loading: tokenInfoLoading, tokenInfo } = useTokenInfo(tx.symbolToken, tx.isL1) @@ -286,7 +257,7 @@ const TxRow = props => { - + @@ -297,12 +268,14 @@ const TxRow = props => { - - {actionText(tx)} - + {type !== "claim" && ( + + {actionText(tx)} + + )} - {tx.initiatedAt ? formatDate(tx.initiatedAt, { withTime: true }) : "-"} + {tx.initiatedAt ? formatDate(tx.initiatedAt, { withTime: true, isUnix: true }) : "-"} @@ -316,12 +289,12 @@ const TxRow = props => { href={generateExploreLink(NETWORKS[+!tx.isL1].explorer, tx.hash)} className="leading-normal flex-1" > - {truncateHash(tx.hash)} + {truncateHash(tx.replayTxHash || tx.hash)} - + {tx.isL1 ? "Scroll" : "Ethereum"}:{" "} {tx.toHash ? ( diff --git a/src/pages/career/Perks/index.tsx b/src/pages/career/Perks/index.tsx index abb2b7a6c..0c62b9e60 100644 --- a/src/pages/career/Perks/index.tsx +++ b/src/pages/career/Perks/index.tsx @@ -106,7 +106,7 @@ const Perks = () => { Perks & benefits {PERKS.map((item, index) => ( - + {item.title} diff --git a/src/pages/ecosystem/Protocols/Category.tsx b/src/pages/ecosystem/Protocols/Category.tsx index 23ed44458..00113f2a2 100644 --- a/src/pages/ecosystem/Protocols/Category.tsx +++ b/src/pages/ecosystem/Protocols/Category.tsx @@ -70,7 +70,7 @@ const Category = props => { return ( {allCategories.current.map(item => ( - onChange(item)}> + onChange(item)}> {item} ))} diff --git a/src/pages/ecosystem/Protocols/ProtocolList/ProtocolCard.tsx b/src/pages/ecosystem/Protocols/ProtocolList/ProtocolCard.tsx index 30f80e4e7..8c4379918 100644 --- a/src/pages/ecosystem/Protocols/ProtocolList/ProtocolCard.tsx +++ b/src/pages/ecosystem/Protocols/ProtocolList/ProtocolCard.tsx @@ -1,6 +1,5 @@ -import { useState } from "react" +import { useRef, useState } from "react" import Img from "react-cool-img" -import LinesEllipsis from "react-lines-ellipsis" import { makeStyles } from "tss-react/mui" import { Box, Button, Stack, SvgIcon, Typography } from "@mui/material" @@ -8,8 +7,8 @@ import { Box, Button, Stack, SvgIcon, Typography } from "@mui/material" import { ecosystemListLogoUrl } from "@/apis/ecosystem" import { ReactComponent as ArrowSvg } from "@/assets/svgs/ecosystem/arrow.svg" import { ReactComponent as TwitterSvg } from "@/assets/svgs/ecosystem/twitter.svg" +import LinesEllipsis from "@/components/LinesEllipsis" import Link from "@/components/Link" -// import RenderIfVisible from "@/components/RenderIfVisible" import TextButton from "@/components/TextButton" import { TWITTER_ORIGIN } from "@/constants" import useCheckViewport from "@/hooks/useCheckViewport" @@ -18,6 +17,7 @@ import NetworkLabel from "./NetworkLabel" const useStyles = makeStyles()(theme => ({ grid: { + marginTop: "2rem", backgroundColor: theme.palette.themeBackground.normal, padding: "2.4rem", borderRadius: "2rem", @@ -65,6 +65,12 @@ const useStyles = makeStyles()(theme => ({ height: "4.8rem", }, }, + name: { + gridArea: "name", + [theme.breakpoints.up("sm")]: { + alignSelf: "flex-end", + }, + }, desc: { gridArea: "desc", fontSize: "1.6rem", @@ -118,18 +124,28 @@ const useStyles = makeStyles()(theme => ({ })) const ProtocolCard = props => { - const { name, hash, ext, tags, desc, website, twitterHandle, networkLabel } = props - const { classes } = useStyles() + const { name, hash, ext, tags, desc, website, twitterHandle, networkLabel, onResize, className, ...restProps } = props + const { classes, cx } = useStyles() const { isMobile, isDesktop } = useCheckViewport() const [isExpended, setIsExpended] = useState(false) + const cardRef = useRef() + const handleClickMore = () => { setIsExpended(true) } + const handleReflow = value => { + // don't trigger measure when the height exceeds the standard height by default + const standardHeight = isDesktop ? 156 : isMobile ? 324 : 196 + if (!isExpended && cardRef.current!.clientHeight > standardHeight) { + return + } + onResize() + } return ( - + { > {name} - + {name} { maxLine={isExpended ? 100 : isMobile ? 4 : 2} ellipsis={ <> - {` ... `} +  ...  More } basedOn="words" + onReflow={handleReflow} /> {!isDesktop && ( diff --git a/src/pages/ecosystem/Protocols/ProtocolList/index.tsx b/src/pages/ecosystem/Protocols/ProtocolList/index.tsx index d2ceab552..7d6b3de21 100644 --- a/src/pages/ecosystem/Protocols/ProtocolList/index.tsx +++ b/src/pages/ecosystem/Protocols/ProtocolList/index.tsx @@ -1,22 +1,33 @@ import { useEffect, useMemo, useState } from "react" import { usePrevious } from "react-use" +import { CellMeasurer, CellMeasurerCache, List, WindowScroller } from "react-virtualized" +import "react-virtualized/styles.css" import { makeStyles } from "tss-react/mui" import { Box } from "@mui/material" +import useScrollTrigger from "@mui/material/useScrollTrigger" +import { keyframes } from "@mui/system" import { ecosystemListUrl } from "@/apis/ecosystem" import Link from "@/components/Link" import LoadingButton from "@/components/LoadingButton" import LoadingPage from "@/components/LoadingPage" -import SuccessionToView, { SuccessionItem } from "@/components/Motion/SuccessionToView" import { ECOSYSTEM_NETWORK_LIST } from "@/constants" -import useCheckViewport from "@/hooks/useCheckViewport" import { isAboveScreen } from "@/utils/dom" import Error from "./Error" import NoData from "./NoData" import ProtocolCard from "./ProtocolCard" +const Fade = keyframes` + to {opacity:1;transform: translateY(0);} +` + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 156, +}) + const useStyles = makeStyles()(theme => ({ listRoot: { gridRow: "2 / 3", @@ -26,10 +37,11 @@ const useStyles = makeStyles()(theme => ({ gridRow: "3 / 4", gridColumn: "1 / 3", }, - - "& > *:nth-of-type(n+2)": { - marginTop: "2rem", - }, + }, + listItem: { + opacity: 0, + transform: "translateY(20px)", + animation: `${Fade} 0.2s forwards`, }, })) @@ -38,9 +50,8 @@ const ProtocolList = props => { searchParams: { category, network, keyword, page }, onAddPage, } = props - const { classes } = useStyles() - const { isMobile } = useCheckViewport() - + const { classes, cx } = useStyles() + const isScrollDown = useScrollTrigger() const [loading, setLoading] = useState(false) const prePage = usePrevious(page) const [ecosystemList, setEcosystemList] = useState([]) @@ -96,6 +107,19 @@ const ProtocolList = props => { fetchEcosystemList(queryStr) } + const rowRenderer = ({ index, isVisible, key, style, parent }) => { + const uniqueKey = (ecosystemList[index] as any).name + return ( + + {({ measure, registerChild }) => ( +
+ +
+ )} +
+ ) + } + const renderList = () => { if (loading && !ecosystemList.length) { return @@ -122,13 +146,25 @@ const ProtocolList = props => { } return ( <> - - {ecosystemList?.map((item: any) => ( - - - - ))} - + + {({ height, isScrolling, onChildScroll, scrollTop }) => ( + + )} + {hasMore && ( diff --git a/src/pages/ourStory/Initail/InlineAvatar.tsx b/src/pages/ourStory/Initail/InlineAvatar.tsx index c9b1dd5fa..61388efe4 100644 --- a/src/pages/ourStory/Initail/InlineAvatar.tsx +++ b/src/pages/ourStory/Initail/InlineAvatar.tsx @@ -25,8 +25,8 @@ const InlineAvater = props => { const { fontSize, size = "middle", ...restProps } = props const { classes } = useStyles({ size }) return ( - - + + ) } diff --git a/src/pages/ourStory/TechPrinciple/index.tsx b/src/pages/ourStory/TechPrinciple/index.tsx index 5b4100539..4336b9116 100644 --- a/src/pages/ourStory/TechPrinciple/index.tsx +++ b/src/pages/ourStory/TechPrinciple/index.tsx @@ -92,7 +92,7 @@ const TechPrinciple = () => { > {PRINCIPLES.map((item, index) => ( - + { const { chainId, onReadd } = props @@ -27,7 +33,9 @@ const AddNetworkButton = props => { return Add to {walletName} } -const Typography = styled(MuiTypography)(({ theme, bold, primary }) => ({ +const Typography = styled(MuiTypography, { + shouldForwardProp: prop => prop !== "bold" && prop !== "primary", +})(({ theme, bold, primary }) => ({ fontWeight: bold ? 600 : 400, color: primary ? theme.palette.primary.main : theme.palette.text.primary, })) diff --git a/src/pages/rollup/batch/index.tsx b/src/pages/rollup/batch/index.tsx index 10b281b9e..ab8c8cb9d 100644 --- a/src/pages/rollup/batch/index.tsx +++ b/src/pages/rollup/batch/index.tsx @@ -65,7 +65,7 @@ const BoxItem = styled(Box)(({ theme }) => ({ [theme.breakpoints.down("md")]: { height: "7.4rem", justifyContent: "space-between", - "& > *:nth-last-child(1)": { + "& > *:nth-last-of-type(1)": { textAlign: "right", marginLeft: "0.4rem", marginRight: "1.6rem", diff --git a/src/pages/rollup/chunk/detail.tsx b/src/pages/rollup/chunk/detail.tsx index ccfa25c80..4a7aad898 100644 --- a/src/pages/rollup/chunk/detail.tsx +++ b/src/pages/rollup/chunk/detail.tsx @@ -35,7 +35,7 @@ const BoxItem = styled(Box)(({ theme }) => ({ [theme.breakpoints.down("md")]: { height: "7.4rem", justifyContent: "space-between", - "& > *:nth-last-child(1)": { + "& > *:nth-last-of-type(1)": { textAlign: "right", marginLeft: "0.4rem", marginRight: "1.6rem", diff --git a/src/stores/claimStore.ts b/src/stores/claimStore.ts index 7d6418721..3e72b3c4a 100644 --- a/src/stores/claimStore.ts +++ b/src/stores/claimStore.ts @@ -1,168 +1,73 @@ -import { readItem } from "squirrel-gill/lib/storage" +// @ts-ignore import { create } from "zustand" import { persist } from "zustand/middleware" -import { fetchTxByHashUrl } from "@/apis/bridge" -import { CLAIM_TRANSACTIONS } from "@/constants/storageKey" -import { BRIDGE_TRANSACTIONS } from "@/constants/storageKey" -import { TimestampTx, TxDirection } from "@/stores/txStore" +import { CLAIM_TRANSACTIONS_V2 } from "@/constants/storageKey" +import { TX_TYPE } from "@/constants/transaction" +import useTxStore from "@/stores/txStore" -interface TxStore { - page: number - total: number - loading: boolean - claimLoading: boolean - txStatus: ClaimStatus - targetTransaction: string | null - pageTransactions: Transaction[] - orderedTxDB: TimestampTx[] - comboPageTransactions: (walletAddress, page, rowsPerPage) => Promise - generateTransactions: (transactions) => void - setTargetTransaction: (address) => void - clearTransactions: () => void -} +import { ClaimStore, fetchOnChainTransactions, formatBackTxList, updateFrontTransactions } from "./utils" -export const enum ClaimStatus { - // Batch not finalized - NOT_READY = 1, - CLAIMABLE = 2, - CLAIMING = 3, - CLAIMED = 4, - FAILED = 5, -} - -interface Transaction { - hash: string - toHash?: string - fromBlockNumber?: number - toBlockNumber?: number - amount: string - isL1: boolean - symbolToken?: string - timestamp?: number - claimInfo?: object - assumedStatus?: string - errMsg?: string - initiatedAt?: string - finalisedAt?: string - loading?: boolean -} - -const MAX_OFFSET_TIME = 30 * 60 * 1000 - -export const isValidOffsetTime = offsetTime => offsetTime < MAX_OFFSET_TIME - -const formatTxList = async backList => { - if (!backList.length) { - return { txList: [] } - } - - const txList = backList.map(tx => { - const amount = tx.amount - const toHash = tx.finalizeTx?.hash - const initiatedAt = tx.blockTimestamp || tx.createdTime - const finalisedAt = tx.finalizeTx?.blockTimestamp - - return { - hash: tx.hash, - amount, - fromBlockNumber: tx.blockNumber, - toHash, - toBlockNumber: tx.finalizeTx?.blockNumber, - isL1: tx.isL1, - symbolToken: tx.isL1 ? tx.l1Token : tx.l2Token, - claimInfo: tx.claimInfo, - initiatedAt, - finalisedAt, - } - }) - - return { - txList, - } -} - -const detailOrderdTxs = async (pageOrderedTxs, frontTransactions, abnormalTransactions) => { - const needFetchTxs = pageOrderedTxs.map(item => item.hash) - - let historyList: Transaction[] = [] - if (needFetchTxs.length) { - const { data } = await scrollRequest(fetchTxByHashUrl, { - method: "post", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ txs: needFetchTxs }), - }) - const { txList } = await formatTxList(data.result) - historyList = txList - } - const allTransactions = [...historyList, ...abnormalTransactions, ...frontTransactions] - - const pageTransactions = pageOrderedTxs - .map(({ hash, position }) => { - return allTransactions.find(item => item.hash === hash) - }) - .filter(item => item) // TODO: fot test - return { pageTransactions } -} - -const useTxStore = create()( +const useClaimStore = create()( persist( (set, get) => ({ page: 1, total: 0, loading: false, - claimLoading: false, - txStatus: 1, pageTransactions: [], - orderedTxDB: [], - targetTransaction: null, + // polling transactions - // slim frontTransactions and keep the latest 3 backTransactions - generateTransactions: async historyList => { + generateTransactions: (walletAddress, historyList) => { const { pageTransactions } = get() + const { frontTransactions, estimatedTimeMap: preEstimatedTimeMap, removeFrontTransactions } = useTxStore.getState() + const { txList: backendTransactions, estimatedTimeMap } = formatBackTxList( + historyList.filter(item => item), + preEstimatedTimeMap, + ) - const realHistoryList = historyList.filter(item => item) - - if (realHistoryList.length) { - const { txList: formattedHistoryList } = await formatTxList(realHistoryList) - const formattedHistoryListMap = Object.fromEntries(formattedHistoryList.map(item => [item.hash, item])) + const currentFrontTransactions = frontTransactions[walletAddress] ?? [] - const refreshPageTransaction = pageTransactions.map(item => { - if (formattedHistoryListMap[item.hash]) { - return formattedHistoryListMap[item.hash] - } - return item - }) + const nextFrontTransactions = updateFrontTransactions(currentFrontTransactions, backendTransactions, walletAddress, removeFrontTransactions) - set({ - pageTransactions: refreshPageTransaction, - }) - } - }, - comboPageTransactions: async (address, page, rowsPerPage) => { - const { state } = readItem(localStorage, BRIDGE_TRANSACTIONS) - const { orderedTxDB, frontTransactions, abnormalTransactions } = state + const refreshPageTransaction = pageTransactions.map(item => { + return backendTransactions.find(tx => tx.hash === item.hash) || item + }) - const orderedTxs = orderedTxDB[address] ?? [] - set({ loading: true }) - const withdrawTx = orderedTxs.filter(tx => tx.direction === TxDirection.Withdraw) - const pageOrderedTxs = withdrawTx.slice((page - 1) * rowsPerPage, page * rowsPerPage) - const { pageTransactions } = await detailOrderdTxs(pageOrderedTxs, frontTransactions, abnormalTransactions) set({ - orderedTxDB: withdrawTx, - pageTransactions, - page, - total: withdrawTx.length, - loading: false, + pageTransactions: refreshPageTransaction, }) - }, - setTargetTransaction: address => { - set({ - targetTransaction: address, + useTxStore.setState({ + estimatedTimeMap, + frontTransactions: { ...frontTransactions, [walletAddress]: nextFrontTransactions }, }) }, + + // page transactions + comboPageTransactions: async (walletAddress, page, rowsPerPage) => { + set({ loading: true }) + try { + const { results, total } = await fetchOnChainTransactions(walletAddress, page, rowsPerPage, TX_TYPE.CLAIM) + const { frontTransactions, estimatedTimeMap: preEstimatedTimeMap, removeFrontTransactions } = useTxStore.getState() + const { txList: backendTransactions, estimatedTimeMap } = formatBackTxList(results, preEstimatedTimeMap) + const currentFrontTransactions = frontTransactions[walletAddress]?.filter(tx => !tx.isL1) ?? [] + const nextFrontTransactions = updateFrontTransactions(currentFrontTransactions, backendTransactions, walletAddress, removeFrontTransactions) + const pageTransactions = page === 1 ? [...nextFrontTransactions, ...backendTransactions] : backendTransactions + set({ + pageTransactions, + page, + total, + loading: false, + }) + useTxStore.setState({ + estimatedTimeMap, + frontTransactions: { ...frontTransactions, [walletAddress]: nextFrontTransactions }, + }) + } catch (error) { + console.log(error, "error") + set({ loading: false }) + } + }, + // when connect and disconnect clearTransactions: () => { set({ pageTransactions: [], @@ -172,9 +77,8 @@ const useTxStore = create()( }, }), { - name: CLAIM_TRANSACTIONS, + name: CLAIM_TRANSACTIONS_V2, }, ), ) - -export default useTxStore +export default useClaimStore diff --git a/src/stores/txStore.ts b/src/stores/txStore.ts index d70e131a2..00852be3c 100644 --- a/src/stores/txStore.ts +++ b/src/stores/txStore.ts @@ -1,261 +1,43 @@ +// @ts-ignore import produce from "immer" -import { isNumber } from "lodash" -import { readItem } from "squirrel-gill/lib/storage" +// import _ from "lodash" import { create } from "zustand" import { persist } from "zustand/middleware" -import { fetchTxByHashUrl } from "@/apis/bridge" -import { TX_STATUS } from "@/constants" -import { BLOCK_NUMBERS, BRIDGE_TRANSACTIONS } from "@/constants/storageKey" -import { convertDateToTimestamp, sentryDebug, storageAvailable } from "@/utils" +import { BRIDGE_TRANSACTIONS_V2 } from "@/constants/storageKey" +import { TX_TYPE } from "@/constants/transaction" -interface OrderedTxDB { - [key: string]: TimestampTx[] -} - -interface TxStore { - page: number - total: number - loading: boolean - estimatedTimeMap: object - frontTransactions: Transaction[] - abnormalTransactions: Transaction[] - pageTransactions: Transaction[] - orderedTxDB: OrderedTxDB - addTransaction: (tx) => void - updateTransaction: (hash, tx) => void - removeFrontTransactions: (hash) => void - addEstimatedTimeMap: (key, value) => void - generateTransactions: (walletAddress, transactions) => void - combineClaimableTransactions: (walletAddress, transactions) => void - comboPageTransactions: (walletAddress, page, rowsPerPage) => Promise - updateOrderedTxs: (walletAddress, hash, param, direction?) => void - addAbnormalTransactions: (walletAddress, tx) => void - clearTransactions: () => void -} - -const enum ITxPosition { - // desc: have not yet been synchronized to the backend, - // status: pending - Frontend = 1, - // desc: abnormal transactions caught by the frontend, usually receipt.status !==1 - // status: failed | cancelled - Abnormal = 2, - // desc: backend data synchronized from the blockchain - // status: successful - Backend = 3, -} - -const TxPosition = { - Frontend: ITxPosition.Frontend, - Abnormal: ITxPosition.Abnormal, - Backend: ITxPosition.Backend, -} - -const enum ITxDirection { - Deposit = 1, - Withdraw = 2, -} - -const TxDirection = { - Deposit: ITxDirection.Deposit, - Withdraw: ITxDirection.Withdraw, -} - -const MAX_LIMIT = 1000 - -interface TimestampTx { - hash: string - timestamp: number - // 1: front tx - // 2: abnormal tx -> failed|cancelled - // 3: successful tx - position: ITxPosition - // 1: deposit - // 2: withdraw - direction?: ITxDirection -} -interface Transaction { - hash: string - toHash?: string - fromBlockNumber?: number - toBlockNumber?: number - amount: string - isL1: boolean - symbolToken?: string - timestamp?: number - isClaimed?: boolean - claimInfo?: object - assumedStatus?: string - errMsg?: string - initiatedAt?: string - finalisedAt?: string -} - -const MAX_OFFSET_TIME = 30 * 60 * 1000 - -const isValidOffsetTime = offsetTime => offsetTime < MAX_OFFSET_TIME - -const formatBackTxList = (backList, estimatedTimeMap) => { - const nextEstimatedTimeMap = { ...estimatedTimeMap } - const blockNumbers = readItem(localStorage, BLOCK_NUMBERS) - if (!backList.length) { - return { txList: [], estimatedTimeMap: nextEstimatedTimeMap } - } - const txList = backList.map(tx => { - const amount = tx.amount - const toHash = tx.finalizeTx?.hash - const initiatedAt = tx.blockTimestamp || tx.createdTime - const finalisedAt = tx.finalizeTx?.blockTimestamp - - // 1. have no time to compute fromEstimatedEndTime - // 2. compute toEstimatedEndTime from backend data - // 3. when tx is marked success then remove estimatedEndTime to slim storage data - // 4. estimatedTime is greater than 30 mins then warn but save - // 5. if the second deal succeeded, then the first should succeed too. - if (tx.isL1) { - if (tx.blockNumber > blockNumbers[0] && blockNumbers[0] !== -1 && !nextEstimatedTimeMap[`from_${tx.hash}`]) { - const estimatedOffsetTime = (tx.blockNumber - blockNumbers[0]) * 12 * 1000 - if (isValidOffsetTime(estimatedOffsetTime)) { - nextEstimatedTimeMap[`from_${tx.hash}`] = Date.now() + estimatedOffsetTime - } else if (!tx.finalizeTx?.blockNumber || tx.finalizeTx.blockNumber > blockNumbers[1]) { - nextEstimatedTimeMap[`from_${tx.hash}`] = 0 - sentryDebug(`safe block number: ${blockNumbers[0]}`) - } - } else if (tx.blockNumber <= blockNumbers[0] && Object.keys(nextEstimatedTimeMap).includes(`from_${tx.hash}`)) { - delete nextEstimatedTimeMap[`from_${tx.hash}`] - } - } else { - if ( - tx.finalizeTx?.blockNumber && - blockNumbers[0] !== -1 && - tx.finalizeTx.blockNumber > blockNumbers[0] && - !nextEstimatedTimeMap[`to_${toHash}`] - ) { - const estimatedOffsetTime = (tx.finalizeTx.blockNumber - blockNumbers[0]) * 12 * 1000 - if (isValidOffsetTime(estimatedOffsetTime)) { - nextEstimatedTimeMap[`to_${toHash}`] = Date.now() + estimatedOffsetTime - } else { - nextEstimatedTimeMap[`to_${toHash}`] = 0 - sentryDebug(`safe block number: ${blockNumbers[0]}`) - } - } else if ( - tx.finalizeTx?.blockNumber && - tx.finalizeTx.blockNumber <= blockNumbers[0] && - Object.keys(nextEstimatedTimeMap).includes(`to_${toHash}`) - ) { - delete nextEstimatedTimeMap[`to_${toHash}`] - } - } - - return { - hash: tx.hash, - amount, - fromBlockNumber: tx.blockNumber, - toHash, - toBlockNumber: tx.finalizeTx?.blockNumber, - isL1: tx.isL1, - symbolToken: tx.isL1 ? tx.l1Token : tx.l2Token, - claimInfo: tx.claimInfo, - isClaimed: tx.finalizeTx?.hash, - initiatedAt, - finalisedAt, - } - }) - - // delete nextEstimatedTimeMap.to_undefined - return { - txList, - estimatedTimeMap: nextEstimatedTimeMap, - } -} - -// assume > 1h tx occurred an uncatchable error -const eliminateOvertimeTx = frontList => { - return produce(frontList, draft => { - draft.forEach(item => { - if (!item.assumedStatus && Date.now() - item.timestamp >= 3600000) { - item.assumedStatus = TX_STATUS.failed - sentryDebug(`The backend has not synchronized data for this transaction(hash: ${item.hash}) for more than an hour.`) - } - }) - }) as any -} - -const detailOrderdTxs = async (pageOrderedTxs, frontTransactions, abnormalTransactions, estimatedTimeMap) => { - const needFetchTxs = pageOrderedTxs.map(item => item.hash) - - let historyList: Transaction[] = [] - let returnedEstimatedTimeMap = estimatedTimeMap - if (needFetchTxs.length) { - const { data } = await scrollRequest(fetchTxByHashUrl, { - method: "post", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ txs: needFetchTxs }), - }) - const { txList, estimatedTimeMap: nextEstimatedTimeMap } = formatBackTxList(data.result, estimatedTimeMap) - historyList = txList - returnedEstimatedTimeMap = nextEstimatedTimeMap - } - - const allTransactions = [...historyList, ...abnormalTransactions, ...frontTransactions] - const pageTransactions = pageOrderedTxs - .map(({ hash }) => { - return allTransactions.find(item => item.hash === hash) - }) - .filter(item => item) // TODO: fot test - return { pageTransactions, estimatedTimeMap: returnedEstimatedTimeMap } -} - -const maxLengthAccount = (orderedTxDB: OrderedTxDB) => { - const briefList = Object.entries(orderedTxDB).map(([key, value]) => [key, value.length]) - let maxLength = 0 - let address - for (let i = 0; i < briefList.length; i++) { - if ((briefList[i][1] as number) > maxLength) { - maxLength = briefList[i][1] as number - address = briefList[i][0] - } - } - return address -} +import { TxStore, fetchOnChainTransactions, formatBackTxList, updateFrontTransactions } from "./utils" const useTxStore = create()( persist( (set, get) => ({ page: 1, total: 0, - // { hash: estimatedEndTime } estimatedTimeMap: {}, - frontTransactions: [], - abnormalTransactions: [], + frontTransactions: {}, loading: false, - orderedTxDB: {}, pageTransactions: [], // when user send a transaction - addTransaction: newTx => - set(state => ({ - frontTransactions: [newTx, ...state.frontTransactions], - })), + addTransaction: (walletAddress, newTx) => { + const frontTransactions = get().frontTransactions + const txList = frontTransactions[walletAddress] ?? [] + txList.unshift(newTx) + set({ + frontTransactions: { ...frontTransactions, [walletAddress]: txList }, + }) + }, + // wait transaction success in from network - updateTransaction: (txHash, updateOpts) => + updateTransaction: (walletAddress, txHash, updateOpts) => set( produce(state => { - const frontTx = state.frontTransactions.find(item => item.hash === txHash) + const frontTx = (state.frontTransactions[walletAddress] ?? []).find(item => item.hash === txHash) if (frontTx) { for (const key in updateOpts) { frontTx[key] = updateOpts[key] } } - // for keep "bridge history" open - const pageTx = state.pageTransactions.find(item => item.hash === txHash) - if (pageTx) { - for (const key in updateOpts) { - pageTx[key] = updateOpts[key] - } - } }), ), @@ -267,99 +49,58 @@ const useTxStore = create()( }, // polling transactions - // slim frontTransactions and keep the latest 3 backTransactions generateTransactions: (walletAddress, historyList) => { - const { frontTransactions, estimatedTimeMap: preEstimatedTimeMap, orderedTxDB, pageTransactions } = get() - const realHistoryList = historyList.filter(item => item) - - const untimedFrontList = eliminateOvertimeTx(frontTransactions) + const { frontTransactions, estimatedTimeMap: preEstimatedTimeMap, pageTransactions } = get() + const { txList: backendTransactions, estimatedTimeMap } = formatBackTxList( + historyList.filter(item => item), + preEstimatedTimeMap, + ) - if (realHistoryList.length) { - const { txList: formattedHistoryList, estimatedTimeMap } = formatBackTxList(realHistoryList, preEstimatedTimeMap) - const formattedHistoryListHash = formattedHistoryList.map(item => item.hash) - const formattedHistoryListMap = Object.fromEntries(formattedHistoryList.map(item => [item.hash, item])) - const pendingFrontList = untimedFrontList.filter(item => !formattedHistoryListHash.includes(item.hash)) + const currentFrontTransactions = frontTransactions[walletAddress] ?? [] - const refreshPageTransaction = pageTransactions.map(item => { - if (formattedHistoryListMap[item.hash]) { - return formattedHistoryListMap[item.hash] - } - return item - }) + const nextFrontTransactions = updateFrontTransactions( + currentFrontTransactions, + backendTransactions, + walletAddress, + get().removeFrontTransactions, + ) - const failedFrontTransactionListHash = untimedFrontList.filter(item => item.assumedStatus === TX_STATUS.failed).map(item => item.hash) - const refreshOrderedDB = produce(orderedTxDB, draft => { - draft[walletAddress].forEach(item => { - if (formattedHistoryListHash.includes(item.hash)) { - item.position = TxPosition.Backend - } else if (failedFrontTransactionListHash.includes(item.hash)) { - item.position = TxPosition.Abnormal - } - }) - }) + const refreshPageTransaction = pageTransactions.map(item => { + return backendTransactions.find(tx => tx.hash === item.hash) || item + }) - set({ - frontTransactions: pendingFrontList, - pageTransactions: refreshPageTransaction, - estimatedTimeMap, - orderedTxDB: refreshOrderedDB, - }) - } else { - set({ - frontTransactions: untimedFrontList, - }) - } + set({ + frontTransactions: { ...frontTransactions, [walletAddress]: nextFrontTransactions }, + pageTransactions: refreshPageTransaction, + estimatedTimeMap, + }) }, // page transactions - comboPageTransactions: async (address, page, rowsPerPage) => { - const { orderedTxDB, frontTransactions, abnormalTransactions, estimatedTimeMap } = get() - const orderedTxs = orderedTxDB[address] ?? [] + comboPageTransactions: async (walletAddress, page, rowsPerPage) => { set({ loading: true }) - const pageOrderedTxs = orderedTxs.slice((page - 1) * rowsPerPage, page * rowsPerPage) - const { pageTransactions, estimatedTimeMap: nextEstimatedTimeMap } = await detailOrderdTxs( - pageOrderedTxs, - frontTransactions, - abnormalTransactions, - estimatedTimeMap, - ) - set({ - pageTransactions, - page, - total: orderedTxs.length, - loading: false, - estimatedTimeMap: nextEstimatedTimeMap, - }) - }, - // combine claimable transactions and sort - combineClaimableTransactions: (walletAddress, claimableList) => { - const { orderedTxDB } = get() - const orderedTxs = orderedTxDB[walletAddress] ?? [] - const claimableTxs = claimableList.map(item => ({ - hash: item.hash, - timestamp: convertDateToTimestamp(item.createdTime), - position: TxPosition.Backend, - direction: TxDirection.Withdraw, - })) - const txList = [...orderedTxs, ...claimableTxs] - .map(tx => { - // Ensure backward compatibility with old data, and add the direction attribute for transactions that haven't been claimed. - if (claimableTxs.some(claimableTx => claimableTx.hash === tx.hash)) { - return { - ...tx, - direction: TxDirection.Withdraw, - } - } - return tx + try { + const { results, total } = await fetchOnChainTransactions(walletAddress, page, rowsPerPage, TX_TYPE.ALL) + const { txList: backendTransactions, estimatedTimeMap } = formatBackTxList(results, get().estimatedTimeMap) + const currentFrontTransactions = get().frontTransactions[walletAddress] ?? [] + const nextFrontTransactions = updateFrontTransactions( + currentFrontTransactions, + backendTransactions, + walletAddress, + get().removeFrontTransactions, + ) + const pageTransactions = page === 1 ? [...nextFrontTransactions, ...backendTransactions] : backendTransactions + set({ + frontTransactions: { ...get().frontTransactions, [walletAddress]: nextFrontTransactions }, + pageTransactions, + page, + total, + loading: false, + estimatedTimeMap, }) - .filter((v, i, a) => a.findIndex(t => t.hash === v.hash) === i) - txList.sort((a, b) => { - return b.timestamp - a.timestamp - }) - set({ - orderedTxDB: { ...orderedTxDB, [walletAddress]: txList }, - total: txList.length, - }) + } catch (error) { + set({ loading: false }) + } }, // when connect and disconnect clearTransactions: () => { @@ -369,76 +110,21 @@ const useTxStore = create()( total: 0, }) }, - removeFrontTransactions: hash => - set( - produce(state => { - const frontTxIndex = state.frontTransactions.findIndex(item => item.hash === hash) - state.frontTransactions.splice(frontTxIndex, 1) - }), - ), - addAbnormalTransactions: (walletAddress, tx) => { - const { abnormalTransactions, orderedTxDB } = get() - const orderedTxs = orderedTxDB[walletAddress] ?? [] - if (storageAvailable("localStorage")) { - set({ - abnormalTransactions: [tx, ...abnormalTransactions], - }) - } else { - const abandonedTxHashs = abnormalTransactions.slice(abnormalTransactions.length - 3).map(item => item.hash) - set({ - orderedTxDB: { ...orderedTxDB, [walletAddress]: orderedTxs.filter(item => !abandonedTxHashs.includes(item.hash)) }, - abnormalTransactions: [tx, ...abnormalTransactions.slice(0, abnormalTransactions.length - 3)], - }) - } - }, - - updateOrderedTxs: (walletAddress, hash, param, direction?) => + removeFrontTransactions: (walletAddress, hash) => set( produce(state => { - // position: 1|2|3 - if (isNumber(param)) { - const current = state.orderedTxDB[walletAddress]?.find(item => item.hash === hash) - if (current) { - current.position = param - current.direction = direction - } else if ( - storageAvailable("localStorage") && - (!state.orderedTxDB[walletAddress] || state.orderedTxDB[walletAddress].length < MAX_LIMIT) - ) { - const newRecord = { hash, timestamp: Date.now(), position: param, direction } - if (state.orderedTxDB[walletAddress]) { - state.orderedTxDB[walletAddress].unshift(newRecord) - } else { - state.orderedTxDB[walletAddress] = [newRecord] - } - } else { - // remove the oldest 3 records - const address = maxLengthAccount(state.orderedTxDB) - const abandonedTxHashs = state.orderedTxDB[address].slice(state.orderedTxDB[address].length - 3).map(item => item.hash) - state.abnormalTransactions = state.abnormalTransactions.filter(item => !abandonedTxHashs.includes(item.hash)) - state.orderedTxDB[address] = state.orderedTxDB[address].slice(0, state.orderedTxDB[address].length - 3) - - const newRecord = { hash, timestamp: Date.now(), position: param, direction } - if (state.orderedTxDB[walletAddress]) { - state.orderedTxDB[walletAddress].unshift(newRecord) - } else { - state.orderedTxDB[walletAddress] = [newRecord] - } - } - } - // repriced tx - else { - state.orderedTxDB[walletAddress].find(item => item.hash === hash).hash = param + const frontTransactions = state.frontTransactions[walletAddress] + if (frontTransactions) { + const frontTxIndex = frontTransactions.findIndex(item => item.hash === hash) + frontTransactions.splice(frontTxIndex, 1) } }), ), }), { - name: BRIDGE_TRANSACTIONS, + name: BRIDGE_TRANSACTIONS_V2, }, ), ) -export { isValidOffsetTime, TxPosition, TxDirection, TimestampTx } - export default useTxStore diff --git a/src/stores/utils.ts b/src/stores/utils.ts new file mode 100644 index 000000000..0f5334a05 --- /dev/null +++ b/src/stores/utils.ts @@ -0,0 +1,134 @@ +// @ts-ignore +import { readItem } from "squirrel-gill/lib/storage" + +import { fetchClaimableTxListUrl, fetchTxListUrl, fetchWithdrawalListUrl } from "@/apis/bridge" +import { BLOCK_NUMBERS } from "@/constants/storageKey" +import { TX_TYPE } from "@/constants/transaction" +import { sentryDebug } from "@/utils" + +export interface FrontendTxDB { + [key: string]: Transaction[] +} + +export interface TxStore { + page: number + total: number + loading: boolean + estimatedTimeMap: object + frontTransactions: FrontendTxDB + pageTransactions: Transaction[] + addTransaction: (walletAddress, tx) => void + updateTransaction: (walletAddress, hash, tx) => void + removeFrontTransactions: (walletAddress, hash?) => void + addEstimatedTimeMap: (key, value) => void + generateTransactions: (walletAddress, transactions) => void + comboPageTransactions: (walletAddress, page, rowsPerPage) => Promise + clearTransactions: () => void +} + +export interface ClaimStore { + page: number + total: number + loading: boolean + pageTransactions: Transaction[] + generateTransactions: (walletAddress, transactions) => void + comboPageTransactions: (walletAddress, page, rowsPerPage) => Promise + clearTransactions: () => void +} + +export interface Transaction { + hash: string + toHash?: string + fromBlockNumber?: number + toBlockNumber?: number + amount: string + isL1: boolean + symbolToken?: string + timestamp?: number + claimInfo?: object + errMsg?: string + initiatedAt?: string + txStatus?: number + msgHash?: string + replayTxHash?: string +} + +export const MAX_OFFSET_TIME = 30 * 60 * 1000 +export const CLAIM_OFFSET_TIME = 60 * 1000 + +export const isValidOffsetTime = offsetTime => offsetTime < MAX_OFFSET_TIME + +export const formatBackTxList = (backList, estimatedTimeMap) => { + const nextEstimatedTimeMap = { ...estimatedTimeMap } + const blockNumbers = readItem(localStorage, BLOCK_NUMBERS) + if (!backList || !backList.length) { + return { txList: [], estimatedTimeMap: nextEstimatedTimeMap } + } + const txList = backList.map(tx => { + const amount = tx.token_amounts[0] + const toHash = tx.counterpart_chain_tx?.hash + const initiatedAt = tx.block_timestamp + + // 1. have no time to compute fromEstimatedEndTime + // 2. compute toEstimatedEndTime from backend data + // 3. when tx is marked success then remove estimatedEndTime to slim storage data + // 4. estimatedTime is greater than 30 mins then warn but save + // 5. if the second deal succeeded, then the first should succeed too. + + // deposit + if (tx.message_type === 1) { + if (tx.block_number > blockNumbers[0] && blockNumbers[0] !== -1 && !nextEstimatedTimeMap[`from_${tx.hash}`]) { + const estimatedOffsetTime = (tx.block_number - blockNumbers[0]) * 12 * 1000 + if (isValidOffsetTime(estimatedOffsetTime)) { + nextEstimatedTimeMap[`from_${tx.hash}`] = Date.now() + estimatedOffsetTime + } else if (!tx.counterpart_chain_tx?.block_number || tx.counterpart_chain_tx.block_number > blockNumbers[1]) { + nextEstimatedTimeMap[`from_${tx.hash}`] = 0 + sentryDebug(`safe block number: ${blockNumbers[0]}`) + } + } else if (tx.blockNumber <= blockNumbers[0] && Object.keys(nextEstimatedTimeMap).includes(`from_${tx.hash}`)) { + delete nextEstimatedTimeMap[`from_${tx.hash}`] + } + } + + return { + hash: tx.hash, + replayTxHash: tx.replay_tx_hash, + fromBlockNumber: tx.block_number, + toHash, + toBlockNumber: tx.counterpart_chain_tx?.block_number, + amount, + isL1: tx.message_type === 1, + symbolToken: tx.message_type === 1 ? tx.l1_token_address : tx.l2_token_address, + claimInfo: tx.claim_info, + initiatedAt, + txStatus: tx.tx_status, + msgHash: tx.message_hash, + } + }) + + return { + txList, + estimatedTimeMap: nextEstimatedTimeMap, + } +} + +export const fetchOnChainTransactions = async (address, page, rowsPerPage, type) => { + const requsetUrl = { + [TX_TYPE.ALL]: `${fetchTxListUrl}?address=${address}&page=${page}&page_size=${rowsPerPage}`, + [TX_TYPE.CLAIM]: `${fetchClaimableTxListUrl}?address=${address}&page=${page}&page_size=${rowsPerPage}`, + [TX_TYPE.WITHDRAW]: `${fetchWithdrawalListUrl}?address=${address}&page=${page}&page_size=${rowsPerPage}`, + } + const response = await scrollRequest(requsetUrl[type]) + return response.data +} + +export const updateFrontTransactions = (currentFrontTransactions, backendTransactions, walletAddress, removeFrontTransactions) => { + const backendHashes = new Set(backendTransactions.map(tx => tx.hash)) + return currentFrontTransactions.filter(tx => { + const isRecord = tx.initiatedAt <= backendTransactions[0]?.initiatedAt || backendHashes.has(tx.hash) + if (isRecord) { + removeFrontTransactions(walletAddress, tx.hash) + } + return !isRecord + }) +} diff --git a/src/theme/options.ts b/src/theme/options.ts index 71dce9f61..d34c40270 100644 --- a/src/theme/options.ts +++ b/src/theme/options.ts @@ -59,6 +59,8 @@ export const paletteOptions = { highlight: "#FFDEB5", optionHightlight: "#FFE6C8", tag: "#262626", + transparent: "transparent", + brand: "#FFEEDA", }, border: { main: "#000", diff --git a/src/types/index.d.ts b/src/types/index.d.ts index a87b6f90f..88a1b8b10 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -61,6 +61,8 @@ declare module "@mui/material/styles" { highlight: string optionHightlight: string tag: string + transparent: string + brand: string } link: { main: string @@ -116,6 +118,8 @@ declare module "@mui/material/styles" { highlight: string optionHightlight: string tag: string + transparent: string + brand: string } link?: { main?: string diff --git a/src/utils/common.ts b/src/utils/common.ts index 767f2d78b..44f8d89f3 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,3 +1,4 @@ +import { isHexString } from "ethers" import { isNil } from "lodash" import find from "lodash/find" import { DependencyList } from "react" @@ -87,3 +88,9 @@ export const formatLargeNumber = (value: number): string => { notation: "compact", }).format(value) } + +export function isValidTransactionHash(txHash: string): boolean { + // A valid transaction hash is a hex string of length 66 characters (including the '0x' prefix) + const isValidLength = txHash.length === 66 + return isValidLength && isHexString(txHash) +} diff --git a/src/utils/format.ts b/src/utils/format.ts index 0d66c6882..e01214564 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -43,7 +43,7 @@ export const toHexadecimal = (value: number): string => { export const toTokenDisplay = (num, decimals: bigint = BigInt(18), symbol?: string) => { // TODO: should be pure - if (_.isNil(num) || !decimals) { + if (_.isNil(num) || !decimals || num === "") { return "-" } @@ -132,9 +132,9 @@ export const formatUTCDate = (date, needSub?: boolean) => { return `${finalDate.utc().format("MMM D,YYYY h:mmA")} GMT` } -export const formatDate = (date, options: { needSub?: boolean; withTime?: boolean } = {}) => { - const { needSub, withTime } = options - let finalDate = dayjs.isDayjs(date) ? date : dayjs(date) +export const formatDate = (date, options: { needSub?: boolean; withTime?: boolean; isUnix?: boolean } = {}) => { + const { needSub, withTime, isUnix } = options + let finalDate = dayjs.isDayjs(date) ? date : isUnix ? dayjs.unix(date) : dayjs(date) if (needSub) { finalDate = date.subtract(1, "ms") } diff --git a/tsconfig.json b/tsconfig.json index 0c7e6c302..5c43d4654 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2015", + "target": "es2020", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/yarn.lock b/yarn.lock index 527334733..76095b3b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3863,10 +3863,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.42.tgz#fa39b2dc8e0eba61bdf51c66502f84e23b66e114" integrity sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg== -"@types/node@^16.11.43": - version "16.18.24" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.24.tgz#f21925dd56cd3467b4e1e0c5071d0f2af5e9a316" - integrity sha512-zvSN2Esek1aeLdKDYuntKAYjti9Z2oT4I8bfkLLhIxHlv3dwZ5vvATxOc31820iYm4hQRCwjUgDpwSMFjfTUnw== +"@types/node@^20.10.6": + version "20.10.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5" + integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw== + dependencies: + undici-types "~5.26.4" "@types/parse-json@^4.0.0": version "4.0.0" @@ -5838,9 +5840,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001481" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz#f58a717afe92f9e69d0e35ff64df596bfad93912" - integrity sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ== + version "1.0.30001574" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz" + integrity sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg== case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" @@ -6031,7 +6033,7 @@ clsx@1.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== -clsx@^1.1.0, clsx@^1.2.1: +clsx@^1.0.4, clsx@^1.1.0, clsx@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== @@ -6946,7 +6948,7 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" -dom-helpers@^5.0.1: +dom-helpers@^5.0.1, dom-helpers@^5.1.3: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== @@ -12814,12 +12816,12 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-device-detect@^1.6.2: - version "1.17.0" - resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.17.0.tgz#a00b4fd6880cebfab3fd8a42a79dc0290cdddca9" - integrity sha512-bBblIStwpHmoS281JFIVqeimcN3LhpoP5YKDWzxQdBIUP8S2xPvHDgizLDhUq2ScguLfVPmwfF5y268EEQR60w== +react-device-detect@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-2.2.3.tgz#97a7ae767cdd004e7c3578260f48cf70c036e7ca" + integrity sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw== dependencies: - ua-parser-js "^0.7.24" + ua-parser-js "^1.0.33" react-dom@^18.2.0: version "18.2.0" @@ -12875,10 +12877,10 @@ react-is@^18.0.0, react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-lines-ellipsis@^0.15.4: - version "0.15.4" - resolved "https://registry.yarnpkg.com/react-lines-ellipsis/-/react-lines-ellipsis-0.15.4.tgz#2bff3089d62a354fa4ccf630d9f755562e260386" - integrity sha512-bIcoVRulN6RdBb9QByRPan7vDjxJv4jhII9eMs5ZkCK4QYEbPiY+8g+IM7/B0kb8UbRRRgR7KwpPWI9j8d2FYg== +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== react-mailchimp-subscribe@^2.1.3: version "2.1.3" @@ -13069,6 +13071,18 @@ react-use@^17.4.0: ts-easing "^0.2.0" tslib "^2.1.0" +react-virtualized@^9.22.5: + version "9.22.5" + resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.5.tgz#bfb96fed519de378b50d8c0064b92994b3b91620" + integrity sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ== + dependencies: + "@babel/runtime" "^7.7.2" + clsx "^1.0.4" + dom-helpers "^5.1.3" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.4" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -14749,12 +14763,7 @@ typescript@^5.1.6: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== -ua-parser-js@^0.7.24: - version "0.7.35" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" - integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== - -ua-parser-js@^1.0.35: +ua-parser-js@^1.0.33, ua-parser-js@^1.0.35: version "1.0.37" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== @@ -14786,6 +14795,11 @@ uncrypto@^0.1.3: resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unenv@^1.7.4: version "1.8.0" resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.8.0.tgz#0f860d5278405700bd95d47b23bc01f3a735d68c"