Feat(3102): bridge page form (#3919)
* fix: refactor state management * fix: update pnpm lock file and use fixed version for zustand * feat: add sidebar menu and mobile menu + transaction history page * feat: add side bar menu and history page * fix: remove unused code + update TransactionClaimButton component * fix: update dockerfile to remove warning during build * feat: bridging page with erc20 bridging * fix: clean components * fix: adjust layout, state management and components * fix: refactor fee calculation + add eth to usd conversion * fix: remove unused code * feat: add fee in transaction history * fix: token image format * fix: linting issue * fix: import issue
@@ -26,7 +26,7 @@
|
||||
"@wagmi/core": "2.13.0",
|
||||
"@web3modal/wagmi": "5.0.8",
|
||||
"axios": "1.7.2",
|
||||
"classnames": "2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"compare-versions": "6.1.1",
|
||||
"date-fns": "3.6.0",
|
||||
"framer-motion": "11.3.17",
|
||||
@@ -43,6 +43,7 @@
|
||||
"react-toastify": "10.0.5",
|
||||
"sharp": "0.33.4",
|
||||
"swiper": "11.1.7",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"viem": "2.18.0",
|
||||
"wagmi": "2.12.0",
|
||||
"zustand": "4.5.4"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_221_68)">
|
||||
<path
|
||||
d="M16.4547 8.33203C16.4547 3.91375 12.873 0.332031 8.45471 0.332031C4.03643 0.332031 0.454712 3.91375 0.454712 8.33203C0.454712 12.7503 4.03643 16.332 8.45471 16.332C12.873 16.332 16.4547 12.7503 16.4547 8.33203Z"
|
||||
fill="#6CF9D8" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M3.92987 4.54185L4.70952 3.76221L7.82517 6.87786C7.63608 6.95843 7.45894 7.07584 7.30469 7.23009C7.18523 7.34955 7.08787 7.48273 7.0126 7.62457L3.92987 4.54185ZM6.95389 9.05187L3.88379 12.1219L4.66343 12.9016L7.70229 9.86275C7.55929 9.78726 7.42503 9.68934 7.30469 9.56901C7.15135 9.41567 7.03442 9.23971 6.95389 9.05187ZM9.24913 9.86111L12.246 12.858L13.0257 12.0784L9.99585 9.04854C9.91528 9.23763 9.79787 9.41476 9.64362 9.56901C9.52416 9.68847 9.39097 9.78584 9.24913 9.86111ZM9.93736 7.62768L12.9796 4.58544L12.2 3.8058L9.12648 6.87928C9.31432 6.95981 9.49028 7.07675 9.64362 7.23009C9.76395 7.35042 9.86187 7.48468 9.93736 7.62768Z"
|
||||
fill="#2D2E33" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_221_68">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.454712 0.332031)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
27
bridge-ui/public/images/logo/ethereum-rounded.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
|
||||
<circle cx="16" cy="16" r="16" fill="#627EEA"/>
|
||||
|
||||
<g fill="#FFF" fill-rule="nonzero">
|
||||
|
||||
<path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/>
|
||||
|
||||
<path d="M16.498 4L9 16.22l7.498-3.35z"/>
|
||||
|
||||
<path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/>
|
||||
|
||||
<path d="M16.498 27.995v-6.028L9 17.616z"/>
|
||||
|
||||
<path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/>
|
||||
|
||||
<path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/>
|
||||
|
||||
</g>
|
||||
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 618 B |
@@ -1,8 +0,0 @@
|
||||
<svg width="89" height="20" viewBox="0 0 89 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M27.1699 4.09375H29.4199V13.75H34.9512V15.9062H27.1699V4.09375Z" fill="white"></path>
|
||||
<path d="M44.7867 4.09375H47.0367V15.9062H44.7867V4.09375Z" fill="white"></path>
|
||||
<path d="M57.8075 4.09375H61.5575C62.3325 4.09375 63.07 4.25625 63.77 4.58125C64.47 4.89375 65.0888 5.31875 65.6263 5.85625C66.1638 6.39375 66.5888 7.01875 66.9013 7.73125C67.2138 8.44375 67.37 9.2 67.37 10C67.37 10.8 67.2138 11.5562 66.9013 12.2688C66.5888 12.9812 66.1638 13.6063 65.6263 14.1438C65.0888 14.6813 64.47 15.1125 63.77 15.4375C63.07 15.75 62.3325 15.9062 61.5575 15.9062H57.8075V4.09375ZM61.5575 13.75C62.02 13.75 62.4638 13.65 62.8888 13.45C63.3263 13.25 63.7075 12.9812 64.0325 12.6438C64.37 12.3062 64.6325 11.9125 64.82 11.4625C65.02 11 65.12 10.5125 65.12 10C65.12 9.4875 65.02 9.00625 64.82 8.55625C64.6325 8.09375 64.37 7.69375 64.0325 7.35625C63.7075 7.01875 63.3263 6.75 62.8888 6.55C62.4638 6.35 62.02 6.25 61.5575 6.25H60.0575V13.75H61.5575Z" fill="white"></path>
|
||||
<path d="M82.5559 16C81.7809 16 81.0434 15.85 80.3434 15.55C79.6559 15.2375 79.0496 14.8125 78.5246 14.275C78.0121 13.725 77.5997 13.0875 77.2872 12.3625C76.9872 11.6375 76.8372 10.85 76.8372 10C76.8372 9.1625 76.9872 8.38125 77.2872 7.65625C77.5997 6.91875 78.0121 6.28125 78.5246 5.74375C79.0496 5.19375 79.6559 4.76875 80.3434 4.46875C81.0434 4.15625 81.7809 4 82.5559 4C83.3309 4 84.0622 4.15625 84.7497 4.46875C85.4497 4.76875 86.0559 5.19375 86.5684 5.74375C87.0934 6.28125 87.5059 6.91875 87.8059 7.65625C88.1184 8.38125 88.2746 9.1625 88.2746 10C88.2746 10.85 88.1184 11.6375 87.8059 12.3625C87.5059 13.0875 87.0934 13.725 86.5684 14.275C86.0559 14.8125 85.4497 15.2375 84.7497 15.55C84.0622 15.85 83.3309 16 82.5559 16ZM82.5559 13.8438C83.0184 13.8438 83.4622 13.75 83.8871 13.5625C84.3121 13.375 84.6809 13.1125 84.9934 12.775C85.3059 12.425 85.5559 12.0188 85.7434 11.5563C85.9309 11.0812 86.0247 10.5625 86.0247 10C86.0247 9.4375 85.9309 8.925 85.7434 8.4625C85.5559 7.9875 85.3059 7.58125 84.9934 7.24375C84.6809 6.89375 84.3121 6.625 83.8871 6.4375C83.4622 6.25 83.0184 6.15625 82.5559 6.15625C82.0809 6.15625 81.6309 6.25 81.2059 6.4375C80.7934 6.625 80.4309 6.89375 80.1184 7.24375C79.8059 7.58125 79.5559 7.9875 79.3684 8.4625C79.1809 8.925 79.0872 9.4375 79.0872 10C79.0872 10.5625 79.1809 11.0812 79.3684 11.5563C79.5559 12.0188 79.8059 12.425 80.1184 12.775C80.4309 13.1125 80.7934 13.375 81.2059 13.5625C81.6309 13.75 82.0809 13.8438 82.5559 13.8438Z" fill="white"></path>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00529 0L11.954 7.74771L7.00502 10.6324L2.05668 7.74763L7.00529 0ZM3.57156 7.38126L7.00529 2.00535L10.439 7.38128L7.00506 9.3829L3.57156 7.38126Z" fill="white"></path>
|
||||
<path d="M6.99796 12.3353L1.25737 8.98872L1.1006 9.23415C-0.667488 12.0023 -0.272613 15.6276 2.04997 17.9502C4.78308 20.6833 9.21433 20.6833 11.9474 17.9502C14.27 15.6276 14.6649 12.0023 12.8968 9.23415L12.74 8.98865L6.9982 12.3355L6.99796 12.3353Z" fill="white"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="111" height="32" viewBox="0 0 111 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M29.203 31.3185H24.6514V9.1693H29.203V31.3185Z" fill="white"/>
|
||||
<path d="M45.6496 8.7345C48.1623 8.7345 50.2149 9.61552 51.8054 11.3776C53.3935 13.1396 54.1899 15.38 54.1899 18.0963V31.3162H49.6383V18.7896C49.6383 17.0848 49.0891 15.6752 47.9906 14.563C46.8922 13.4509 45.5352 12.8948 43.9173 12.8948C42.0958 12.8948 40.6083 13.4509 39.4527 14.563C38.2971 15.6752 37.7181 17.0848 37.7181 18.7896V31.3162H33.1665V9.16929H37.7181V14.8902C38.3245 12.984 39.3291 11.4806 40.7319 10.3821C42.1324 9.28371 43.7732 8.7345 45.6519 8.7345H45.6496Z" fill="white"/>
|
||||
<path d="M68.3801 8.7345C71.934 8.7345 74.8242 10.0068 77.0485 12.5492C79.2728 15.0939 80.1836 18.1695 79.7786 21.7828H61.444C61.7621 23.5747 62.5859 25.0278 63.9155 26.1399C65.245 27.2521 66.8469 27.8082 68.7257 27.8082C70.1696 27.8082 71.4626 27.4535 72.6045 26.7463C73.7464 26.0392 74.6343 25.0781 75.2704 23.863L79.0852 25.5541C78.1035 27.4329 76.7007 28.934 74.8814 30.0622C73.0622 31.1904 70.966 31.7533 68.5952 31.7533C65.2725 31.7533 62.483 30.6549 60.2289 28.4603C57.9749 26.2658 56.8467 23.5335 56.8467 20.2679C56.8467 17.0024 57.952 14.2655 60.1625 12.0527C62.3731 9.84207 65.11 8.73679 68.3778 8.73679L68.3801 8.7345ZM68.3801 12.6797C66.7027 12.6797 65.2519 13.1923 64.023 14.2197C62.7942 15.2449 61.9772 16.5814 61.5745 18.229H75.1858C74.8105 16.5814 74.0073 15.2449 72.7807 14.2197C71.5518 13.1946 70.085 12.6797 68.3801 12.6797Z" fill="white"/>
|
||||
<path d="M92.3625 8.7345C95.0216 8.7345 97.1887 9.42788 98.8637 10.8146C100.539 12.2014 101.379 14.1969 101.379 16.7965V31.3185H96.8271V25.4237C95.4678 29.6434 92.7378 31.7533 88.6347 31.7533C86.7262 31.7533 85.138 31.1835 83.8657 30.0416C82.5934 28.8997 81.9595 27.4054 81.9595 25.5564C81.9595 23.3893 82.7238 21.7623 84.257 20.6798C85.7879 19.5974 87.7399 18.9247 90.1084 18.6638L94.3991 18.229C96.017 18.0848 96.8271 17.3617 96.8271 16.0619C96.8271 14.9932 96.4152 14.1397 95.5914 13.5058C94.7675 12.8696 93.692 12.5515 92.3625 12.5515C91.0329 12.5515 89.8544 12.8994 88.9162 13.5927C87.9779 14.2861 87.3784 15.2976 87.1175 16.6271L82.6094 15.5424C83.0144 13.4623 84.0968 11.8078 85.8612 10.5789C87.6232 9.35236 89.7903 8.73679 92.3625 8.73679V8.7345ZM90.1084 28.0667C91.9872 28.0667 93.5753 27.4374 94.8751 26.1811C96.1749 24.9248 96.8248 23.4579 96.8248 21.7806V20.5677C96.4793 21.1467 95.6394 21.506 94.3099 21.6501L90.1061 22.1696C89.0077 22.316 88.1335 22.6387 87.4836 23.1444C86.8337 23.6502 86.5088 24.3367 86.5088 25.204C86.5088 26.0713 86.8337 26.7647 87.4836 27.2841C88.1335 27.8036 89.0077 28.0645 90.1061 28.0645L90.1084 28.0667Z" fill="white"/>
|
||||
<path d="M21.5165 31.3185H0.493164V9.1693H5.30334V27.0278H21.5165V31.3185Z" fill="white"/>
|
||||
<path d="M105.67 8.7345C108.039 8.7345 109.96 6.81348 109.96 4.44378C109.96 2.07409 108.039 0.153069 105.67 0.153069C103.3 0.153069 101.379 2.07409 101.379 4.44378C101.379 6.81348 103.3 8.7345 105.67 8.7345Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,12 +1,17 @@
|
||||
<svg width="200" height="208" viewBox="0 0 200 208" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="199.4" height="207.623" fill="#121212"/>
|
||||
<g clip-path="url(#clip0_2303_643)">
|
||||
<path d="M132.369 155.99H49.7001V68.8854H68.6148V139.109H132.369V155.981V155.99Z" fill="white"/>
|
||||
<path d="M132.369 85.7575C141.687 85.7575 149.241 78.2036 149.241 68.8855C149.241 59.5673 141.687 52.0134 132.369 52.0134C123.05 52.0134 115.497 59.5673 115.497 68.8855C115.497 78.2036 123.05 85.7575 132.369 85.7575Z" fill="white"/>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_452_1318)">
|
||||
<path d="M17.9969 0.00317383H0.00292969V17.9972H17.9969V0.00317383Z" fill="white"/>
|
||||
<mask id="mask0_452_1318" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="5" y="4" width="9" height="9">
|
||||
<path d="M13.1914 4.63232H5.4707V12.6971H13.1914V4.63232Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_452_1318)">
|
||||
<path d="M11.8827 12.6971H5.4707V5.94092H6.93779V11.3877H11.8827V12.6963V12.6971Z" fill="#121212"/>
|
||||
<path d="M11.8829 7.24962C12.6056 7.24962 13.1915 6.66374 13.1915 5.94097C13.1915 5.21823 12.6056 4.63232 11.8829 4.63232C11.1601 4.63232 10.5742 5.21823 10.5742 5.94097C10.5742 6.66374 11.1601 7.24962 11.8829 7.24962Z" fill="#121212"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2303_643">
|
||||
<rect width="99.5407" height="103.977" fill="white" transform="translate(49.7001 52.0134)"/>
|
||||
<clipPath id="clip0_452_1318">
|
||||
<rect width="18" height="18" rx="9" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 909 B |
@@ -1,12 +1,17 @@
|
||||
<svg width="200" height="208" viewBox="0 0 200 208" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="199.4" height="207.623" fill="#61DFFF"/>
|
||||
<g clip-path="url(#clip0_2318_291)">
|
||||
<path d="M132.369 155.99H49.7001V68.8854H68.6148V139.109H132.369V155.981V155.99Z" fill="#121212"/>
|
||||
<path d="M132.369 85.7575C141.687 85.7575 149.241 78.2037 149.241 68.8855C149.241 59.5673 141.687 52.0134 132.369 52.0134C123.05 52.0134 115.497 59.5673 115.497 68.8855C115.497 78.2037 123.05 85.7575 132.369 85.7575Z" fill="#121212"/>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_452_1318)">
|
||||
<path d="M17.9969 0.00317383H0.00292969V17.9972H17.9969V0.00317383Z" fill="#61DFFF"/>
|
||||
<mask id="mask0_452_1318" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="5" y="4" width="9" height="9">
|
||||
<path d="M13.1914 4.63232H5.4707V12.6971H13.1914V4.63232Z" fill="#61DFFF"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_452_1318)">
|
||||
<path d="M11.8827 12.6971H5.4707V5.94092H6.93779V11.3877H11.8827V12.6963V12.6971Z" fill="#121212"/>
|
||||
<path d="M11.8829 7.24962C12.6056 7.24962 13.1915 6.66374 13.1915 5.94097C13.1915 5.21823 12.6056 4.63232 11.8829 4.63232C11.1601 4.63232 10.5742 5.21823 10.5742 5.94097C10.5742 6.66374 11.1601 7.24962 11.8829 7.24962Z" fill="#121212"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2318_291">
|
||||
<rect width="99.5407" height="103.977" fill="white" transform="translate(49.7001 52.0134)"/>
|
||||
<clipPath id="clip0_452_1318">
|
||||
<rect width="18" height="18" rx="9" fill="#61DFFF"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 688 B After Width: | Height: | Size: 915 B |
@@ -1,9 +1,8 @@
|
||||
<svg width="215" height="45" viewBox="0 0 215 45" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="115" height="45" viewBox="0 0 115 45" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28.7521 31.3524H24.1938V9.17065H28.7521V31.3524Z" fill="white"/>
|
||||
<path d="M45.2229 8.73523C47.7392 8.73523 49.7949 9.61755 51.3877 11.3822C52.9781 13.1468 53.7757 15.3904 53.7757 18.1107V31.3501H49.2174V18.8051C49.2174 17.0978 48.6674 15.6861 47.5673 14.5723C46.4673 13.4585 45.1083 12.9016 43.488 12.9016C41.6638 12.9016 40.1742 13.4585 39.0168 14.5723C37.8595 15.6861 37.2797 17.0978 37.2797 18.8051V31.3501H32.7214V9.17066H37.2797V14.9C37.887 12.991 38.8931 11.4853 40.2979 10.3853C41.7005 9.28525 43.3437 8.73523 45.2252 8.73523H45.2229Z" fill="white"/>
|
||||
<path d="M67.9867 8.73523C71.5457 8.73523 74.4402 10.0094 76.6678 12.5556C78.8953 15.104 79.8074 18.1841 79.4018 21.8027H61.0404C61.3589 23.5972 62.184 25.0524 63.5155 26.1662C64.847 27.28 66.4512 27.8369 68.3327 27.8369C69.7788 27.8369 71.0736 27.4817 72.2172 26.7735C73.3608 26.0654 74.25 25.1028 74.8871 23.8859L78.7074 25.5795C77.7243 27.461 76.3194 28.9644 74.4975 30.0942C72.6756 31.2241 70.5763 31.7878 68.2021 31.7878C64.8745 31.7878 62.0808 30.6878 59.8235 28.49C57.5661 26.2922 56.4363 23.5559 56.4363 20.2856C56.4363 17.0153 57.5432 14.2744 59.757 12.0583C61.9708 9.84443 64.7118 8.73752 67.9844 8.73752L67.9867 8.73523ZM67.9867 12.6862C66.3068 12.6862 64.8538 13.1995 63.6232 14.2285C62.3925 15.2552 61.5744 16.5936 61.171 18.2437H74.8023C74.4264 16.5936 73.622 15.2552 72.3937 14.2285C71.163 13.2018 69.694 12.6862 67.9867 12.6862Z" fill="white"/>
|
||||
<path d="M92.0042 8.73523C94.6672 8.73523 96.8375 9.42963 98.515 10.8184C100.193 12.2072 101.034 14.2056 101.034 16.809V31.3524H96.4754V25.4489C95.1141 29.6749 92.38 31.7878 88.2709 31.7878C86.3596 31.7878 84.7692 31.2172 83.495 30.0736C82.2207 28.93 81.5859 27.4335 81.5859 25.5818C81.5859 23.4115 82.3514 21.7821 83.8868 20.6981C85.42 19.6141 87.3749 18.9403 89.7468 18.6791L94.0438 18.2437C95.6641 18.0993 96.4754 17.3751 96.4754 16.0734C96.4754 15.0031 96.0629 14.1483 95.2378 13.5135C94.4128 12.8764 93.3357 12.5579 92.0042 12.5579C90.6727 12.5579 89.4924 12.9062 88.5528 13.6006C87.6132 14.295 87.0128 15.3079 86.7515 16.6394L82.2368 15.5532C82.6424 13.47 83.7264 11.813 85.4934 10.5824C87.258 9.354 89.4283 8.73752 92.0042 8.73752V8.73523ZM89.7468 28.0959C91.6283 28.0959 93.2188 27.4656 94.5205 26.2075C95.8222 24.9493 96.4731 23.4803 96.4731 21.8004V20.5858C96.127 21.1656 95.286 21.5254 93.9545 21.6698L89.7445 22.19C88.6445 22.3367 87.7691 22.6598 87.1182 23.1663C86.4673 23.6728 86.1419 24.3603 86.1419 25.2289C86.1419 26.0975 86.4673 26.7918 87.1182 27.3121C87.7691 27.8323 88.6445 28.0936 89.7445 28.0936L89.7468 28.0959Z" fill="white"/>
|
||||
<path d="M21.0542 31.3524H0V9.17065H4.81724V27.0554H21.0542V31.3524Z" fill="white"/>
|
||||
<path d="M105.331 8.73514C107.704 8.73514 109.628 6.8113 109.628 4.43813C109.628 2.06495 107.704 0.141113 105.331 0.141113C102.958 0.141113 101.034 2.06495 101.034 4.43813C101.034 6.8113 102.958 8.73514 105.331 8.73514Z" fill="white"/>
|
||||
<path d="M132.418 19.8254C134.107 19.9731 135.457 20.5324 136.47 21.5032C137.505 22.4528 138.022 23.6769 138.022 25.1753C138.022 26.8214 137.42 28.2037 136.217 29.3222C135.035 30.4407 133.516 31 131.659 31H121.845V8.84069H130.709C132.566 8.84069 134.086 9.39995 135.268 10.5185C136.47 11.637 137.072 13.0193 137.072 14.6654C137.072 16.0161 136.65 17.1452 135.806 18.0526C134.983 18.9601 133.854 19.551 132.418 19.8254ZM130.551 11.025H124.283V18.844H130.551C131.775 18.844 132.777 18.4747 133.558 17.7361C134.339 16.9763 134.729 16.0372 134.729 14.9187C134.729 13.8213 134.339 12.9032 133.558 12.1646C132.777 11.4048 131.775 11.025 130.551 11.025ZM124.283 28.8157H131.374C132.577 28.8157 133.569 28.4359 134.35 27.6761C135.13 26.9164 135.521 25.9878 135.521 24.8904C135.521 23.7718 135.13 22.8433 134.35 22.1046C133.569 21.366 132.577 20.9967 131.374 20.9967H124.283V28.8157ZM143.279 14.982V19.2872C143.827 17.4512 144.672 16.1744 145.811 15.4568C146.972 14.7393 148.302 14.5177 149.8 14.7921V17.103C148.006 16.702 146.465 16.9763 145.178 17.926C143.912 18.8546 143.279 20.163 143.279 21.8514V31H140.905V14.982H143.279ZM151.485 10.2336C151.485 9.79038 151.633 9.42106 151.928 9.1256C152.224 8.83014 152.593 8.68241 153.036 8.68241C153.458 8.68241 153.817 8.83014 154.113 9.1256C154.429 9.42106 154.587 9.79038 154.587 10.2336C154.587 10.6556 154.429 11.0144 154.113 11.3099C153.817 11.6053 153.458 11.7531 153.036 11.7531C152.593 11.7531 152.224 11.6053 151.928 11.3099C151.633 11.0144 151.485 10.6556 151.485 10.2336ZM154.208 31H151.833V14.982H154.208V31ZM171.401 19.6354V7.57445H173.775V31H171.401V26.3465C170.852 27.866 169.976 29.0795 168.773 29.987C167.57 30.8734 166.178 31.3166 164.595 31.3166C162.421 31.3166 160.606 30.5252 159.15 28.9424C157.715 27.3595 156.997 25.3758 156.997 22.991C156.997 20.6273 157.715 18.6541 159.15 17.0713C160.606 15.4674 162.421 14.6654 164.595 14.6654C166.178 14.6654 167.57 15.1192 168.773 16.0266C169.976 16.913 170.852 18.1159 171.401 19.6354ZM165.26 29.1006C167.032 29.1006 168.499 28.5203 169.66 27.3595C170.82 26.1988 171.401 24.7426 171.401 22.991C171.401 21.2394 170.82 19.7832 169.66 18.6224C168.499 17.4617 167.032 16.8814 165.26 16.8814C163.634 16.8814 162.252 17.4723 161.113 18.6541C159.994 19.8148 159.435 21.2605 159.435 22.991C159.435 24.7215 159.994 26.1777 161.113 27.3595C162.252 28.5203 163.634 29.1006 165.26 29.1006ZM190.955 19.5721V14.982H193.329V29.3222C193.329 31.6859 192.516 33.6591 190.891 35.2419C189.287 36.8458 187.293 37.6478 184.908 37.6478C183.199 37.6478 181.679 37.2152 180.35 36.3499C179.02 35.5057 178.018 34.3767 177.343 32.9627L179.495 31.9497C180.002 33.026 180.73 33.8807 181.679 34.5138C182.629 35.147 183.705 35.4635 184.908 35.4635C186.618 35.4635 188.053 34.8726 189.214 33.6908C190.374 32.53 190.955 31.0739 190.955 29.3222V26.2199C190.406 27.7183 189.53 28.9107 188.327 29.7971C187.124 30.6834 185.731 31.1266 184.149 31.1266C181.975 31.1266 180.16 30.3458 178.704 28.7841C177.269 27.2013 176.551 25.2386 176.551 22.896C176.551 20.5535 177.269 18.6013 178.704 17.0396C180.16 15.4568 181.975 14.6654 184.149 14.6654C185.731 14.6654 187.124 15.1086 188.327 15.995C189.53 16.8814 190.406 18.0737 190.955 19.5721ZM184.845 28.9424C186.618 28.9424 188.074 28.3725 189.214 27.2329C190.374 26.0722 190.955 24.6266 190.955 22.896C190.955 21.1866 190.374 19.7621 189.214 18.6224C188.074 17.4617 186.618 16.8814 184.845 16.8814C183.199 16.8814 181.806 17.4617 180.666 18.6224C179.548 19.7621 178.989 21.1866 178.989 22.896C178.989 24.6055 179.548 26.0405 180.666 27.2013C181.806 28.362 183.199 28.9424 184.845 28.9424ZM204.367 14.6654C206.836 14.6654 208.862 15.5412 210.445 17.2929C212.049 19.0234 212.756 21.1655 212.566 23.7191H198.479C198.648 25.323 199.292 26.642 200.41 27.6761C201.55 28.6891 202.932 29.1956 204.557 29.1956C205.802 29.1956 206.91 28.8896 207.881 28.2776C208.852 27.6656 209.59 26.8319 210.097 25.7767L212.123 26.6315C211.448 28.0454 210.445 29.185 209.116 30.0503C207.786 30.8945 206.256 31.3166 204.526 31.3166C202.141 31.3166 200.136 30.5252 198.511 28.9424C196.907 27.3384 196.105 25.3547 196.105 22.991C196.105 20.6273 196.896 18.6541 198.479 17.0713C200.062 15.4674 202.025 14.6654 204.367 14.6654ZM204.367 16.7864C202.911 16.7864 201.645 17.2507 200.568 18.1793C199.492 19.1078 198.817 20.3002 198.542 21.7564H210.16C209.928 20.3213 209.274 19.1395 208.198 18.2109C207.142 17.2612 205.866 16.7864 204.367 16.7864Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="24" height="19" viewBox="0 0 24 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.7443 18.3712C14.7365 18.3712 18.7835 14.3243 18.7835 9.332C18.7835 4.33979 14.7365 0.292786 9.7443 0.292786C4.75208 0.292786 0.705078 4.33979 0.705078 9.332C0.705078 14.3243 4.75208 18.3712 9.7443 18.3712Z"
|
||||
fill="#00D1FF" />
|
||||
<path
|
||||
d="M23.4186 5.6277C23.4112 5.56423 23.4028 5.49975 23.3932 5.43628C23.3647 5.24382 23.3255 5.05244 23.2759 4.86419C23.2103 4.61147 23.1257 4.36191 23.0252 4.12292C22.8592 3.72742 22.6488 3.3552 22.4003 3.01575C22.174 2.70485 21.9138 2.41722 21.6262 2.16026C21.3513 1.91387 21.0509 1.69497 20.7337 1.50991L20.6681 1.4729C20.3699 1.30265 20.0537 1.16095 19.7291 1.05097C19.3717 0.927251 18.9942 0.83948 18.6061 0.792954C18.182 0.74325 17.7538 0.74325 17.3329 0.792954C17.0759 0.822562 16.8179 0.870146 16.5662 0.936768C16.3145 1.00233 16.0661 1.08693 15.8292 1.18633C15.7626 1.21382 15.697 1.24343 15.6325 1.27304C14.6596 1.72458 13.8486 2.44471 13.2775 3.36683C13.2775 3.36683 13.2775 3.36683 13.2775 3.36789L13.2172 3.47152L13.1485 3.5889L13.1464 3.59312C12.588 4.60301 12.3617 5.74086 12.495 6.88183C12.5235 7.13776 12.5722 7.39473 12.6377 7.64746C12.7033 7.89914 12.7879 8.14762 12.8873 8.38556C13.0501 8.77577 13.2595 9.14907 13.5112 9.49484C13.7375 9.80575 13.9977 10.0934 14.2853 10.3482C14.5401 10.5777 14.8182 10.7828 15.1122 10.9605L15.1788 11.0007C15.4961 11.1857 15.8334 11.3412 16.1824 11.4607C16.5472 11.5854 16.9247 11.6711 17.3065 11.7166C17.5201 11.742 17.7358 11.7547 17.9526 11.7547C18.1693 11.7547 18.3702 11.743 18.5786 11.7187C18.8355 11.6891 19.0936 11.6405 19.3474 11.5749C19.5991 11.5093 19.8465 11.4258 20.0834 11.3253C20.15 11.2978 20.2155 11.2682 20.2801 11.2386C21.2529 10.7871 22.064 10.068 22.6329 9.14694V9.14482C22.6329 9.14482 22.635 9.14482 22.635 9.14378L22.7662 8.91958C23.3245 7.90972 23.5508 6.77188 23.4175 5.63086L23.4186 5.6277ZM22.0354 3.1363L17.2335 11.3359C16.9903 11.301 16.7492 11.2492 16.5134 11.1805L21.5469 2.5843C21.7224 2.75561 21.8853 2.94067 22.0354 3.1363ZM20.7052 1.9181C20.9029 2.04394 21.0932 2.18246 21.273 2.33474L16.1623 11.062C15.9423 10.9795 15.7277 10.8822 15.5215 10.7712L20.7052 1.9181ZM18.2846 1.12817L13.6476 9.04541C13.4985 8.81488 13.3695 8.57273 13.2606 8.32317L17.464 1.14403C17.7358 1.11654 18.0107 1.11125 18.2846 1.12817ZM12.8217 6.24948C12.8217 5.38447 13.0385 4.53638 13.4636 3.76866L13.5905 3.55294C14.0526 2.80743 14.685 2.20573 15.4379 1.7838L12.8228 6.24948H12.8217ZM12.8651 6.89561L16.0248 1.49934C16.23 1.4158 16.4425 1.34389 16.6593 1.28679C16.7735 1.25718 16.8888 1.2318 17.0051 1.2096L13.0914 7.89277C13.0533 7.78069 13.0195 7.66754 12.9899 7.55334C12.9328 7.33655 12.8926 7.11556 12.8651 6.89561ZM13.8781 9.37114L18.6812 1.17047C18.9265 1.20642 19.1665 1.25824 19.4002 1.32698L14.3667 9.92311C14.1901 9.75181 14.0272 9.56676 13.8781 9.37218V9.37114ZM14.6416 10.1706L19.7513 1.44541C19.9713 1.52684 20.1859 1.62412 20.3921 1.73621L15.2084 10.5883C15.0107 10.4635 14.8214 10.3239 14.6416 10.1716V10.1706ZM17.6311 11.3772L22.267 3.45988C22.4151 3.6883 22.5441 3.93046 22.6551 4.18215L18.4506 11.3623C18.1767 11.3888 17.9029 11.3951 17.6311 11.3772ZM19.2564 11.2196C19.1412 11.2492 19.0248 11.2756 18.9085 11.2978L22.8233 4.61251C22.8613 4.72567 22.8951 4.83883 22.9258 4.95407C22.9819 5.16981 23.0231 5.38976 23.0506 5.60971L19.8898 11.006C19.6847 11.0895 19.4722 11.1614 19.2564 11.2185V11.2196ZM22.4521 8.73666L22.3252 8.95344C22.2892 9.01159 22.249 9.06764 22.2099 9.12474C22.1729 9.17868 22.1391 9.2347 22.0999 9.28759C22.0555 9.34786 22.0069 9.40496 21.9604 9.46418C21.9244 9.50966 21.8895 9.55723 21.8525 9.60059C21.8007 9.66089 21.7457 9.71691 21.6918 9.77401C21.6548 9.81316 21.6209 9.85439 21.5828 9.89141C21.5247 9.94956 21.4633 10.0035 21.402 10.0585C21.365 10.0923 21.3301 10.1272 21.292 10.16C21.2318 10.2118 21.1673 10.2594 21.1049 10.308C21.0636 10.3398 21.0245 10.3747 20.9822 10.4053C20.9209 10.4497 20.8574 10.4899 20.795 10.5312C20.7474 10.5629 20.702 10.5957 20.6534 10.6253C20.5962 10.6602 20.5359 10.6908 20.4767 10.7247L23.0919 6.25901C23.0929 7.12402 22.8751 7.97211 22.451 8.73875L22.4521 8.73666Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -1,22 +0,0 @@
|
||||
<svg width="90" height="23" viewBox="0 0 90 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13.8782 11.6197C14.5811 10.5653 14.9326 9.37433 14.9326 8.08574C14.9326 6.91426 14.6397 5.84044 14.0735 4.8447C13.4877 3.84895 12.7068 3.06798 11.711 2.48224C10.7153 1.91604 9.62191 1.62317 8.46997 1.62317H0.152588V4.35658H8.46997C9.50475 4.35658 10.3833 4.72755 11.1253 5.44995C11.8477 6.17235 12.2187 7.05096 12.2187 8.10528C12.2187 9.15959 11.8477 10.0187 11.1253 10.7606C10.4029 11.483 9.52428 11.8539 8.46997 11.8539H0.152588V21.4014H2.886V14.5873H8.46997C8.48951 14.5873 8.52852 14.5873 8.5676 14.5873C8.60667 14.5873 8.64569 14.5873 8.66523 14.5873L12.2187 21.4209H14.9521L11.1058 14.0212C12.2382 13.4745 13.1558 12.6935 13.8782 11.6197Z"
|
||||
fill="#C4FF61" />
|
||||
<path
|
||||
d="M17.8416 10.1554V12.8888H31.7626V10.1554H17.8416ZM17.8416 18.7461L16.4164 21.4209H31.782V18.7461H17.8416ZM16.4164 1.62317L17.8416 4.35658H31.7626V1.62317H16.4164Z"
|
||||
fill="#C4FF61" />
|
||||
<path d="M48.4754 1.62312V15.8955L33.8322 0.803101V21.4404H36.5656V7.36327L51.2089 22.3385V1.62312H48.4754Z"
|
||||
fill="#C4FF61" />
|
||||
<path
|
||||
d="M58.7843 14.1188L63.6459 11.2487C64.8957 10.5068 65.8912 9.51103 66.6138 8.24192C67.3364 6.97287 67.6875 5.60615 67.6875 4.14182V1.62317H53.4738V4.35658H64.9738V4.39563C64.9738 5.33281 64.7394 6.2114 64.2711 7.03143C63.8022 7.85147 63.1577 8.49573 62.3576 8.96434L57.5544 11.7954C56.2853 12.5373 55.3091 13.533 54.5671 14.8021C53.8447 16.0712 53.4738 17.4379 53.4738 18.9022V21.4014H67.6875V18.668H56.1877C56.1877 17.7308 56.422 16.8522 56.8906 16.0517C57.3396 15.2317 57.9839 14.5873 58.7843 14.1188Z"
|
||||
fill="#C4FF61" />
|
||||
<path
|
||||
d="M89.6526 12.381C89.6526 12.4005 89.6526 12.4201 89.6526 12.4396C89.2228 17.2035 85.4354 20.9327 80.8077 21.4013C80.7686 21.4013 80.7494 21.4013 80.7103 21.4013L89.6526 12.381Z"
|
||||
fill="#C4FF61" />
|
||||
<path
|
||||
d="M84.0876 2.52124C83.8339 2.4041 83.5802 2.28696 83.3265 2.18934C82.8185 1.99409 82.2919 1.83789 81.7449 1.72075C81.0812 1.58407 80.4175 1.50598 79.7142 1.50598C79.3823 1.50598 79.0505 1.5255 78.7186 1.54503C76.415 1.77932 74.365 2.77507 72.7834 4.29797C72.6854 4.39559 72.5688 4.51274 72.4708 4.61036C70.9673 6.19184 69.991 8.28093 69.7764 10.5653C69.7571 10.8582 69.7373 11.1511 69.7373 11.4634C69.7373 12.2054 69.8155 12.9277 69.9718 13.6306C70.089 14.1578 70.2453 14.6654 70.4401 15.1731C70.5573 15.4464 70.6745 15.7197 70.7917 15.9736C71.0262 16.4226 71.2799 16.8522 71.5726 17.2621L73.2126 15.6026L83.7558 4.94227L85.3963 3.2827C85.0056 3.00936 84.5565 2.75554 84.0876 2.52124ZM72.2562 13.3573C72.2363 13.2987 72.2171 13.2206 72.2171 13.1621C72.0999 12.6154 72.0415 12.0491 72.0415 11.483C72.0415 9.51097 72.7834 7.65615 74.1306 6.23088L74.4035 5.95754C75.8487 4.57131 77.7231 3.80986 79.734 3.80986C80.2612 3.80986 80.7884 3.86843 81.2958 3.96605C81.374 3.98557 81.4521 4.00511 81.5303 4.02463L72.2562 13.3573Z"
|
||||
fill="#C4FF61" />
|
||||
<path
|
||||
d="M76.278 20.8547C76.0243 20.7571 75.7899 20.6595 75.556 20.5618C75.0679 20.3275 74.599 20.0737 74.1499 19.7614L87.9345 5.84045C88.2273 6.26999 88.5008 6.71903 88.7154 7.20717C88.8326 7.46098 88.9498 7.71479 89.0472 7.9686L76.278 20.8547Z"
|
||||
fill="#C4FF61" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
3
bridge-ui/public/images/logo/sidebar/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="16" viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.9647 1.33998C16.6309 0.713992 15.2048 0.259029 13.7139 0C13.5307 0.332088 13.3168 0.778745 13.1693 1.13408C11.5844 0.894979 10.014 0.894979 8.45825 1.13408C8.31079 0.778745 8.09197 0.332088 7.90727 0C6.41469 0.259029 4.98694 0.715652 3.65319 1.3433C0.962995 5.42134 0.233727 9.39813 0.598361 13.3184C2.38264 14.6551 4.11182 15.467 5.81182 15.9983C6.23156 15.4189 6.60591 14.8029 6.92842 14.1536C6.31421 13.9195 5.72593 13.6306 5.17006 13.2951C5.31754 13.1856 5.46177 13.071 5.60115 12.9531C8.99146 14.5438 12.675 14.5438 16.0249 12.9531C16.1658 13.071 16.3101 13.1856 16.4559 13.2951C15.8984 13.6322 15.3085 13.9211 14.6943 14.1553C15.0168 14.8029 15.3895 15.4205 15.8109 16C17.5125 15.4687 19.2433 14.6567 21.0276 13.3184C21.4555 8.77379 20.2967 4.83354 17.9647 1.33998ZM7.39028 10.9075C6.37255 10.9075 5.53794 9.95433 5.53794 8.79373C5.53794 7.63306 6.35472 6.67832 7.39028 6.67832C8.42586 6.67832 9.26043 7.63139 9.24261 8.79373C9.24427 9.95433 8.42586 10.9075 7.39028 10.9075ZM14.2357 10.9075C13.218 10.9075 12.3833 9.95433 12.3833 8.79373C12.3833 7.63306 13.2001 6.67832 14.2357 6.67832C15.2712 6.67832 16.1059 7.63139 16.088 8.79373C16.088 9.95433 15.2712 10.9075 14.2357 10.9075Z" fill="#C0C0C0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
4
bridge-ui/public/images/logo/sidebar/linea-mirror.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.0700684" width="18" height="18" rx="9" fill="#C0C0C0"/>
|
||||
<path d="M5.47961 7.70898C5.47961 5.72812 7.08542 4.12231 9.06629 4.12231C11.0471 4.12231 12.653 5.72812 12.653 7.70899V13.0249C12.653 13.3297 12.4058 13.5769 12.101 13.5769H6.03157C5.72674 13.5769 5.47961 13.3297 5.47961 13.0249V7.70898Z" fill="#121212"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 427 B |
3
bridge-ui/public/images/logo/sidebar/twitter.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.26484 16C13.6938 16 17.7583 9.84369 17.7583 4.50658C17.7583 4.33351 17.7544 4.15659 17.7467 3.98352C18.5374 3.41173 19.2197 2.70348 19.7617 1.89206C19.0253 2.21968 18.2435 2.43365 17.4429 2.52665C18.2858 2.02137 18.917 1.22761 19.2194 0.292501C18.4264 0.762466 17.5591 1.09399 16.6548 1.27285C16.0456 0.625448 15.24 0.196792 14.3626 0.0531537C13.4853 -0.0904844 12.585 0.0588966 11.8011 0.478201C11.0172 0.897505 10.3932 1.56338 10.0257 2.37287C9.65814 3.18236 9.56751 4.09039 9.76779 4.95656C8.16207 4.87598 6.59121 4.45886 5.15705 3.73224C3.72288 3.00561 2.45742 1.98571 1.44271 0.738639C0.92698 1.62782 0.769166 2.68001 1.00134 3.68136C1.23352 4.68271 1.83826 5.5581 2.69266 6.12959C2.05123 6.10923 1.42384 5.93653 0.862347 5.62577V5.67576C0.861772 6.60889 1.18436 7.51342 1.77529 8.2356C2.36621 8.95777 3.18899 9.45302 4.10377 9.63716C3.50958 9.79973 2.88595 9.82342 2.28114 9.70638C2.53927 10.5089 3.04151 11.2108 3.71775 11.7141C4.39399 12.2174 5.21049 12.497 6.05331 12.514C4.62245 13.6379 2.85493 14.2476 1.03542 14.2447C0.712742 14.2442 0.390384 14.2244 0.0700684 14.1855C1.9185 15.3713 4.06872 16.0011 6.26484 16Z" fill="#C0C0C0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
bridge-ui/public/images/logo/sidebar/youtube.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="16" viewBox="0 0 24 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.2842 3.45237C23.2842 3.45237 23.0621 1.88392 22.3778 1.19522C21.5114 0.288809 20.5428 0.284365 20.0984 0.231047C16.9171 -1.27121e-07 12.1407 0 12.1407 0H12.1318C12.1318 0 7.35532 -1.27121e-07 4.17398 0.231047C3.72966 0.284365 2.76104 0.288809 1.89462 1.19522C1.21036 1.88392 0.992644 3.45237 0.992644 3.45237C0.992644 3.45237 0.761597 5.29631 0.761597 7.1358V8.85976C0.761597 10.6992 0.9882 12.5432 0.9882 12.5432C0.9882 12.5432 1.21036 14.1116 1.89017 14.8003C2.7566 15.7067 3.89406 15.6756 4.40059 15.7734C6.2223 15.9467 12.1362 16 12.1362 16C12.1362 16 16.9171 15.9911 20.0984 15.7645C20.5428 15.7112 21.5114 15.7067 22.3778 14.8003C23.0621 14.1116 23.2842 12.5432 23.2842 12.5432C23.2842 12.5432 23.5108 10.7037 23.5108 8.85976V7.1358C23.5108 5.29631 23.2842 3.45237 23.2842 3.45237ZM9.78576 10.9525V4.55873L15.9307 7.76673L9.78576 10.9525Z" fill="#C0C0C0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 979 B |
@@ -1,7 +0,0 @@
|
||||
<svg width="132" height="369" viewBox="0 0 132 369" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M-35.8008 349.975L-35.8008 335.107L-17.1932 335.107L-17.1932 349.93L-2.32507 349.93L-2.32507 335.062L-17.1932 335.062L-17.1932 316.454L-2.32507 316.454L-2.32507 282.979L16.2827 282.979L16.2827 268.111L-2.32507 268.111L-2.32507 249.503L16.2827 249.503L16.2827 234.68L31.1506 234.68L31.1506 249.548L16.2827 249.548L16.2827 268.111L31.1506 268.111L31.1506 335.062L49.7586 335.062L49.7586 349.93L31.1506 349.93L31.1506 368.538L-17.1932 368.538L-17.1932 349.975L-35.8008 349.975Z" fill="#61DFFF"/>
|
||||
<path d="M31.1506 216.072L31.1506 201.204L49.7586 201.204L49.7586 167.728L64.6267 167.728L64.6267 201.204L49.7586 201.204L49.7586 216.072L31.1506 216.072Z" fill="#61DFFF"/>
|
||||
<path d="M64.852 134.072L64.852 148.941L83.4597 148.941L83.4597 134.072L64.852 134.072Z" fill="#61DFFF"/>
|
||||
<path d="M116.935 33.7798L116.935 48.648L131.803 48.648L131.803 33.7798L116.935 33.7798Z" fill="#61DFFF"/>
|
||||
<path d="M116.935 0.304681L116.935 15.1724L131.803 15.1724L131.803 0.30468L116.935 0.304681Z" fill="#61DFFF"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="201" height="120" viewBox="0 0 201 120" fill="none">
|
||||
<path
|
||||
d="M-15.6706 0.359619H-0.802399V18.9672H-15.6257V33.8353H-0.757118V18.9672H17.8506V33.8353H51.3258V52.4431H66.1939V33.8353H84.802V52.4431H99.6249V67.311H84.7568V52.4431H66.1939V67.311H-0.757118V85.919H-15.6257V67.311H-34.2334V18.9672H-15.6706V0.359619Z"
|
||||
fill="#61DFFF" />
|
||||
<path d="M118.233 67.311H133.1V85.919H166.577V100.787H133.1V85.919H118.233V67.311Z" fill="#61DFFF" />
|
||||
<path d="M200.233 101.012H185.364V119.62H200.233V101.012Z" fill="#61DFFF" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 577 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
Before Width: | Height: | Size: 629 B |
@@ -21,4 +21,27 @@ body {
|
||||
.btn-custom {
|
||||
@apply min-h-[2.5rem] h-[2.5rem] px-6;
|
||||
}
|
||||
|
||||
.divider:not(:empty) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.menu-horizontal > li:not(.menu-title) > details > ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:where(.menu-horizontal > li:not(.menu-title) > details > ul) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.tooltip:after {
|
||||
border-color: #505050 transparent transparent transparent;
|
||||
}
|
||||
|
||||
.tooltip:before {
|
||||
border: 1px solid #505050;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import BridgeLayout from "@/components/bridge/BridgeLayout";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div className="min-w-min max-w-lg md:m-auto md:mt-32">
|
||||
<div className="min-w-min max-w-lg md:m-auto">
|
||||
<h1 className="mb-6 text-4xl font-bold md:hidden">Bridge</h1>
|
||||
<BridgeLayout />
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { useWeb3Modal } from "@web3modal/wagmi/react";
|
||||
import { cn } from "@/utils/cn";
|
||||
import Button from "./bridge/Button";
|
||||
|
||||
export default function ConnectButton() {
|
||||
type ConnectButtonProps = {
|
||||
fullWidth?: boolean;
|
||||
};
|
||||
|
||||
export default function ConnectButton({ fullWidth }: ConnectButtonProps) {
|
||||
const { open } = useWeb3Modal();
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
id="wallet-connect-btn"
|
||||
className="btn btn-primary rounded-full text-sm font-semibold uppercase md:text-[0.9375rem]"
|
||||
variant="primary"
|
||||
size="md"
|
||||
className={cn({
|
||||
"w-full": fullWidth,
|
||||
})}
|
||||
onClick={() => open()}
|
||||
>
|
||||
Connect Wallet
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
137
bridge-ui/src/components/Dropdown.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { MdCallMade } from "react-icons/md";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
export type DropdownItem = {
|
||||
title: string;
|
||||
iconPath: string;
|
||||
onClick?: () => Promise<void>;
|
||||
externalLink?: string;
|
||||
};
|
||||
|
||||
type DropdownProps = {
|
||||
initialValue?: DropdownItem;
|
||||
outline?: boolean;
|
||||
showDropdownToggle?: boolean;
|
||||
items: DropdownItem[];
|
||||
};
|
||||
|
||||
export default function Dropdown({ items, initialValue, outline = false, showDropdownToggle = true }: DropdownProps) {
|
||||
const ref = useRef<HTMLDetailsElement>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<DropdownItem | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedItem(initialValue || null);
|
||||
}, [initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
ref.current.removeAttribute("open");
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleItemClick = async (item: DropdownItem) => {
|
||||
setSelectedItem(item);
|
||||
if (item.onClick) {
|
||||
await item.onClick();
|
||||
}
|
||||
ref.current?.removeAttribute("open");
|
||||
};
|
||||
|
||||
return (
|
||||
<details className="dropdown relative" ref={ref}>
|
||||
<summary
|
||||
className={cn("flex cursor-pointer items-center gap-2 rounded-full p-2 px-3", {
|
||||
"border-2 border-card": outline,
|
||||
"bg-[#2D2D2D] text-white": !outline,
|
||||
})}
|
||||
>
|
||||
{selectedItem ? (
|
||||
<>
|
||||
{selectedItem.iconPath && (
|
||||
<Image
|
||||
src={selectedItem.iconPath}
|
||||
alt="MetaMask"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
)}
|
||||
<span>{selectedItem.title}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>Select an option</span>
|
||||
)}
|
||||
{showDropdownToggle && (
|
||||
<svg
|
||||
className="ml-1 size-4 text-card transition-transform"
|
||||
fill="none"
|
||||
stroke={"white"}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
)}
|
||||
</summary>
|
||||
<ul className="menu dropdown-content absolute right-0 z-10 mt-2 min-w-max border-2 border-card bg-cardBg p-0 shadow">
|
||||
{items.map((item, index) => (
|
||||
<li key={`dropdown-${item}-${index}`} className="w-full">
|
||||
{item.externalLink ? (
|
||||
<Link
|
||||
href={item.externalLink}
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-md flex justify-start rounded-none border-none bg-cardBg text-white"
|
||||
>
|
||||
{item.iconPath && (
|
||||
<Image
|
||||
src={item.iconPath}
|
||||
alt="MetaMask"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
)}
|
||||
{item.title}
|
||||
<MdCallMade />
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className={cn("btn btn-md flex justify-start border-none bg-cardBg rounded-none", {
|
||||
"text-white": !outline,
|
||||
})}
|
||||
onClick={() => handleItemClick(item)}
|
||||
>
|
||||
{item.iconPath && (
|
||||
<Image
|
||||
src={item.iconPath}
|
||||
alt="MetaMask"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
)}
|
||||
{item.title}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<style>{`
|
||||
details[open] summary svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
`}</style>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
48
bridge-ui/src/components/DropdownItem.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { MdCallMade } from "react-icons/md";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
export type DropdownItemProps = {
|
||||
title: string;
|
||||
onClick?: () => void;
|
||||
iconPath?: string;
|
||||
externalLink?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function DropdownItem({ title, iconPath, onClick, externalLink, className }: DropdownItemProps) {
|
||||
if (externalLink) {
|
||||
return (
|
||||
<li key={`dropdown-item-${title}`} className="w-full">
|
||||
<Link
|
||||
href={externalLink}
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn("btn btn-md flex justify-start rounded-none border-none bg-cardBg ", className)}
|
||||
>
|
||||
{iconPath && (
|
||||
<Image src={iconPath} alt={title} width={18} height={18} style={{ width: "18px", height: "auto" }} />
|
||||
)}
|
||||
{title}
|
||||
<MdCallMade />
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={`dropdown-item-${title}`} className="w-full">
|
||||
<button
|
||||
className={cn("btn btn-md flex justify-start border-none bg-cardBg rounded-none", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{iconPath && (
|
||||
<Image src={iconPath} alt={title} width={18} height={18} style={{ width: "18px", height: "auto" }} />
|
||||
)}
|
||||
{title}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
138
bridge-ui/src/components/bridge/Amount.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ChangeEvent, useCallback, useEffect } from "react";
|
||||
import { useAccount } from "wagmi";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { parseUnits, zeroAddress } from "viem";
|
||||
import { PiApproximateEqualsBold } from "react-icons/pi";
|
||||
|
||||
import { NetworkType, TokenType } from "@/config";
|
||||
import useTokenPrices from "@/hooks/useTokenPrices";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
const AMOUNT_REGEX = "^[0-9]*[.,]?[0-9]*$";
|
||||
const MAX_AMOUNT_CHAR = 20;
|
||||
|
||||
export function Amount() {
|
||||
const { token, fromChain, tokenAddress, networkType } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
fromChain: state.fromChain,
|
||||
tokenAddress: state.token?.[state.networkLayer] || zeroAddress,
|
||||
networkType: state.networkType,
|
||||
}));
|
||||
const { address } = useAccount();
|
||||
|
||||
const { setValue, getValues, setError, clearErrors, trigger, watch } = useFormContext();
|
||||
const watchBalance = watch("balance") || "0";
|
||||
const amount = getValues("amount");
|
||||
const gasFees = getValues("gasFees") || BigInt(0);
|
||||
const minFees = getValues("minFees") || BigInt(0);
|
||||
|
||||
const { data: tokenPrices } = useTokenPrices([tokenAddress], fromChain?.id);
|
||||
|
||||
const compareAmountBalance = useCallback(
|
||||
(_amount: string) => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const amountToCompare =
|
||||
token.type === TokenType.ETH
|
||||
? parseUnits(_amount, token.decimals) + gasFees + parseUnits(minFees.toString(), 18)
|
||||
: parseUnits(_amount, token.decimals);
|
||||
|
||||
const balanceToCompare = parseUnits(watchBalance, token.decimals);
|
||||
|
||||
if (amountToCompare > balanceToCompare) {
|
||||
setError("amount", {
|
||||
type: "custom",
|
||||
message: "Not enough funds (Incl fees)",
|
||||
});
|
||||
} else {
|
||||
clearErrors("amount");
|
||||
}
|
||||
},
|
||||
[token, gasFees, minFees, clearErrors, setError, watchBalance],
|
||||
);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const { key } = event;
|
||||
|
||||
// Allow control keys, numeric keys, decimal point (if not already present), +, -, and arrow keys
|
||||
const allowedKeys = ["Backspace", "Tab", "ArrowLeft", "ArrowRight", "Delete"];
|
||||
|
||||
if (/[0-9]/.test(key) && !amount.includes(".") && amount[0] === "0") {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(
|
||||
/[0-9]/.test(key) ||
|
||||
allowedKeys.includes(key) ||
|
||||
(key === "." && !amount.includes(".")) ||
|
||||
(key === "," && !amount.includes(","))
|
||||
)
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const checkAmountHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
// Replace minus
|
||||
const amount = e.target.value.replace(/,/g, ".");
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new RegExp(AMOUNT_REGEX).test(amount) || amount === "") {
|
||||
// Limit max char
|
||||
if (amount.length > MAX_AMOUNT_CHAR) {
|
||||
setValue("amount", amount.substring(0, MAX_AMOUNT_CHAR));
|
||||
} else {
|
||||
setValue("amount", amount);
|
||||
}
|
||||
}
|
||||
compareAmountBalance(amount);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (amount) {
|
||||
trigger(["amount"]);
|
||||
compareAmountBalance(amount);
|
||||
}
|
||||
}, [amount, trigger, compareAmountBalance]);
|
||||
|
||||
// Detect when changing account
|
||||
useEffect(() => {
|
||||
setValue("amount", "");
|
||||
clearErrors("amount");
|
||||
}, [address, setValue, clearErrors]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
id="amount-input"
|
||||
type="text"
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
inputMode="decimal"
|
||||
value={amount}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={checkAmountHandler}
|
||||
pattern={AMOUNT_REGEX}
|
||||
placeholder="Enter amount"
|
||||
className="input input-md w-full border-0 bg-inherit p-0 text-right text-lg font-bold placeholder:text-right focus:border-0 focus:outline-none md:text-2xl"
|
||||
/>
|
||||
{networkType === NetworkType.MAINNET && (
|
||||
<span className="label-text flex items-center justify-end">
|
||||
<PiApproximateEqualsBold /> $
|
||||
{amount && tokenPrices?.[tokenAddress]?.usd
|
||||
? `${(Number(amount) * tokenPrices?.[tokenAddress]?.usd).toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 10,
|
||||
})}`
|
||||
: "0.00"}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,11 +4,12 @@ import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useAccount, useWaitForTransactionReceipt } from "wagmi";
|
||||
import { parseUnits } from "viem";
|
||||
import classNames from "classnames";
|
||||
import { toast } from "react-toastify";
|
||||
import { useSwitchNetwork, useAllowance, useApprove } from "@/hooks";
|
||||
import { Transaction } from "@/models";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { useTokenBalance } from "@/hooks/useTokenBalance";
|
||||
|
||||
export type BridgeForm = {
|
||||
amount: string;
|
||||
@@ -25,19 +26,25 @@ export default function Approve() {
|
||||
const watchBalance = watch("balance", false);
|
||||
|
||||
// Context
|
||||
const { token, fromChain, tokenBridgeAddress } = useChainStore((state) => ({
|
||||
const { token, fromChain, tokenBridgeAddress, tokenAddress } = useChainStore((state) => ({
|
||||
tokenAddress: state.token?.[state.networkLayer],
|
||||
token: state.token,
|
||||
fromChain: state.fromChain,
|
||||
tokenBridgeAddress: state.tokenBridgeAddress,
|
||||
}));
|
||||
|
||||
// Hooks
|
||||
const { balance } = useTokenBalance(tokenAddress, token?.decimals);
|
||||
|
||||
const { switchChain } = useSwitchNetwork(fromChain?.id);
|
||||
const { allowance, fetchAllowance } = useAllowance();
|
||||
const { allowance, refetchAllowance } = useAllowance();
|
||||
const { hash: newTxHash, setHash, writeApprove, isLoading: isApprovalLoading } = useApprove();
|
||||
|
||||
// Wagmi
|
||||
const { address } = useAccount();
|
||||
|
||||
const hasInsufficientBalance = watchAmount && balance < watchAmount;
|
||||
|
||||
const {
|
||||
isLoading: isWaitingLoading,
|
||||
isSuccess: isWaitingSuccess,
|
||||
@@ -70,13 +77,13 @@ export default function Approve() {
|
||||
|
||||
// Refresh allowance after successful tx
|
||||
useEffect(() => {
|
||||
fetchAllowance();
|
||||
refetchAllowance();
|
||||
if (isWaitingSuccess) {
|
||||
toast.success("Token approval successful!");
|
||||
setWaitingTransaction(undefined);
|
||||
setHash(null);
|
||||
}
|
||||
}, [isWaitingSuccess, fetchAllowance, setHash]);
|
||||
}, [isWaitingSuccess, refetchAllowance, setHash]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWaitingError) {
|
||||
@@ -105,30 +112,23 @@ export default function Approve() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
id="approve-btn"
|
||||
className={classNames("btn btn-primary w-48 rounded-full", {
|
||||
"btn-disabled":
|
||||
token &&
|
||||
(isApprovalLoading ||
|
||||
isWaitingLoading ||
|
||||
!watchAmount ||
|
||||
parseUnits(watchAmount, token.decimals) === BigInt(0) ||
|
||||
(allowance && parseUnits(watchAmount, token.decimals) <= allowance) ||
|
||||
parseUnits(watchAmount, token.decimals) > parseUnits(watchBalance, token.decimals)),
|
||||
})}
|
||||
type="button"
|
||||
onClick={approveHandler}
|
||||
>
|
||||
{(isApprovalLoading || isWaitingLoading) && <span className="loading loading-spinner"></span>}
|
||||
Approve
|
||||
</button>
|
||||
{/* <div className="mt-5 text-xs">
|
||||
Allowed:{" "}
|
||||
{allowance && token ? formatUnits(allowance, token.decimals) : ""}{" "}
|
||||
{token?.symbol}
|
||||
</div> */}
|
||||
</div>
|
||||
<button
|
||||
id="approve-btn"
|
||||
className={cn("btn btn-primary w-full uppercase rounded-full", {
|
||||
"btn-disabled":
|
||||
token &&
|
||||
(isApprovalLoading ||
|
||||
isWaitingLoading ||
|
||||
!watchAmount ||
|
||||
parseUnits(watchAmount, token.decimals) === BigInt(0) ||
|
||||
(allowance && parseUnits(watchAmount, token.decimals) <= allowance) ||
|
||||
parseUnits(watchAmount, token.decimals) > parseUnits(watchBalance, token.decimals)),
|
||||
})}
|
||||
type="button"
|
||||
onClick={approveHandler}
|
||||
>
|
||||
{(isApprovalLoading || isWaitingLoading) && <span className="loading loading-spinner"></span>}
|
||||
{hasInsufficientBalance ? "Insufficient balance" : "Approve"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
36
bridge-ui/src/components/bridge/Balance.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { useBlockNumber } from "wagmi";
|
||||
import { formatBalance } from "@/utils/format";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { useTokenBalance } from "@/hooks/useTokenBalance";
|
||||
|
||||
export function Balance() {
|
||||
// Context
|
||||
const { token, networkLayer } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
networkLayer: state.networkLayer,
|
||||
}));
|
||||
|
||||
const tokenAddress = token?.[networkLayer];
|
||||
// Wagmi
|
||||
const queryClient = useQueryClient();
|
||||
const { data: blockNumber } = useBlockNumber({ watch: true });
|
||||
const { balance, queryKey } = useTokenBalance(tokenAddress, token?.decimals);
|
||||
|
||||
// Form
|
||||
const { setValue } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
setValue("balance", balance);
|
||||
}, [balance, setValue, token?.decimals]);
|
||||
|
||||
useEffect(() => {
|
||||
if (blockNumber && blockNumber % 5n === 0n) {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
}
|
||||
}, [blockNumber, queryClient, queryKey]);
|
||||
|
||||
return <span className="label-text ml-1">{balance && `Balance: ${formatBalance(balance)} ${token?.symbol}`}</span>;
|
||||
}
|
||||
189
bridge-ui/src/components/bridge/Bridge.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useAccount, useWaitForTransactionReceipt } from "wagmi";
|
||||
import { BridgeExternal } from "./BridgeExternal";
|
||||
import { FromChain } from "./FromChain";
|
||||
import { Balance } from "./Balance";
|
||||
import { Amount } from "./Amount";
|
||||
import SwapIcon from "@/assets/icons/swap.svg";
|
||||
import { ToChain } from "./ToChain";
|
||||
import { ReceivedAmount } from "./ReceivedAmount";
|
||||
import { Recipient } from "./Recipient";
|
||||
import { ClaimingType } from "./form/ClaimingType";
|
||||
import { Fees } from "./fees";
|
||||
import { Submit } from "./Submit";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { BridgeForm, Transaction } from "@/models";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { NetworkLayer, TokenType } from "@/config";
|
||||
import { useBridge, useSwitchNetwork, useFetchHistory } from "@/hooks";
|
||||
import { parseEther } from "viem";
|
||||
import TokenList from "./TokenList";
|
||||
import { toast } from "react-toastify";
|
||||
import { ERC20Stepper } from "./ERC20Stepper";
|
||||
import ConnectButton from "../ConnectButton";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { useReceivedAmount } from "@/hooks/useReceivedAmount";
|
||||
import { ModalContext } from "@/contexts/modal.context";
|
||||
import TransactionConfirmationModal from "./modals/TransactionConfirmationModal";
|
||||
|
||||
const Bridge = () => {
|
||||
const [waitingTransaction, setWaitingTransaction] = useState<Transaction | undefined>();
|
||||
const { handleShow, handleClose } = useContext(ModalContext);
|
||||
const { fromChain, token, networkLayer } = useChainStore((state) => ({
|
||||
fromChain: state.fromChain,
|
||||
token: state.token,
|
||||
networkLayer: state.networkLayer,
|
||||
}));
|
||||
|
||||
const { handleSubmit, watch, reset } = useFormContext<BridgeForm>();
|
||||
|
||||
const [amount, bridgingAllowed, claim] = watch(["amount", "bridgingAllowed", "claim"]);
|
||||
|
||||
const enoughAllowance = useMemo(() => bridgingAllowed || token?.type === TokenType.ETH, [bridgingAllowed, token]);
|
||||
|
||||
const { totalReceived, fees } = useReceivedAmount({
|
||||
amount,
|
||||
enoughAllowance,
|
||||
claim,
|
||||
});
|
||||
|
||||
const { fetchHistory } = useFetchHistory();
|
||||
|
||||
const { isConnected, address } = useAccount();
|
||||
|
||||
const {
|
||||
isLoading: isWaitingLoading,
|
||||
isSuccess: isWaitingSuccess,
|
||||
isError: isWaitingError,
|
||||
} = useWaitForTransactionReceipt({
|
||||
hash: waitingTransaction?.txHash,
|
||||
chainId: waitingTransaction?.chainId,
|
||||
});
|
||||
const { switchChain } = useSwitchNetwork(fromChain?.id);
|
||||
const { hash, isLoading, bridge, error: bridgeError } = useBridge();
|
||||
|
||||
// Set tx hash to trigger useWaitForTransaction
|
||||
useEffect(() => {
|
||||
if (hash) {
|
||||
setWaitingTransaction({
|
||||
txHash: hash,
|
||||
chainId: fromChain?.id,
|
||||
name: fromChain?.name,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hash]);
|
||||
|
||||
// Clear tx waiting when changing account
|
||||
useEffect(() => {
|
||||
setWaitingTransaction(undefined);
|
||||
}, [address]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWaitingSuccess && waitingTransaction) {
|
||||
handleShow(<TransactionConfirmationModal handleClose={handleClose} />, {
|
||||
showCloseButton: true,
|
||||
});
|
||||
setWaitingTransaction(undefined);
|
||||
fetchHistory();
|
||||
reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isWaitingSuccess, waitingTransaction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWaitingError) {
|
||||
toast.error("Token bridging failed.");
|
||||
setWaitingTransaction(undefined);
|
||||
}
|
||||
}, [isWaitingError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bridgeError?.displayInToast) {
|
||||
toast.error(
|
||||
<>
|
||||
{bridgeError.message}
|
||||
<a href={bridgeError.link} target="_blank" className="ml-1 underline">
|
||||
here
|
||||
</a>
|
||||
</>,
|
||||
{
|
||||
autoClose: false,
|
||||
draggable: false,
|
||||
closeOnClick: false,
|
||||
style: { width: 500, marginLeft: -90 },
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [bridgeError]);
|
||||
|
||||
// Click on approve
|
||||
const onSubmit = async (data: BridgeForm) => {
|
||||
if (isLoading || isWaitingLoading) return;
|
||||
await switchChain();
|
||||
bridge(data.amount, parseEther(data.minFees), data.recipient);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="min-w-min max-w-lg rounded-lg border-2 border-card bg-cardBg p-6 shadow-lg sm:p-4">
|
||||
<div
|
||||
className={cn({
|
||||
"opacity-30 pointer-events-none": !isConnected,
|
||||
})}
|
||||
>
|
||||
<FromChain />
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="grid grid-flow-col items-center gap-2 rounded-lg bg-[#2D2D2D] p-3">
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<TokenList />
|
||||
<Balance />
|
||||
</div>
|
||||
<div className="grid grid-flow-row">
|
||||
<Amount />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider my-6 flex justify-center">
|
||||
<SwapIcon />
|
||||
</div>
|
||||
|
||||
<ToChain />
|
||||
<div className="mb-4">
|
||||
<ReceivedAmount receivedAmount={totalReceived} />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<Recipient />
|
||||
</div>
|
||||
|
||||
<div className="mb-7">
|
||||
<ClaimingType />
|
||||
</div>
|
||||
|
||||
<div className="mb-7">{isConnected && <Fees totalReceived={totalReceived} fees={fees} />}</div>
|
||||
|
||||
<div className="text-center">
|
||||
{isConnected && <Submit isLoading={isLoading} isWaitingLoading={isWaitingLoading} />}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">{isConnected && <BridgeExternal />}</div>
|
||||
</div>
|
||||
{!isConnected && <ConnectButton fullWidth />}
|
||||
</div>
|
||||
</form>
|
||||
{token && networkLayer !== NetworkLayer.UNKNOWN && token[networkLayer] && (
|
||||
<div className="mt-4 px-2">
|
||||
<ERC20Stepper />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bridge;
|
||||
17
bridge-ui/src/components/bridge/BridgeExternal.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import Link from "next/link";
|
||||
import { MdArrowOutward } from "react-icons/md";
|
||||
|
||||
export function BridgeExternal() {
|
||||
return (
|
||||
<Link
|
||||
href="https://linea.build/apps?types=bridge"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 p-2"
|
||||
>
|
||||
<span>Bridge using Third-Party bridges</span>
|
||||
<MdArrowOutward className="text-primary" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import BridgeUI from "@/components/bridge/BridgeUI";
|
||||
import { useAccount } from "wagmi";
|
||||
import Bridge from "../bridge/Bridge";
|
||||
import { BridgeExternal } from "./BridgeExternal";
|
||||
import { useTokenStore } from "@/stores/tokenStore";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { BridgeForm } from "@/models";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { TokenType } from "@/config";
|
||||
|
||||
export default function BridgeLayout() {
|
||||
return <BridgeUI />;
|
||||
const { isConnected } = useAccount();
|
||||
|
||||
const configContextValue = useTokenStore((state) => state.tokensConfig);
|
||||
const token = useChainStore((state) => state.token);
|
||||
|
||||
const methods = useForm<BridgeForm>({
|
||||
defaultValues: {
|
||||
token: configContextValue?.UNKNOWN[0],
|
||||
claim: token?.type === TokenType.ETH ? "auto" : "manual",
|
||||
amount: "",
|
||||
minFees: "0",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isConnected && (
|
||||
<div className="mb-4 min-w-min max-w-lg rounded-lg border-2 border-card bg-cardBg p-2 shadow-lg">
|
||||
<BridgeExternal />
|
||||
</div>
|
||||
)}
|
||||
<FormProvider {...methods}>
|
||||
<Bridge />
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useAccount } from "wagmi";
|
||||
import WrongNetwork from "./WrongNetwork";
|
||||
import TermsModal from "../terms/TermsModal";
|
||||
import Bridge from "@/components/bridge/forms/Bridge";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { NetworkType } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
const BridgeUI: React.FC = () => {
|
||||
const networkType = useChainStore((state) => state.networkType);
|
||||
|
||||
// Hooks
|
||||
const { isConnected } = useAccount();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key="bridge-tech-operator"
|
||||
initial={{ opacity: 0, y: 200 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
onAnimationStart={() => {
|
||||
if (!isConnected) {
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
}}
|
||||
onAnimationComplete={() => {
|
||||
document.body.style.overflow = "";
|
||||
}}
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<div className="card w-full bg-base-100 shadow-xl md:w-[500px]">
|
||||
{networkType !== NetworkType.WRONG_NETWORK || !isConnected ? <Bridge /> : <WrongNetwork />}
|
||||
</div>
|
||||
{/* <Debug /> */}
|
||||
</div>
|
||||
</motion.div>
|
||||
<TermsModal />
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default BridgeUI;
|
||||
67
bridge-ui/src/components/bridge/Button.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useMemo, forwardRef } from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "outline" | "link";
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
type = "button",
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
disabled = false,
|
||||
loading = false,
|
||||
onClick,
|
||||
children,
|
||||
className,
|
||||
id,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const computedClassName = useMemo(() => {
|
||||
const baseClasses = "btn rounded-full font-semibold uppercase";
|
||||
const variantClasses = {
|
||||
primary: "btn-primary",
|
||||
secondary: "btn-secondary",
|
||||
outline:
|
||||
"btn-outline border-card text-white border-2 hover:bg-transparent hover:border-card hover:text-white disabled:border-2 disabled:border-card disabled:bg-transparent",
|
||||
link: "btn-link",
|
||||
}[variant];
|
||||
const sizeClasses = {
|
||||
xs: "btn-xs",
|
||||
sm: "btn-sm",
|
||||
md: "btn-md",
|
||||
lg: "btn-lg",
|
||||
}[size];
|
||||
|
||||
return cn(baseClasses, variantClasses, sizeClasses, className, {
|
||||
"btn-disabled": disabled,
|
||||
"cursor-wait btn-disabled": loading,
|
||||
});
|
||||
}, [variant, size, className, disabled, loading]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
id={id}
|
||||
type={type}
|
||||
className={computedClassName}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{loading && <span className="loading loading-spinner"></span>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export default Button;
|
||||
@@ -1,85 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { NetworkLayer } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
export default function Debug() {
|
||||
// Context
|
||||
const { networkType, networkLayer, tokenBridgeAddress, fromChain, toChain, token, messageServiceAddress } =
|
||||
useChainStore((state) => ({
|
||||
networkType: state.networkType,
|
||||
networkLayer: state.networkLayer,
|
||||
tokenBridgeAddress: state.tokenBridgeAddress,
|
||||
fromChain: state.fromChain,
|
||||
toChain: state.toChain,
|
||||
token: state.token,
|
||||
messageServiceAddress: state.messageServiceAddress,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="collapse">
|
||||
<input type="checkbox" />
|
||||
<button className="btn collapse-title btn-ghost text-xs font-medium opacity-30">Debug</button>
|
||||
<div className="collapse-content">
|
||||
<div className="card mt-5 w-full bg-base-100 shadow-xl md:w-[470px]">
|
||||
<div className="p-5 text-xs">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr className="text-left">
|
||||
<td>networkType:</td>
|
||||
<td>{networkType}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>networkLayer:</td>
|
||||
<td>{networkLayer}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>activeChain:</td>
|
||||
<td>{fromChain?.name}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>alternativeChain:</td>
|
||||
<td>{toChain?.name}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>fromChain:</td>
|
||||
<td>{fromChain?.name}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>toChain:</td>
|
||||
<td>{toChain?.name}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td colSpan={2}>
|
||||
<div className="divider"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>tokenBridge address:</td>
|
||||
<td>{tokenBridgeAddress}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>messageService address:</td>
|
||||
<td>{messageServiceAddress}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td colSpan={2}>
|
||||
<div className="divider"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>token:</td>
|
||||
<td>{token?.name}</td>
|
||||
</tr>
|
||||
<tr className="text-left">
|
||||
<td>address:</td>
|
||||
<td>{token && networkLayer !== NetworkLayer.UNKNOWN && token[networkLayer]}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
bridge-ui/src/components/bridge/ERC20Stepper.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { Stepper } from "./Stepper";
|
||||
|
||||
const STEPS = ["Approve", "Bridge"];
|
||||
|
||||
export function ERC20Stepper() {
|
||||
const token = useChainStore((state) => state.token);
|
||||
|
||||
const { watch } = useFormContext();
|
||||
|
||||
const watchAmount = watch("amount");
|
||||
const watchAllowance = watch("allowance");
|
||||
|
||||
const isETHTransfer = token && token.symbol === "ETH";
|
||||
const isApproved =
|
||||
!isETHTransfer && watchAmount && watchAmount > 0 && (watchAllowance || watchAllowance >= watchAmount);
|
||||
|
||||
return <Stepper steps={STEPS} activeStep={isApproved ? 1 : 0} />;
|
||||
}
|
||||
10
bridge-ui/src/components/bridge/FromChain.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import FromChainDropdown from "./FromChainDropdown";
|
||||
|
||||
export function FromChain() {
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="text-white">From this network</span>
|
||||
<FromChainDropdown />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
bridge-ui/src/components/bridge/FromChainDropdown.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { NetworkType } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { useEffect, useRef } from "react";
|
||||
import DropdownItem from "@/components/DropdownItem";
|
||||
import { getChainLogoPath } from "@/utils/chainsUtil";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
export default function FromChainDropdown() {
|
||||
const { networkType, fromChain, toChain, switchChain } = useChainStore((state) => ({
|
||||
networkType: state.networkType,
|
||||
fromChain: state.fromChain,
|
||||
toChain: state.toChain,
|
||||
switchChain: state.switchChain,
|
||||
}));
|
||||
const { reset } = useFormContext();
|
||||
|
||||
const detailsRef = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (detailsRef.current && !detailsRef.current.contains(e.target as Node)) {
|
||||
detailsRef.current.removeAttribute("open");
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const switchNetworkHandler = async () => {
|
||||
switchChain();
|
||||
reset();
|
||||
};
|
||||
|
||||
if (networkType == NetworkType.SEPOLIA || networkType == NetworkType.MAINNET) {
|
||||
return (
|
||||
<details className="dropdown relative" ref={detailsRef}>
|
||||
<summary className="flex cursor-pointer items-center gap-2 rounded-full bg-[#2D2D2D] p-2 px-3 text-white">
|
||||
{fromChain && (
|
||||
<Image
|
||||
src={getChainLogoPath(fromChain.id)}
|
||||
alt="MetaMask"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
)}
|
||||
<span className="hidden md:block">{fromChain?.name}</span>
|
||||
<svg
|
||||
className="size-4 text-card transition-transform"
|
||||
fill="none"
|
||||
stroke={"white"}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</summary>
|
||||
<ul className="menu dropdown-content absolute right-0 z-10 mt-2 min-w-max border-2 border-card bg-cardBg p-0 shadow">
|
||||
<DropdownItem
|
||||
title={toChain?.name || ""}
|
||||
iconPath={toChain && getChainLogoPath(toChain.id)}
|
||||
onClick={switchNetworkHandler}
|
||||
/>
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
}
|
||||
46
bridge-ui/src/components/bridge/ReceivedAmount.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useAccount } from "wagmi";
|
||||
import { PiApproximateEqualsBold } from "react-icons/pi";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { formatBalance } from "@/utils/format";
|
||||
import useTokenPrices from "@/hooks/useTokenPrices";
|
||||
import { zeroAddress } from "viem";
|
||||
import { NetworkType } from "@/config";
|
||||
|
||||
type ReceivedAmountProps = {
|
||||
receivedAmount?: string;
|
||||
};
|
||||
|
||||
export function ReceivedAmount({ receivedAmount }: ReceivedAmountProps) {
|
||||
const { isConnected } = useAccount();
|
||||
const { token, fromChain, tokenAddress, networkType } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
fromChain: state.fromChain,
|
||||
tokenAddress: state.token?.[state.networkLayer] || zeroAddress,
|
||||
networkType: state.networkType,
|
||||
}));
|
||||
|
||||
const { data: tokenPrices } = useTokenPrices([tokenAddress], fromChain?.id);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-20 flex-col gap-2 rounded-lg bg-[#2D2D2D] p-3">
|
||||
{isConnected && (
|
||||
<>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{formatBalance(receivedAmount) || 0} {token?.symbol}
|
||||
</span>
|
||||
{networkType === NetworkType.MAINNET && (
|
||||
<span className="label-text flex items-center">
|
||||
<PiApproximateEqualsBold /> $
|
||||
{tokenPrices?.[tokenAddress]?.usd
|
||||
? (Number(receivedAmount) * tokenPrices?.[tokenAddress]?.usd).toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 10,
|
||||
})
|
||||
: "0.00"}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { isAddress } from "viem";
|
||||
import { useAccount } from "wagmi";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { MdKeyboardArrowDown } from "react-icons/md";
|
||||
import { MdInfo, MdAdd } from "react-icons/md";
|
||||
import { Tooltip } from "../tooltip";
|
||||
|
||||
export default function Recipient() {
|
||||
export function Recipient() {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
|
||||
// Form
|
||||
const { register, formState, setValue, setError, clearErrors, watch } = useFormContext();
|
||||
const { errors } = formState;
|
||||
const watchRecipient = watch("recipient", false);
|
||||
|
||||
// Hooks
|
||||
const { isConnected } = useAccount();
|
||||
const watchRecipient = watch("recipient");
|
||||
|
||||
useEffect(() => {
|
||||
if (watchRecipient && !isAddress(watchRecipient)) {
|
||||
@@ -36,39 +29,29 @@ export default function Recipient() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("rounded-none collapse", {
|
||||
"text-neutral-600": !isConnected,
|
||||
})}
|
||||
>
|
||||
<div className="collapse rounded-none">
|
||||
<input type="checkbox" className="min-h-0" onChange={toggleCheckbox} />
|
||||
<div className="collapse-title flex min-h-0 flex-row justify-end space-x-1 p-0 text-sm">
|
||||
<div>Optional: Add recipient</div>{" "}
|
||||
<MdKeyboardArrowDown
|
||||
className={classNames("text-xl", {
|
||||
"rotate-180": isChecked,
|
||||
})}
|
||||
/>
|
||||
<div className="align-items collapse-title flex h-6 min-h-0 flex-row items-center gap-2 p-0 text-sm">
|
||||
<MdAdd className="size-6 text-primary" />
|
||||
<span className="">To different address</span>
|
||||
<Tooltip text="Input the address you want to bridge assets to on the recipient chain">
|
||||
<MdInfo />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className={classNames("collapse-content p-1 !pb-1", {
|
||||
"mt-3 h-18": isChecked,
|
||||
})}
|
||||
>
|
||||
|
||||
<div className="collapse-content p-1 !pb-1">
|
||||
<div className="form-control w-full">
|
||||
<div className="flex flex-row">
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full bg-[#2D2D2D]"
|
||||
placeholder="0x..."
|
||||
{...register("recipient", {
|
||||
validate: (value) => !value || isAddress(value) || "Invalid address",
|
||||
})}
|
||||
maxLength={42}
|
||||
disabled={!isConnected}
|
||||
placeholder="0x..."
|
||||
className="input input-bordered input-info w-full [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors.recipient && <div className="pt-2 text-error">{errors.recipient.message?.toString()}</div>}
|
||||
</div>
|
||||
</div>
|
||||
48
bridge-ui/src/components/bridge/Stepper.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
type StepperProps = {
|
||||
steps: string[];
|
||||
activeStep: number;
|
||||
};
|
||||
|
||||
export function Stepper({ steps, activeStep }: StepperProps) {
|
||||
return (
|
||||
<div className="flex items-end">
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={`step-${index}`}
|
||||
className={cn({
|
||||
"w-full": index !== steps.length - 1,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"-mx-px flex size-14 shrink-0 items-center justify-center rounded-full border-2 border-card bg-cardBg p-1.5",
|
||||
{
|
||||
"border-primary": index <= activeStep,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn("text-base font-bold text-[#E5E5E5]", {
|
||||
"text-primary": index <= activeStep,
|
||||
})}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
{index !== steps.length - 1 && (
|
||||
<div
|
||||
className={cn("h-1 w-full border-t-4 border-dotted border-card", {
|
||||
"border-primary": index < activeStep,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<h6 className="text-md mt-2 w-max text-[#C0C0C0]">{step}</h6>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
bridge-ui/src/components/bridge/Submit.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NetworkLayer } from "@/config";
|
||||
import { useBridge } from "@/hooks";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import ApproveERC20 from "./ApproveERC20";
|
||||
import Button from "./Button";
|
||||
import { useAccount, useBalance } from "wagmi";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
type SubmitProps = {
|
||||
isLoading: boolean;
|
||||
isWaitingLoading: boolean;
|
||||
};
|
||||
|
||||
export function Submit({ isLoading = false, isWaitingLoading = false }: SubmitProps) {
|
||||
// Form
|
||||
const { watch, formState } = useFormContext();
|
||||
const { errors } = formState;
|
||||
|
||||
const [watchAmount, watchAllowance, watchClaim] = watch(["amount", "allowance", "claim"]);
|
||||
|
||||
// Context
|
||||
const { token, networkLayer, toChainId } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
networkLayer: state.networkLayer,
|
||||
toChainId: state.toChain?.id,
|
||||
}));
|
||||
|
||||
// Wagmi
|
||||
const { bridgeEnabled } = useBridge();
|
||||
const { address } = useAccount();
|
||||
const { data: destinationChainBalance } = useBalance({
|
||||
address,
|
||||
chainId: toChainId,
|
||||
query: {
|
||||
enabled: !!address && !!toChainId,
|
||||
},
|
||||
});
|
||||
|
||||
const isERC20Token = token && networkLayer !== NetworkLayer.UNKNOWN && token[networkLayer];
|
||||
const isButtonDisabled = !bridgeEnabled(watchAmount, watchAllowance || BigInt(0), errors);
|
||||
const isETHTransfer = token && token.symbol === "ETH";
|
||||
const showApproveERC20 = !isETHTransfer && (!watchAllowance || watchAllowance < watchAmount);
|
||||
|
||||
// TODO: refactor this
|
||||
const destinationBalanceTooLow =
|
||||
watchClaim === "manual" && destinationChainBalance && destinationChainBalance.value === 0n;
|
||||
|
||||
const buttonText = errors?.amount?.message
|
||||
? "Insufficient balance"
|
||||
: destinationBalanceTooLow
|
||||
? "Bridge anyway"
|
||||
: "Bridge";
|
||||
|
||||
return isETHTransfer ? (
|
||||
<Button
|
||||
type="submit"
|
||||
className={cn("w-full", {
|
||||
"btn-secondary": destinationBalanceTooLow,
|
||||
})}
|
||||
disabled={isButtonDisabled}
|
||||
loading={isLoading || isWaitingLoading}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
) : showApproveERC20 && isERC20Token ? (
|
||||
<ApproveERC20 />
|
||||
) : (
|
||||
<Button
|
||||
id="submit-erc-btn"
|
||||
className="w-full"
|
||||
disabled={isButtonDisabled}
|
||||
loading={isLoading || isWaitingLoading}
|
||||
type="submit"
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
10
bridge-ui/src/components/bridge/ToChain.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import ToChainDropdown from "./ToChainDropdown";
|
||||
|
||||
export function ToChain() {
|
||||
return (
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="text-white">To this network</span>
|
||||
<ToChainDropdown />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
bridge-ui/src/components/bridge/ToChainDropdown.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { NetworkType } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { useEffect, useRef } from "react";
|
||||
import DropdownItem from "@/components/DropdownItem";
|
||||
import { getChainLogoPath } from "@/utils/chainsUtil";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
export default function ToChainDropdown() {
|
||||
const { networkType, fromChain, toChain, switchChain } = useChainStore((state) => ({
|
||||
networkType: state.networkType,
|
||||
fromChain: state.fromChain,
|
||||
toChain: state.toChain,
|
||||
switchChain: state.switchChain,
|
||||
}));
|
||||
|
||||
const { reset } = useFormContext();
|
||||
|
||||
const detailsRef = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (detailsRef.current && !detailsRef.current.contains(e.target as Node)) {
|
||||
detailsRef.current.removeAttribute("open");
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const switchNetworkHandler = async () => {
|
||||
switchChain();
|
||||
reset();
|
||||
};
|
||||
|
||||
if (networkType == NetworkType.SEPOLIA || networkType == NetworkType.MAINNET) {
|
||||
return (
|
||||
<details className="dropdown relative" ref={detailsRef}>
|
||||
<summary className="flex cursor-pointer items-center gap-2 rounded-full bg-[#2D2D2D] p-2 px-3 text-white">
|
||||
{toChain && (
|
||||
<Image
|
||||
src={getChainLogoPath(toChain.id)}
|
||||
alt="MetaMask"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
)}
|
||||
<span className="hidden md:block">{toChain?.name}</span>
|
||||
<svg
|
||||
className="size-4 text-card transition-transform"
|
||||
fill="none"
|
||||
stroke={"white"}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</summary>
|
||||
<ul className="menu dropdown-content absolute right-0 z-10 mt-2 min-w-max border-2 border-card bg-cardBg p-0 shadow">
|
||||
<DropdownItem
|
||||
title={fromChain?.name || ""}
|
||||
iconPath={fromChain && getChainLogoPath(fromChain.id)}
|
||||
onClick={switchNetworkHandler}
|
||||
/>
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,25 +2,26 @@
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useAccount, useBalance, useBlockNumber } from "wagmi";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { FieldValues, UseFormClearErrors, UseFormSetValue } from "react-hook-form";
|
||||
import { useBlockNumber } from "wagmi";
|
||||
import { config } from "@/config";
|
||||
import { formatBalance } from "@/utils/format";
|
||||
import { NetworkLayer, NetworkType, TokenInfo, TokenType } from "@/config/config";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { useTokenBalance } from "@/hooks/useTokenBalance";
|
||||
|
||||
interface TokenDetailsProps {
|
||||
token: TokenInfo;
|
||||
onTokenClick: (token: TokenInfo) => void;
|
||||
setValue: UseFormSetValue<FieldValues>;
|
||||
clearErrors: UseFormClearErrors<FieldValues>;
|
||||
tokenPrice?: number;
|
||||
}
|
||||
export default function TokenDetails({ token, onTokenClick }: TokenDetailsProps) {
|
||||
const { address } = useAccount();
|
||||
const { networkLayer, fromChain, setToken, setTokenBridgeAddress, networkType } = useChainStore((state) => ({
|
||||
|
||||
export default function TokenDetails({ token, onTokenClick, setValue, clearErrors, tokenPrice }: TokenDetailsProps) {
|
||||
const { networkLayer, setToken, setTokenBridgeAddress, networkType } = useChainStore((state) => ({
|
||||
networkLayer: state.networkLayer,
|
||||
fromChain: state.fromChain,
|
||||
setToken: state.setToken,
|
||||
setTokenBridgeAddress: state.setTokenBridgeAddress,
|
||||
networkType: state.networkType,
|
||||
@@ -28,33 +29,21 @@ export default function TokenDetails({ token, onTokenClick }: TokenDetailsProps)
|
||||
|
||||
const tokenNotFromCurrentLayer = !token[networkLayer] && token.type !== TokenType.ETH;
|
||||
|
||||
// Form
|
||||
const { setValue, clearErrors } = useFormContext();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: blockNumber } = useBlockNumber({ watch: true });
|
||||
const { data: balance, queryKey } = useBalance({
|
||||
address,
|
||||
token: token[networkLayer] ?? undefined,
|
||||
chainId: fromChain?.id,
|
||||
});
|
||||
const { balance, refetch } = useTokenBalance(token[networkLayer], token?.decimals);
|
||||
|
||||
useEffect(() => {
|
||||
if (blockNumber && blockNumber % 5n === 0n) {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
refetch();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockNumber, queryClient]);
|
||||
}, [blockNumber, refetch]);
|
||||
|
||||
return (
|
||||
<button
|
||||
id={`token-details-${token.symbol}-btn`}
|
||||
className={classNames(
|
||||
"flex items-center justify-between w-full gap-5 px-8 py-3 mt-3 bg-transparent border-0 hover:bg-slate-900/20",
|
||||
{
|
||||
"btn-disabled": tokenNotFromCurrentLayer,
|
||||
},
|
||||
)}
|
||||
className={cn("flex items-center justify-between w-full px-4 py-3 bg-transparent border-0 hover:bg-[#262626]", {
|
||||
"btn-disabled": tokenNotFromCurrentLayer,
|
||||
})}
|
||||
disabled={tokenNotFromCurrentLayer}
|
||||
onClick={() => {
|
||||
if (networkLayer !== NetworkLayer.UNKNOWN && token && networkType !== NetworkType.WRONG_NETWORK) {
|
||||
@@ -75,25 +64,26 @@ export default function TokenDetails({ token, onTokenClick }: TokenDetailsProps)
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-5">
|
||||
<Image
|
||||
src={token.image}
|
||||
alt={token.name}
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "40px", height: "auto" }}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<Image src={token.image} alt={token.name} width={40} height={40} className="rounded-full" />
|
||||
<div className="text-left">
|
||||
<p className="text-semibold">{token.name}</p>
|
||||
<p className="text-sm text-zinc-300">{token.symbol}</p>
|
||||
<p className="font-normal text-white">{token.symbol}</p>
|
||||
<p className="text-sm font-normal text-[#C0C0C0]">{token.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!tokenNotFromCurrentLayer && (
|
||||
<div className="text-right">
|
||||
<p>Balance</p>
|
||||
<p className="text-sm text-zinc-300">
|
||||
{formatBalance(balance?.formatted)} {balance?.symbol}
|
||||
<p className="text-md text-white">
|
||||
{formatBalance(balance)} {token.symbol}
|
||||
</p>
|
||||
{tokenPrice ? (
|
||||
<p className="text-[#C0C0C0]">
|
||||
$
|
||||
{(tokenPrice * Number(balance)).toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{tokenNotFromCurrentLayer && (
|
||||
40
bridge-ui/src/components/bridge/TokenList.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Image from "next/image";
|
||||
import { MdKeyboardArrowDown } from "react-icons/md";
|
||||
import { ModalContext } from "@/contexts/modal.context";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { useContext } from "react";
|
||||
import { useAccount } from "wagmi";
|
||||
import TokenModal from "./TokenModal";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import Button from "./Button";
|
||||
|
||||
export default function TokenList() {
|
||||
const token = useChainStore((state) => state.token);
|
||||
const { isConnected } = useAccount();
|
||||
|
||||
const { handleShow } = useContext(ModalContext);
|
||||
const { setValue, clearErrors } = useFormContext();
|
||||
|
||||
return (
|
||||
<div className="dropdown">
|
||||
{token && (
|
||||
<Button
|
||||
id="token-select-btn"
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="px-2 py-1"
|
||||
disabled={!isConnected}
|
||||
onClick={() =>
|
||||
handleShow(<TokenModal setValue={setValue} clearErrors={clearErrors} />, {
|
||||
showCloseButton: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Image src={token.image} alt={token.name} width={25} height={25} className="rounded-full" />
|
||||
{token.symbol}
|
||||
<MdKeyboardArrowDown className="text-white" size={20} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { isAddress, getAddress } from "viem";
|
||||
import { isAddress, getAddress, Address, zeroAddress } from "viem";
|
||||
import TokenDetails from "./TokenDetails";
|
||||
import { NetworkType, TokenInfo, TokenType } from "@/config/config";
|
||||
import useERC20Storage from "@/hooks/useERC20Storage";
|
||||
import { useERC20Storage, useTokenFetch } from "@/hooks";
|
||||
import { safeGetAddress } from "@/utils/format";
|
||||
import { useBridge } from "@/hooks";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { useTokenStore } from "@/stores/tokenStore";
|
||||
import { fetchTokenInfo } from "@/services";
|
||||
import { FieldValues, UseFormClearErrors, UseFormSetValue } from "react-hook-form";
|
||||
import { CiSearch } from "react-icons/ci";
|
||||
import useTokenPrices from "@/hooks/useTokenPrices";
|
||||
import { isEmptyObject } from "@/utils/utils";
|
||||
|
||||
interface Props {
|
||||
tokensModalRef: React.RefObject<HTMLDialogElement>;
|
||||
interface TokenModalProps {
|
||||
setValue: UseFormSetValue<FieldValues>;
|
||||
clearErrors: UseFormClearErrors<FieldValues>;
|
||||
}
|
||||
|
||||
export default function TokenModal({ tokensModalRef }: Props) {
|
||||
export default function TokenModal({ setValue, clearErrors }: TokenModalProps) {
|
||||
const tokensConfig = useTokenStore((state) => state.tokensConfig);
|
||||
const [filteredTokens, setFilteredTokens] = useState<TokenInfo[]>([]);
|
||||
const [searchTokenIsNew, setSearchTokenIsNew] = useState<boolean>(false);
|
||||
const { fillMissingTokenAddress } = useBridge();
|
||||
const { fillMissingTokenAddress } = useTokenFetch();
|
||||
|
||||
// Context
|
||||
const { networkType, networkLayer, fromChain } = useChainStore((state) => ({
|
||||
@@ -29,6 +33,12 @@ export default function TokenModal({ tokensModalRef }: Props) {
|
||||
}));
|
||||
const { updateOrInsertUserTokenList } = useERC20Storage();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { data } = useTokenPrices(
|
||||
filteredTokens.map((token) =>
|
||||
token.name === "Ether" ? zeroAddress : (safeGetAddress(token[networkLayer]) as Address),
|
||||
),
|
||||
fromChain?.id,
|
||||
);
|
||||
|
||||
useMemo(async () => {
|
||||
let found = false;
|
||||
@@ -82,27 +92,36 @@ export default function TokenModal({ tokensModalRef }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog ref={tokensModalRef} id="token_picker_modal" className="modal px-0" onClose={() => setSearchQuery("")}>
|
||||
<form method="dialog" className="modal-box overflow-hidden px-0">
|
||||
<button id="close-token-picker-modal-btn" className="btn btn-circle btn-ghost btn-sm absolute right-2 top-2">
|
||||
✕
|
||||
</button>
|
||||
<h3 className="pl-8 text-lg font-bold">Select Token</h3>
|
||||
|
||||
{/* SEARCH FORM */}
|
||||
<div className="my-3 flex justify-center border-b border-b-zinc-200 px-7 pb-5 dark:border-b-slate-900/50">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search token by name, symbol or address"
|
||||
className="input input-bordered w-full"
|
||||
onChange={({ target: { value } }) => setSearchQuery(normalizeInput(value))}
|
||||
value={searchQuery}
|
||||
/>
|
||||
<div id="token-picker-modal">
|
||||
<form method="dialog" className="overflow-hidden">
|
||||
<div className="mt-1 flex justify-center px-4">
|
||||
<label className="input input-bordered flex w-full items-center gap-2 rounded-full bg-inherit focus:border-0 focus:outline-none">
|
||||
<CiSearch size="20" />
|
||||
<input
|
||||
type="text"
|
||||
className="grow"
|
||||
placeholder="Search token by name, symbol or address"
|
||||
onChange={({ target: { value } }) => setSearchQuery(normalizeInput(value))}
|
||||
value={searchQuery}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="max-h-[50vh] overflow-auto">
|
||||
<div className="mt-3 max-h-[50vh] overflow-auto">
|
||||
{filteredTokens.length > 0 ? (
|
||||
filteredTokens.map((token: TokenInfo, index: number) => (
|
||||
<TokenDetails token={token} onTokenClick={onTokenClick} key={index} />
|
||||
<TokenDetails
|
||||
token={token}
|
||||
onTokenClick={onTokenClick}
|
||||
key={index}
|
||||
setValue={setValue}
|
||||
clearErrors={clearErrors}
|
||||
tokenPrice={
|
||||
(networkType === NetworkType.MAINNET &&
|
||||
!isEmptyObject(data) &&
|
||||
data[safeGetAddress(token[networkLayer])?.toLowerCase() || zeroAddress]?.usd) ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="pl-7 text-error">
|
||||
@@ -111,6 +130,6 @@ export default function TokenModal({ tokensModalRef }: Props) {
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { linea, mainnet } from "viem/chains";
|
||||
import { wagmiConfig } from "@/config";
|
||||
import { switchChain } from "@wagmi/core";
|
||||
|
||||
export default function WrongNetwork() {
|
||||
const switchNetwork = async (id: number) => {
|
||||
await switchChain(wagmiConfig, {
|
||||
chainId: id,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card-body">
|
||||
<div className="space-y-5">
|
||||
<div>Wrong network</div>
|
||||
<ul className="space-y-5">
|
||||
<li>
|
||||
<button className="btn" onClick={() => switchNetwork(mainnet.id)}>
|
||||
<Image
|
||||
src={"/images/logo/ethereum.svg"}
|
||||
alt="Ethereum"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "12px", height: "auto" }}
|
||||
/>{" "}
|
||||
Mainnet
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className="btn" onClick={() => switchNetwork(linea.id)}>
|
||||
<Image
|
||||
src={"/images/logo/linea-mainnet.svg"}
|
||||
alt="Linea"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>{" "}
|
||||
Linea
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
bridge-ui/src/components/bridge/fees/FeeLine.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { BsDashLg } from "react-icons/bs";
|
||||
import { MdInfo } from "react-icons/md";
|
||||
import { Tooltip } from "@/components/tooltip";
|
||||
|
||||
interface FeeLineProps {
|
||||
label: string;
|
||||
value: string | undefined;
|
||||
tooltip?: string;
|
||||
tooltipClassName?: string;
|
||||
}
|
||||
|
||||
export const FeeLine: React.FC<FeeLineProps> = ({ label, value, tooltip, tooltipClassName }) => (
|
||||
<div className="flex justify-between text-[#C0C0C0]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{label}:</span>
|
||||
{tooltip && (
|
||||
<Tooltip text={tooltip} className={tooltipClassName}>
|
||||
<MdInfo />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<span>{value ? value : <BsDashLg />}</span>
|
||||
</div>
|
||||
);
|
||||
85
bridge-ui/src/components/bridge/fees/Fees.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAccount, useBalance } from "wagmi";
|
||||
import { formatEther, zeroAddress } from "viem";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { NetworkLayer, NetworkType } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { formatBalance } from "@/utils/format";
|
||||
import { FeeLine } from "./FeeLine";
|
||||
import useTokenPrices from "@/hooks/useTokenPrices";
|
||||
|
||||
type FeesProps = {
|
||||
totalReceived: string;
|
||||
fees: {
|
||||
total: bigint;
|
||||
executionFeeInWei: bigint;
|
||||
bridgingFeeInWei: bigint;
|
||||
};
|
||||
};
|
||||
|
||||
export function Fees({ totalReceived, fees: { total, executionFeeInWei, bridgingFeeInWei } }: FeesProps) {
|
||||
// Context
|
||||
const { token, networkLayer, fromChain, networkType } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
networkType: state.networkType,
|
||||
networkLayer: state.networkLayer,
|
||||
fromChain: state.fromChain,
|
||||
}));
|
||||
|
||||
// Form
|
||||
const { watch, setError, clearErrors } = useFormContext();
|
||||
const amount = watch("amount");
|
||||
|
||||
// Wagmi
|
||||
const { address, isConnected } = useAccount();
|
||||
const { data: ethBalance } = useBalance({
|
||||
address,
|
||||
chainId: fromChain?.id,
|
||||
});
|
||||
|
||||
// Hooks
|
||||
const { data: ethPrice } = useTokenPrices([zeroAddress], fromChain?.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (ethBalance && total && total > 0 && ethBalance.value <= total) {
|
||||
setError("minFees", {
|
||||
type: "custom",
|
||||
message: "Execution fees exceed ETH balance",
|
||||
});
|
||||
} else {
|
||||
clearErrors("minFees");
|
||||
}
|
||||
}, [setError, clearErrors, ethBalance, total]);
|
||||
|
||||
const estimatedTime = networkLayer === NetworkLayer.L1 ? "20 mins" : "8 hrs to 32 hrs";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<FeeLine
|
||||
label="Estimated Time"
|
||||
value={amount && estimatedTime}
|
||||
tooltip="Linea has a minimum 8 hour delay on withdrawals as a security measure.
|
||||
Withdrawals can take up to 32 hours to complete"
|
||||
/>
|
||||
<FeeLine
|
||||
label="Estimated Total Fee"
|
||||
value={
|
||||
isConnected &&
|
||||
amount &&
|
||||
(networkType === NetworkType.MAINNET && ethPrice && ethPrice?.[zeroAddress]
|
||||
? `$${(Number(formatEther(total)) * ethPrice[zeroAddress].usd).toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 10,
|
||||
})}`
|
||||
: `${formatBalance(formatEther(total))} ETH`)
|
||||
}
|
||||
tooltipClassName="before:whitespace-pre-wrap before:content-[attr(data-tip)] text-left"
|
||||
tooltip={`Execution Fee: ${formatEther(executionFeeInWei)} ETH\nBridging fee: ${formatEther(bridgingFeeInWei)} ETH`}
|
||||
/>
|
||||
<FeeLine
|
||||
label="Total Received"
|
||||
value={totalReceived && totalReceived !== "0" ? `${formatBalance(totalReceived)} ${token?.symbol}` : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
bridge-ui/src/components/bridge/fees/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { FeeLine } from "./FeeLine";
|
||||
export { Fees } from "./Fees";
|
||||
83
bridge-ui/src/components/bridge/form/ClaimingType.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useAccount } from "wagmi";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { NetworkLayer, TokenType } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import ClaimingTypeOption from "./ClaimingTypeOption";
|
||||
import { ModalContext } from "@/contexts/modal.context";
|
||||
import ManualClaimModal from "../modals/ManualClaimModal";
|
||||
|
||||
export function ClaimingType() {
|
||||
const { handleShow, handleClose } = useContext(ModalContext);
|
||||
const { token, networkLayer } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
networkLayer: state.networkLayer,
|
||||
}));
|
||||
|
||||
const { isConnected } = useAccount();
|
||||
const { setValue, register, watch } = useFormContext();
|
||||
const [isManualConfirmed, setIsManualConfirmed] = useState(false);
|
||||
|
||||
const selectedClaimType = watch("claim");
|
||||
|
||||
const isAutoDisabled = networkLayer === NetworkLayer.L2 || token?.type !== TokenType.ETH || !isConnected;
|
||||
|
||||
useEffect(() => {
|
||||
if (networkLayer === NetworkLayer.L2 || token?.type !== TokenType.ETH) {
|
||||
setValue("claim", "manual");
|
||||
} else if (token?.type === TokenType.ETH && networkLayer === NetworkLayer.L1) {
|
||||
setValue("claim", "auto");
|
||||
} else if (isManualConfirmed) {
|
||||
setValue("claim", "manual");
|
||||
}
|
||||
}, [token, setValue, networkLayer, isManualConfirmed]);
|
||||
|
||||
const handleManualClaimClick = () => {
|
||||
if (selectedClaimType !== "manual") {
|
||||
handleShow(
|
||||
<ManualClaimModal
|
||||
handleYesClose={() => {
|
||||
setIsManualConfirmed(true);
|
||||
setValue("claim", "manual");
|
||||
handleClose();
|
||||
}}
|
||||
handleNoClose={() => {
|
||||
setIsManualConfirmed(false);
|
||||
handleClose();
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isManualConfirmed) {
|
||||
setValue("claim", "manual");
|
||||
}
|
||||
}, [isManualConfirmed, setValue]);
|
||||
|
||||
return (
|
||||
<div className="form-control grid grid-flow-row gap-2 rounded-lg bg-[#2D2D2D] p-2 sm:grid-flow-col sm:rounded-full">
|
||||
<ClaimingTypeOption
|
||||
id="claim-auto"
|
||||
value="auto"
|
||||
label="Automatic"
|
||||
tooltip="Your transaction will be automatically deposited to the receiving address on destination chain. Suitable for first time bridges"
|
||||
disabled={isAutoDisabled}
|
||||
isConnected={isConnected}
|
||||
register={register("claim")}
|
||||
isSelected={selectedClaimType === "auto"}
|
||||
/>
|
||||
<ClaimingTypeOption
|
||||
id="claim-manual"
|
||||
value="manual"
|
||||
label="Manual Claim (Advanced)"
|
||||
tooltip="You will need to claim your transaction on the destination chain with an additional transaction that requires ETH on the destination chain"
|
||||
disabled={!isConnected}
|
||||
isConnected={isConnected}
|
||||
onClick={handleManualClaimClick}
|
||||
isSelected={selectedClaimType === "manual"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
bridge-ui/src/components/bridge/form/ClaimingTypeOption.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import { UseFormRegisterReturn } from "react-hook-form";
|
||||
import { MdInfo } from "react-icons/md";
|
||||
import { Tooltip } from "../../tooltip";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
interface ClaimOptionProps {
|
||||
id: string;
|
||||
value?: string;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
disabled: boolean;
|
||||
isConnected: boolean;
|
||||
onClick?: () => void;
|
||||
register?: UseFormRegisterReturn;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const ClaimingTypeOption: React.FC<ClaimOptionProps> = ({
|
||||
id,
|
||||
value,
|
||||
label,
|
||||
tooltip,
|
||||
disabled,
|
||||
isConnected,
|
||||
onClick,
|
||||
register,
|
||||
isSelected,
|
||||
}) => (
|
||||
<div>
|
||||
<input
|
||||
{...(register ? register : {})}
|
||||
id={id}
|
||||
type="radio"
|
||||
value={value}
|
||||
className="peer hidden"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
checked={isSelected}
|
||||
readOnly
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={cn("btn btn-outline normal-case font-normal w-full rounded-full", {
|
||||
"btn-disabled": disabled,
|
||||
"peer-checked:border-primary peer-checked:bg-cardBg peer-checked:text-white": isConnected,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
<Tooltip text={tooltip}>
|
||||
<MdInfo />
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ClaimingTypeOption;
|
||||
@@ -1,179 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ChangeEvent, useCallback, useEffect } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import Image from "next/image";
|
||||
import { useAccount } from "wagmi";
|
||||
import { formatEther, parseUnits } from "viem";
|
||||
import { useBridge } from "@/hooks";
|
||||
import { TokenType } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import useMinimumFee from "@/hooks/useMinimumFee";
|
||||
|
||||
const MAX_AMOUNT_CHAR = 24;
|
||||
const FEES_MARGIN_PERCENT = 20;
|
||||
const AMOUNT_REGEX = "^[0-9]*[.,]?[0-9]*$";
|
||||
|
||||
interface Props {
|
||||
tokensModalRef: React.RefObject<HTMLDialogElement>;
|
||||
}
|
||||
|
||||
export default function Amount({ tokensModalRef }: Props) {
|
||||
// Context
|
||||
const token = useChainStore((state) => state.token);
|
||||
|
||||
// Form
|
||||
const { setValue, getValues, formState, setError, clearErrors, trigger, watch } = useFormContext();
|
||||
const { errors } = formState;
|
||||
const watchBalance = watch("balance", false);
|
||||
const amount = getValues("amount");
|
||||
const gasFees = getValues("gasFees") || BigInt(0);
|
||||
const minFees = getValues("minFees") || BigInt(0);
|
||||
|
||||
// Wagmi
|
||||
const { address } = useAccount();
|
||||
|
||||
// Hooks
|
||||
const { isConnected } = useAccount();
|
||||
const { estimateGasBridge } = useBridge();
|
||||
const { minimumFee } = useMinimumFee();
|
||||
|
||||
const compareAmountBalance = useCallback(
|
||||
(_amount: string) => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const amountToCompare =
|
||||
token.type === TokenType.ETH
|
||||
? parseUnits(_amount, token.decimals) + gasFees + parseUnits(minFees.toString(), 18)
|
||||
: parseUnits(_amount, token.decimals);
|
||||
const balanceToCompare = parseUnits(watchBalance, token.decimals);
|
||||
if (amountToCompare > balanceToCompare) {
|
||||
setError("amount", {
|
||||
type: "custom",
|
||||
message: "Not enough funds (Incl fees)",
|
||||
});
|
||||
} else {
|
||||
clearErrors("amount");
|
||||
}
|
||||
},
|
||||
[token, gasFees, minFees, clearErrors, setError, watchBalance],
|
||||
);
|
||||
|
||||
/**
|
||||
* Set Max Amount
|
||||
*/
|
||||
const setMaxHandler = async () => {
|
||||
if (!token || !watchBalance) return;
|
||||
|
||||
let maxAmount;
|
||||
if (token.type === TokenType.ETH) {
|
||||
const bridgeGasFee = await estimateGasBridge(watchBalance, minimumFee);
|
||||
if (!bridgeGasFee) return;
|
||||
|
||||
// Add margin to gas fees for prevent error when gas fees change
|
||||
const gasFeesMargin = (bridgeGasFee * BigInt(100 + FEES_MARGIN_PERCENT)) / BigInt(100);
|
||||
|
||||
maxAmount = formatEther(parseUnits(watchBalance, token.decimals) - gasFeesMargin - parseUnits(minFees, 18));
|
||||
} else {
|
||||
maxAmount = watchBalance;
|
||||
}
|
||||
|
||||
setValue("amount", maxAmount);
|
||||
compareAmountBalance(maxAmount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamic check amount
|
||||
* @param e
|
||||
* @returns
|
||||
*/
|
||||
const checkAmountHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
// Replace minus
|
||||
const amount = e.target.value.replace(/,/g, ".");
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new RegExp(AMOUNT_REGEX).test(amount) || amount === "") {
|
||||
// Limit max char
|
||||
if (amount.length > MAX_AMOUNT_CHAR) {
|
||||
setValue("amount", amount.substring(0, MAX_AMOUNT_CHAR));
|
||||
} else {
|
||||
setValue("amount", amount);
|
||||
}
|
||||
}
|
||||
|
||||
compareAmountBalance(amount);
|
||||
};
|
||||
|
||||
// Detect changes
|
||||
useEffect(() => {
|
||||
if (amount) {
|
||||
trigger(["amount"]);
|
||||
compareAmountBalance(amount);
|
||||
}
|
||||
}, [amount, trigger, compareAmountBalance]);
|
||||
|
||||
// Detect when changing account
|
||||
useEffect(() => {
|
||||
setValue("amount", "");
|
||||
clearErrors("amount");
|
||||
}, [address, setValue, clearErrors]);
|
||||
|
||||
return (
|
||||
<div className="form-control">
|
||||
<div className="flex flex-row">
|
||||
<div className="dropdown">
|
||||
{token && (
|
||||
<button
|
||||
id={`token-select-btn`}
|
||||
type="button"
|
||||
className="btn btn-neutral mr-2 flex w-28 flex-row px-0"
|
||||
disabled={!isConnected}
|
||||
onClick={() => tokensModalRef?.current?.showModal()}
|
||||
>
|
||||
<Image
|
||||
src={token.image}
|
||||
alt={token.name}
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "20px", height: "auto" }}
|
||||
className="rounded-full"
|
||||
/>
|
||||
{token.symbol}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="amount-input"
|
||||
type="text"
|
||||
pattern={AMOUNT_REGEX}
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
inputMode="decimal"
|
||||
value={amount}
|
||||
onChange={checkAmountHandler}
|
||||
disabled={!isConnected}
|
||||
placeholder="Enter amount"
|
||||
className="input input-bordered input-info w-full max-w-xs [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
{token?.type !== TokenType.ETH && (
|
||||
<button
|
||||
id="max-amount-btn"
|
||||
className="btn btn-primary btn-xs -ml-14 mt-3 rounded-full"
|
||||
type="button"
|
||||
disabled={!isConnected}
|
||||
onClick={setMaxHandler}
|
||||
>
|
||||
Max
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{errors.amount && <div className="pt-2 text-right text-error">{errors.amount.message?.toString()}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useAccount, useBalance, UseBalanceReturnType, useBlockNumber } from "wagmi";
|
||||
import classNames from "classnames";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { formatBalance } from "@/utils/format";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
export default function Balance() {
|
||||
const [currentBalance, setCurrentBalance] = useState<UseBalanceReturnType["data"] | undefined>();
|
||||
// Context
|
||||
const { token, networkLayer, fromChain } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
networkLayer: state.networkLayer,
|
||||
fromChain: state.fromChain,
|
||||
}));
|
||||
|
||||
const tokenAddress = token && token[networkLayer] ? token[networkLayer] : undefined;
|
||||
|
||||
// Wagmi
|
||||
const { address, isConnected } = useAccount();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: blockNumber } = useBlockNumber({ watch: true });
|
||||
const { data: balance, queryKey } = useBalance({
|
||||
address,
|
||||
token: tokenAddress ?? undefined,
|
||||
chainId: fromChain?.id,
|
||||
});
|
||||
|
||||
// Form
|
||||
const { setValue } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (balance) {
|
||||
setValue("balance", balance.formatted);
|
||||
setCurrentBalance(balance);
|
||||
} else {
|
||||
setValue("balance", "");
|
||||
setCurrentBalance(undefined);
|
||||
}
|
||||
}, [balance, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (blockNumber && blockNumber % 5n === 0n) {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockNumber, queryClient]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("", {
|
||||
"text-neutral-600": !isConnected,
|
||||
})}
|
||||
>
|
||||
Balance: {formatBalance(currentBalance?.formatted)} {currentBalance?.symbol}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
import { useAccount, useWaitForTransactionReceipt } from "wagmi";
|
||||
import { parseEther } from "viem";
|
||||
import { toast } from "react-toastify";
|
||||
import { BridgeForm, Transaction } from "@/models";
|
||||
import FromChainToChain from "./FromChainToChain";
|
||||
import Amount from "./Amount";
|
||||
import Balance from "./Balance";
|
||||
import { useSwitchNetwork } from "@/hooks";
|
||||
import Submit from "./Submit";
|
||||
import Fees from "./Fees";
|
||||
import TokenModal from "./TokenModal";
|
||||
import useBridge from "@/hooks/useBridge";
|
||||
import Recipient from "./Recipient";
|
||||
import { TokenType } from "@/config/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import { useTokenStore } from "@/stores/tokenStore";
|
||||
import useFetchHistory from "@/hooks/useFetchHistory";
|
||||
|
||||
export default function Bridge() {
|
||||
const configContextValue = useTokenStore((state) => state.tokensConfig);
|
||||
const [waitingTransaction, setWaitingTransaction] = useState<Transaction | undefined>();
|
||||
|
||||
// Dialog Ref
|
||||
const tokensModalRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
// Context
|
||||
const { fromChain, token } = useChainStore((state) => ({
|
||||
fromChain: state.fromChain,
|
||||
token: state.token,
|
||||
}));
|
||||
const { fetchHistory } = useFetchHistory();
|
||||
|
||||
// Wagmi
|
||||
const { address } = useAccount();
|
||||
const {
|
||||
isLoading: isWaitingLoading,
|
||||
isSuccess: isWaitingSuccess,
|
||||
isError: isWaitingError,
|
||||
} = useWaitForTransactionReceipt({
|
||||
hash: waitingTransaction?.txHash,
|
||||
chainId: waitingTransaction?.chainId,
|
||||
});
|
||||
|
||||
// Form
|
||||
const methods = useForm<BridgeForm>({
|
||||
defaultValues: {
|
||||
token: configContextValue?.UNKNOWN[0],
|
||||
claim: token?.type === TokenType.ETH ? "auto" : "manual",
|
||||
amount: "",
|
||||
},
|
||||
});
|
||||
|
||||
// Hooks
|
||||
const { switchChain } = useSwitchNetwork(fromChain?.id);
|
||||
const { hash, isLoading, bridge, error: bridgeError } = useBridge();
|
||||
|
||||
// Set tx hash to trigger useWaitForTransaction
|
||||
useEffect(() => {
|
||||
if (hash) {
|
||||
setWaitingTransaction({
|
||||
txHash: hash,
|
||||
chainId: fromChain?.id,
|
||||
name: fromChain?.name,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hash]);
|
||||
|
||||
// Clear tx waiting when changing account
|
||||
useEffect(() => {
|
||||
setWaitingTransaction(undefined);
|
||||
}, [address]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWaitingSuccess && waitingTransaction) {
|
||||
toast.success(`Transaction validated on ${waitingTransaction?.name}.`);
|
||||
setWaitingTransaction(undefined);
|
||||
fetchHistory();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isWaitingSuccess, waitingTransaction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWaitingError) {
|
||||
toast.error("Token bridging failed.");
|
||||
setWaitingTransaction(undefined);
|
||||
}
|
||||
}, [isWaitingError]);
|
||||
|
||||
useEffect(() => {
|
||||
bridgeError &&
|
||||
bridgeError.displayInToast &&
|
||||
toast.error(
|
||||
<>
|
||||
{bridgeError.message}
|
||||
<a href={bridgeError.link} target="_blank" className="ml-1 underline">
|
||||
here
|
||||
</a>
|
||||
</>,
|
||||
{
|
||||
autoClose: false,
|
||||
draggable: false,
|
||||
closeOnClick: false,
|
||||
style: { width: 500, marginLeft: -90 },
|
||||
},
|
||||
);
|
||||
}, [bridgeError]);
|
||||
|
||||
// Click on approve
|
||||
const onSubmit = async (data: BridgeForm) => {
|
||||
if (isLoading || isWaitingLoading) {
|
||||
return;
|
||||
}
|
||||
await switchChain();
|
||||
bridge(data.amount, parseEther(data.minFees), data.recipient);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
<div className="card-body">
|
||||
<h2 className="card-title mb-5 font-medium text-white">Bridge</h2>
|
||||
<ul className="space-y-6">
|
||||
<li>
|
||||
<FromChainToChain />
|
||||
</li>
|
||||
<li>
|
||||
<Amount tokensModalRef={tokensModalRef} />
|
||||
</li>
|
||||
<li className="text-end text-sm">
|
||||
<Balance />
|
||||
</li>
|
||||
<li>
|
||||
<Recipient />
|
||||
</li>
|
||||
<li>
|
||||
<div className="divider"></div>
|
||||
</li>
|
||||
<li className="text-sm">
|
||||
<Fees />
|
||||
</li>
|
||||
<li>
|
||||
<Submit isLoading={isLoading ? true : false} isWaitingLoading={isWaitingLoading} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
<TokenModal tokensModalRef={tokensModalRef} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { MdInfoOutline } from "react-icons/md";
|
||||
import classNames from "classnames";
|
||||
import { formatEther, parseEther, parseUnits } from "viem";
|
||||
import { useAccount, useBalance } from "wagmi";
|
||||
import { useApprove, useExecutionFee } from "@/hooks";
|
||||
import useBridge from "@/hooks/useBridge";
|
||||
import { NetworkLayer, TokenType } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import useMinimumFee from "@/hooks/useMinimumFee";
|
||||
|
||||
export default function Fees() {
|
||||
const [estimatedGasFee, setEstimatedGasFee] = useState<bigint>();
|
||||
|
||||
// Context
|
||||
const { tokenBridgeAddress, token, networkLayer, networkType, toChain, fromChain } = useChainStore((state) => ({
|
||||
tokenBridgeAddress: state.tokenBridgeAddress,
|
||||
token: state.token,
|
||||
networkLayer: state.networkLayer,
|
||||
networkType: state.networkType,
|
||||
toChain: state.toChain,
|
||||
fromChain: state.fromChain,
|
||||
}));
|
||||
|
||||
// Wagmi
|
||||
const { address, isConnected } = useAccount();
|
||||
const { data: ethBalance } = useBalance({
|
||||
address,
|
||||
chainId: fromChain?.id,
|
||||
});
|
||||
|
||||
// Form
|
||||
const { setValue, register, watch, setError, formState, clearErrors } = useFormContext();
|
||||
const { errors } = formState;
|
||||
const watchAmount = watch("amount", false);
|
||||
const bridgingAllowed = watch("bridgingAllowed", false);
|
||||
const claim = watch("claim", false);
|
||||
const balance = watch("balance", false);
|
||||
|
||||
// Hooks
|
||||
const { minimumFee } = useMinimumFee();
|
||||
const { estimateGasBridge } = useBridge();
|
||||
const { estimateApprove } = useApprove();
|
||||
const minFees = useExecutionFee({
|
||||
token,
|
||||
claim,
|
||||
networkLayer,
|
||||
networkType,
|
||||
minimumFee,
|
||||
});
|
||||
|
||||
// To check which estimateFee and to add the briging fee only if we are bridging
|
||||
const enoughAllowance = bridgingAllowed || token?.type === TokenType.ETH;
|
||||
|
||||
useEffect(() => {
|
||||
if (minFees) {
|
||||
setValue("minFees", minFees);
|
||||
}
|
||||
}, [minFees, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const estimate = async () => {
|
||||
let calculatedGasFee = BigInt(0);
|
||||
if (watchAmount && minimumFee !== null && token?.decimals) {
|
||||
if (enoughAllowance) {
|
||||
const bridgeGasFee = await estimateGasBridge(watchAmount, minimumFee);
|
||||
calculatedGasFee = bridgeGasFee || BigInt(0);
|
||||
} else {
|
||||
const approveGasFee = await estimateApprove(parseUnits(watchAmount, token.decimals), tokenBridgeAddress);
|
||||
calculatedGasFee = approveGasFee || BigInt(0);
|
||||
}
|
||||
}
|
||||
setEstimatedGasFee(calculatedGasFee);
|
||||
setValue("gasFees", calculatedGasFee);
|
||||
};
|
||||
!errors.amount && estimate();
|
||||
}, [
|
||||
watchAmount,
|
||||
minimumFee,
|
||||
estimateGasBridge,
|
||||
enoughAllowance,
|
||||
estimateApprove,
|
||||
tokenBridgeAddress,
|
||||
token,
|
||||
setValue,
|
||||
errors.amount,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (token?.type === TokenType.ETH && networkLayer === NetworkLayer.L1) {
|
||||
setValue("claim", "auto");
|
||||
} else {
|
||||
setValue("claim", "manual");
|
||||
}
|
||||
}, [token, setValue, networkLayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ethBalance && minFees != undefined && parseEther(minFees) > 0 && ethBalance.value <= parseEther(minFees)) {
|
||||
setError("minFees", {
|
||||
type: "custom",
|
||||
message: "Execution fees exceed ETH balance",
|
||||
});
|
||||
} else {
|
||||
clearErrors("minFees");
|
||||
}
|
||||
}, [setError, balance, minFees, clearErrors, ethBalance]);
|
||||
|
||||
return (
|
||||
<ul className="space-y-5">
|
||||
<li className="">
|
||||
<div className="form-control grid w-full grid-cols-2 gap-6">
|
||||
<div>
|
||||
<input
|
||||
{...register("claim")}
|
||||
id="claim-auto"
|
||||
type="radio"
|
||||
value="auto"
|
||||
className="peer hidden"
|
||||
disabled={token?.type !== TokenType.ETH}
|
||||
required
|
||||
/>
|
||||
|
||||
<label
|
||||
htmlFor="claim-auto"
|
||||
className={classNames("btn btn-outline normal-case font-normal w-full", {
|
||||
"btn-disabled": networkLayer === NetworkLayer.L2 || token?.type !== TokenType.ETH || !isConnected,
|
||||
|
||||
"peer-checked:btn-outline peer-checked:btn-primary": isConnected,
|
||||
})}
|
||||
>
|
||||
<div className="block">
|
||||
<div className="text-md w-full ">Automatic claiming</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input {...register("claim")} id="claim-manual" type="radio" value="manual" className="peer hidden" />
|
||||
<label
|
||||
htmlFor="claim-manual"
|
||||
className={classNames("btn btn-outline normal-case font-normal w-full", {
|
||||
"btn-disabled": !isConnected,
|
||||
"peer-checked:btn-outline peer-checked:btn-primary": isConnected,
|
||||
})}
|
||||
>
|
||||
<div className="block">
|
||||
<div className="text-md w-full">Manual claiming</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
className={classNames("flex flex-row justify-between", {
|
||||
"text-neutral-600": !isConnected,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Maximum execution fees :{" "}
|
||||
<div
|
||||
className="tooltip tooltip-bottom tooltip-info ml-1 "
|
||||
data-tip={
|
||||
claim === "auto"
|
||||
? "Automatic bridging: this fee is used to reimburse gas fees paid by the postman to execute the transaction on your behalf on the other chain. If gas fees are lower than the execution fees, the remaining amount will be reimbursed to the recipient address on the other chain."
|
||||
: "Manual bridging: user will need to claim the funds on the target chain (requires an address funded with ETH for gas fees)"
|
||||
}
|
||||
>
|
||||
<MdInfoOutline className="text-lg" />
|
||||
</div>
|
||||
</div>
|
||||
<span>{minFees} ETH</span>
|
||||
</div>
|
||||
|
||||
{errors.minFees && <div className="pt-2 text-right text-error">{errors.minFees.message?.toString()}</div>}
|
||||
</li>
|
||||
|
||||
<li
|
||||
className={classNames("flex flex-row justify-between", {
|
||||
"text-neutral-600": !isConnected,
|
||||
})}
|
||||
>
|
||||
<span>Estimated gas fees:</span>
|
||||
{isConnected && <span>{estimatedGasFee ? formatEther(estimatedGasFee) : ""} ETH</span>}
|
||||
</li>
|
||||
|
||||
{claim === "manual" && (
|
||||
<li
|
||||
className={classNames("justify-between", {
|
||||
"text-neutral-600": !isConnected,
|
||||
"text-white": isConnected,
|
||||
})}
|
||||
>
|
||||
You will have to{" "}
|
||||
<Link
|
||||
href="https://docs.linea.build/use-mainnet/bridges-of-linea"
|
||||
target="_blank"
|
||||
referrerPolicy="no-referrer"
|
||||
className={classNames("", {
|
||||
"text-neutral-600": !isConnected,
|
||||
"text-primary": isConnected,
|
||||
})}
|
||||
>
|
||||
claim assets manually
|
||||
</Link>{" "}
|
||||
once the transaction reaches the other layer.{" "}
|
||||
{networkLayer === NetworkLayer.L1
|
||||
? `This can take up to ~20min. You will need ETH on ${toChain?.name} to pay for gas fees.`
|
||||
: `This can take between 8 and 32 hours. You will need ETH on ${toChain?.name} to pay for gas fees.`}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { MdOutlineArrowRightAlt, MdOutlineCompareArrows } from "react-icons/md";
|
||||
import { useAccount } from "wagmi";
|
||||
import ChainLogo from "@/components/widgets/ChainLogo";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
export default function FromChainToChain() {
|
||||
// Context
|
||||
const { fromChain, toChain, switchChain, resetToken } = useChainStore((state) => ({
|
||||
fromChain: state.fromChain,
|
||||
toChain: state.toChain,
|
||||
switchChain: state.switchChain,
|
||||
resetToken: state.resetToken,
|
||||
}));
|
||||
|
||||
// Hooks
|
||||
const { isConnected } = useAccount();
|
||||
|
||||
/**
|
||||
* Swith chain
|
||||
*/
|
||||
const switchChainHandler = () => {
|
||||
resetToken();
|
||||
switchChain();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between space-x-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
{isConnected && (
|
||||
<>
|
||||
{fromChain && <ChainLogo chainId={fromChain.id} />} <span>{fromChain && fromChain.name}</span>
|
||||
<div>
|
||||
<MdOutlineArrowRightAlt />
|
||||
</div>
|
||||
{toChain && <ChainLogo chainId={toChain.id} />} <span>{toChain && toChain.name}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
id="switch-chain-btn"
|
||||
className="btn btn-circle btn-info btn-sm"
|
||||
type="button"
|
||||
onClick={switchChainHandler}
|
||||
disabled={!isConnected}
|
||||
>
|
||||
<MdOutlineCompareArrows className="text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import classNames from "classnames";
|
||||
import ApproveERC20 from "./ApproveERC20";
|
||||
import useBridge from "@/hooks/useBridge";
|
||||
import { NetworkLayer } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
isWaitingLoading: boolean;
|
||||
}
|
||||
|
||||
export default function Submit({ isLoading = false, isWaitingLoading = false }: Props) {
|
||||
// Form
|
||||
const { watch, formState } = useFormContext();
|
||||
const { errors } = formState;
|
||||
|
||||
const watchAmount = watch("amount", false);
|
||||
const watchAllowance = watch("allowance", false);
|
||||
|
||||
// Wagmi
|
||||
const { bridgeEnabled } = useBridge();
|
||||
|
||||
// Context
|
||||
const { token, networkLayer } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
networkLayer: state.networkLayer,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
{token && networkLayer !== NetworkLayer.UNKNOWN && token[networkLayer] ? (
|
||||
<div className="flex flex-row justify-between">
|
||||
<ApproveERC20 />
|
||||
<button
|
||||
id="submit-erc-btn"
|
||||
className={classNames("btn btn-primary w-48 rounded-full uppercase", {
|
||||
"cursor-wait": isLoading || isWaitingLoading,
|
||||
"btn-disabled": !bridgeEnabled(watchAmount, watchAllowance, errors),
|
||||
})}
|
||||
type="submit"
|
||||
>
|
||||
{(isLoading || isWaitingLoading) && <span className="loading loading-spinner"></span>}
|
||||
Start bridging
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
id="submit-eth-btn"
|
||||
className={classNames("btn w-full btn-primary rounded-full uppercase", {
|
||||
"cursor-wait": isLoading || isWaitingLoading,
|
||||
"btn-disabled": !bridgeEnabled(watchAmount, BigInt(0), errors),
|
||||
})}
|
||||
type="submit"
|
||||
>
|
||||
{(isLoading || isWaitingLoading) && <span className="loading loading-spinner"></span>}
|
||||
Start bridging
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
bridge-ui/src/components/bridge/modals/ManualClaimModal.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Link from "next/link";
|
||||
import Button from "../Button";
|
||||
import { MdCallMade } from "react-icons/md";
|
||||
|
||||
type ManualClaimModalProps = {
|
||||
handleNoClose: () => void;
|
||||
handleYesClose: () => void;
|
||||
};
|
||||
|
||||
const ManualClaimModal: React.FC<ManualClaimModalProps> = ({ handleNoClose, handleYesClose }) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-8 px-8">
|
||||
<h2 className="text-xl">Manual claim on destination</h2>
|
||||
<div className="space-y-5 text-center">
|
||||
<div className="text-sm">
|
||||
Activating Manual claim means you will need to claim the message on the destination chain with ETH with a
|
||||
second transaction, once the first transaction has completed.
|
||||
</div>
|
||||
<div className="text-sm">Are you sure you want to enable manual claim?</div>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button onClick={handleYesClose}>Yes</Button>
|
||||
<Button variant="outline" size="md" className="border-primary px-4" onClick={handleNoClose}>
|
||||
No
|
||||
</Button>
|
||||
<Link
|
||||
href="#"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="hover:border-b-1 border-b-1 btn btn-ghost btn-sm rounded-none border-b-primary p-0 font-normal text-[#E5E5E5] hover:border-b-primary hover:bg-transparent"
|
||||
>
|
||||
More Info
|
||||
<MdCallMade color="white" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManualClaimModal;
|
||||
@@ -0,0 +1,27 @@
|
||||
import Link from "next/link";
|
||||
|
||||
type TransactionConfirmationModalProps = {
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const TransactionConfirmationModal: React.FC<TransactionConfirmationModalProps> = ({ handleClose }) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-8 px-8 py-4">
|
||||
<h2 className="text-xl">Trandaction confirmed!</h2>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Link href="/" className="btn btn-primary rounded-full uppercase" onClick={handleClose}>
|
||||
Start a new transaction
|
||||
</Link>
|
||||
<Link
|
||||
className="border-1 btn btn-outline rounded-full border-primary uppercase text-white hover:border-primary hover:bg-cardBg hover:text-white"
|
||||
href="/transactions"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Track transactions
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionConfirmationModal;
|
||||
@@ -1,111 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useBlockNumber } from "wagmi";
|
||||
import { toast } from "react-toastify";
|
||||
import { Variants } from "framer-motion";
|
||||
|
||||
import HistoryItem from "./HistoryItem";
|
||||
import useFetchHistory from "@/hooks/useFetchHistory";
|
||||
|
||||
export type BridgeForm = {
|
||||
amount: string;
|
||||
};
|
||||
|
||||
const variants: Variants = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function History() {
|
||||
const clearHistoryModalRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
// Wagmi
|
||||
const { data: blockNumber } = useBlockNumber({
|
||||
watch: true,
|
||||
});
|
||||
|
||||
// Context
|
||||
const { transactions, fetchHistory, isLoading, clearHistory } = useFetchHistory();
|
||||
|
||||
useEffect(() => {
|
||||
if (blockNumber && blockNumber % BigInt(2) === BigInt(0)) {
|
||||
fetchHistory();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockNumber]);
|
||||
|
||||
return (
|
||||
<div className="card w-full bg-base-100 shadow-xl md:w-[500px]">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title mb-5 justify-between font-medium text-white">
|
||||
<span>Recent transactions</span>
|
||||
<span className="text-sm font-normal">({transactions.length})</span>
|
||||
</h2>
|
||||
<div
|
||||
className="max-h-[545px] overflow-y-auto pr-4 scrollbar-thin scrollbar-track-gray-700 scrollbar-thumb-gray-500"
|
||||
id="transactions-list"
|
||||
>
|
||||
{transactions.map((transaction) => (
|
||||
<HistoryItem key={transaction.transactionHash} transaction={transaction} variants={variants} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!transactions.length && (
|
||||
<div className="mb-5 flex h-16 w-full flex-row items-center justify-center text-center text-sm italic">
|
||||
<span>No recent transactions</span>
|
||||
{isLoading ? <div className="loading loading-dots loading-xs mb-3 mt-6"></div> : <span>.</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
id="reload-history-btn"
|
||||
className="btn-link btn-sm font-light normal-case text-gray-200 no-underline opacity-60 hover:text-primary hover:opacity-100"
|
||||
onClick={() => clearHistoryModalRef.current?.showModal()}
|
||||
>
|
||||
Reload history
|
||||
</button>
|
||||
</div>
|
||||
<dialog ref={clearHistoryModalRef} id="clear_history_modal" className="modal">
|
||||
<form method="dialog" className="modal-box">
|
||||
<h3 className="text-lg font-bold">Are you sure?</h3>
|
||||
<p className="py-4">
|
||||
This might be necessary if you're encountering issues and need to reset the synchronization process.
|
||||
However, rebuilding your history could take a considerable amount of time.
|
||||
</p>
|
||||
<p className="py-4">Proceed with caution and only if absolutely necessary.</p>
|
||||
<div className="modal-action justify-between">
|
||||
<button
|
||||
id="reload-history-confirm-btn"
|
||||
className="btn btn-warning btn-sm rounded-full"
|
||||
onClick={() => {
|
||||
clearHistory();
|
||||
clearHistoryModalRef.current?.close();
|
||||
toast.success("History cleared");
|
||||
}}
|
||||
>
|
||||
Reload history
|
||||
</button>
|
||||
<button
|
||||
id="reload-history-cancel-btn"
|
||||
className="btn btn-sm rounded-full"
|
||||
onClick={() => clearHistoryModalRef.current?.close()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form method="dialog" className="modal-backdrop">
|
||||
<button id="close-history-reload-btn">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { format, fromUnixTime } from "date-fns";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import { MdOutlineArrowRightAlt } from "react-icons/md";
|
||||
import { Address, formatUnits, zeroAddress } from "viem";
|
||||
|
||||
import ChainLogo from "@/components/widgets/ChainLogo";
|
||||
import { formatAddress, safeGetAddress } from "@/utils/format";
|
||||
import { TransactionHistory } from "@/models/history";
|
||||
import TransactionClaimButton from "../transactions/modals/TransactionClaimButton";
|
||||
|
||||
interface Props {
|
||||
transaction: TransactionHistory;
|
||||
variants: Variants;
|
||||
}
|
||||
export default function HistoryItem({ transaction, variants }: Props) {
|
||||
let destinationAddress: Address | null = null;
|
||||
const tokenAddress = safeGetAddress(transaction.tokenAddress);
|
||||
const l1Address = safeGetAddress(transaction.token.L1);
|
||||
const l2Address = safeGetAddress(transaction.token.L2);
|
||||
|
||||
if (tokenAddress && tokenAddress === l1Address) {
|
||||
destinationAddress = transaction.token.L2;
|
||||
} else if (tokenAddress && tokenAddress === l2Address) {
|
||||
destinationAddress = transaction.token.L1;
|
||||
}
|
||||
|
||||
const getL1Token = () => {
|
||||
if (transaction.tokenAddress && transaction.tokenAddress !== zeroAddress) {
|
||||
return (
|
||||
<Link
|
||||
href={`${transaction.fromChain.blockExplorers?.default.url}/address/${transaction.tokenAddress}`}
|
||||
target={"_blank"}
|
||||
className="link text-xs font-bold"
|
||||
passHref
|
||||
>
|
||||
{transaction.token.symbol}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return <span className="font-bold">{transaction.token.symbol}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const getL2Token = () => {
|
||||
if (destinationAddress && destinationAddress !== zeroAddress) {
|
||||
return (
|
||||
<Link
|
||||
href={`${transaction.toChain.blockExplorers?.default.url}/address/${destinationAddress}`}
|
||||
target={"_blank"}
|
||||
className="link text-xs font-bold"
|
||||
passHref
|
||||
>
|
||||
{transaction.token.symbol}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return <span className="font-bold">{transaction.token.symbol}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.ul className="space-y-2" variants={variants} initial="hidden" animate="show">
|
||||
<li className="flex flex-row justify-between">
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
<ChainLogo chainId={transaction.fromChain.id} />
|
||||
<span className="flex flex-row">{transaction.fromChain.name}</span>
|
||||
<div>
|
||||
<MdOutlineArrowRightAlt className="mx-1" />
|
||||
</div>
|
||||
<ChainLogo chainId={transaction.toChain.id} />
|
||||
<span className="flex flex-row">{transaction.toChain.name}</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
{transaction && format(fromUnixTime(parseInt(transaction.timestamp.toString())), "LLL dd yyyy, HH:mm")}
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex flex-row items-center justify-between text-xs">
|
||||
<div className="space-x-1">
|
||||
<span>{formatUnits(transaction.amount, transaction.token.decimals)}</span>
|
||||
<span>
|
||||
{transaction.fromChain.name} {getL1Token()}
|
||||
</span>
|
||||
<span>to</span>
|
||||
<span>
|
||||
{transaction.toChain.name} {getL2Token()}
|
||||
</span>
|
||||
<span>To</span>
|
||||
<span>
|
||||
<Link
|
||||
href={`${transaction.toChain.blockExplorers?.default.url}/address/${transaction.recipient}`}
|
||||
target={"_blank"}
|
||||
className="link"
|
||||
passHref
|
||||
>
|
||||
{formatAddress(transaction.recipient)}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li className="space-x-1 text-xs">
|
||||
<span>{transaction.fromChain.name} transaction:</span>
|
||||
<Link
|
||||
href={`${transaction.fromChain.blockExplorers?.default.url}/tx/${transaction.transactionHash}`}
|
||||
target={"_blank"}
|
||||
className="link"
|
||||
passHref
|
||||
>
|
||||
{formatAddress(transaction.transactionHash)}
|
||||
</Link>
|
||||
</li>
|
||||
{transaction.messages?.map((message) => {
|
||||
return <TransactionClaimButton key={message.messageHash} message={message} transaction={transaction} />;
|
||||
})}
|
||||
<li className="divider"></li>
|
||||
</motion.ul>
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,8 @@
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import atypTextFont from "@/app/font/atypText";
|
||||
import atypFont from "@/app/font/atyp";
|
||||
import Header from "./header/Header";
|
||||
import SwitchNetwork from "../widgets/SwitchNetwork";
|
||||
import useInitialiseChain from "@/hooks/useInitialiseChain";
|
||||
import useInitialiseToken from "@/hooks/useInitialiseToken";
|
||||
import { Header } from "./header";
|
||||
import { useInitialiseChain, useInitialiseToken } from "@/hooks";
|
||||
import Sidebar from "./Sidebar";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
@@ -28,10 +26,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<div className="md:ml-64">
|
||||
<Header />
|
||||
</div>
|
||||
<main className="m-0 flex-1 p-10 md:ml-64">
|
||||
{children}
|
||||
<SwitchNetwork />
|
||||
</main>
|
||||
<main className="m-0 flex-1 p-3 md:ml-64 md:p-10">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,77 +1,35 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { MENU_ITEMS } from "@/utils/constants";
|
||||
import { MdOutlineClose } from "react-icons/md";
|
||||
import Button from "../bridge/Button";
|
||||
import { SocialLinks, FooterLinks } from "./footer";
|
||||
import { Menu } from "./menu";
|
||||
|
||||
type MobileMenuProps = {
|
||||
toggleMenu: () => void;
|
||||
};
|
||||
|
||||
export default function MobileMenu({ toggleMenu }: MobileMenuProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-[#121212] p-8 md:hidden">
|
||||
<div className="fixed inset-0 z-50 flex flex-col bg-[#121212] px-8 py-4 md:hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<Image src={"/images/logo/linea.svg"} alt="Linea logo" width={95} height={45} />
|
||||
<button onClick={toggleMenu} className="btn btn-circle btn-ghost">
|
||||
<Image
|
||||
src={"/images/logo/linea.svg"}
|
||||
alt="Linea logo"
|
||||
width={0}
|
||||
height={0}
|
||||
priority
|
||||
style={{ width: "auto", height: "auto" }}
|
||||
/>
|
||||
<Button onClick={toggleMenu} className="btn-circle btn-ghost">
|
||||
<MdOutlineClose size="2em" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-5 flex h-full flex-col justify-between overflow-y-auto py-4">
|
||||
<div>
|
||||
<ul className="space-y-2 font-medium">
|
||||
{MENU_ITEMS.map(({ title, href, external, Icon }) => (
|
||||
<li key={title}>
|
||||
{external ? (
|
||||
<Link
|
||||
href={href}
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 py-4"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<Icon alt={title} width={30} height={30} />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
className={`flex items-center gap-2 py-3 ${pathname === href ? "text-primary" : ""}`}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<Icon alt={title} width={30} height={30} />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-2 py-8 font-medium">
|
||||
<Link
|
||||
className="flex items-center hover:text-primary"
|
||||
href="#"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
Contact Support
|
||||
</Link>
|
||||
<Link
|
||||
className="flex items-center hover:text-primary"
|
||||
href="https://linea.build/terms-of-service"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
Terms of service
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-5 flex-1 overflow-y-auto">
|
||||
<Menu toggleMenu={toggleMenu} />
|
||||
</div>
|
||||
<div>
|
||||
<FooterLinks toggleMenu={toggleMenu} />
|
||||
<SocialLinks />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import classNames from "classnames";
|
||||
import { useContext } from "react";
|
||||
import { ModalContext } from "@/contexts/modal.context";
|
||||
import { MdOutlineClose } from "react-icons/md";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
type ModalProps = Record<string, never>;
|
||||
|
||||
@@ -13,13 +13,15 @@ const Modal: React.FC<ModalProps> = () => {
|
||||
const width = options?.width ? options.width : "w-screen md:w-[600px]";
|
||||
|
||||
return (
|
||||
<dialog ref={ref} className="modal">
|
||||
<div className={classNames("modal-box bg-cardBg border-card border-2", width)}>
|
||||
<form method="dialog">
|
||||
<button className="btn btn-circle btn-ghost btn-sm absolute right-2 top-2">
|
||||
<MdOutlineClose className="text-lg" />
|
||||
</button>
|
||||
</form>
|
||||
<dialog ref={ref} className="modal" onClose={options?.onClose}>
|
||||
<div className={cn("modal-box px-0 bg-cardBg border-card border-2", width)}>
|
||||
{options?.showCloseButton && (
|
||||
<form method="dialog">
|
||||
<button className="btn btn-circle btn-ghost btn-sm absolute right-2 top-2">
|
||||
<MdOutlineClose className="text-lg" />
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{modalContent}
|
||||
</div>
|
||||
<form method="dialog" className="modal-backdrop">
|
||||
|
||||
@@ -1,66 +1,32 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { MENU_ITEMS } from "@/utils/constants";
|
||||
import { Menu } from "./menu";
|
||||
import { FooterLinks, SocialLinks } from "./footer";
|
||||
|
||||
export default function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside id="sidebar" className="fixed left-0 top-0 z-40 hidden h-screen w-52 md:block" aria-label="Sidebar">
|
||||
<div className="flex h-full flex-col justify-between overflow-y-auto bg-cardBg py-4">
|
||||
<div>
|
||||
<div className="flex h-24 items-center p-4">
|
||||
<Link href="/">
|
||||
<Image src={"/images/logo/linea.svg"} alt="Linea logo" width={95} height={45} />
|
||||
<Image
|
||||
src={"/images/logo/linea.svg"}
|
||||
alt="Linea logo"
|
||||
width={0}
|
||||
height={0}
|
||||
priority
|
||||
style={{ width: "auto", height: "auto" }}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<ul className="space-y-2 font-medium">
|
||||
{MENU_ITEMS.map(({ title, href, external, Icon }) => (
|
||||
<li key={title}>
|
||||
{external ? (
|
||||
<Link
|
||||
href={href}
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-4"
|
||||
>
|
||||
<Icon alt={title} width={30} height={30} />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href={href}
|
||||
className={`flex items-center gap-2 p-3 ${pathname === href ? "border-r-2 border-primary text-primary" : ""}`}
|
||||
>
|
||||
<Icon alt={title} width={30} height={30} />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="pl-4">
|
||||
<Menu border />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 p-4 font-medium">
|
||||
<Link
|
||||
className="flex items-center hover:text-primary"
|
||||
href="#"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Contact Support
|
||||
</Link>
|
||||
<Link
|
||||
className="flex items-center hover:text-primary"
|
||||
href="https://linea.build/terms-of-service"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Terms of service
|
||||
</Link>
|
||||
<div className="px-4">
|
||||
<FooterLinks />
|
||||
<SocialLinks />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
40
bridge-ui/src/components/layouts/footer/FooterLinks.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Link from "next/link";
|
||||
|
||||
type FooterLinksProps = {
|
||||
toggleMenu?: () => void;
|
||||
};
|
||||
|
||||
export const FooterLinks = ({ toggleMenu }: FooterLinksProps) => (
|
||||
<div className="space-y-2 py-4 font-medium">
|
||||
<Link
|
||||
className="flex items-center hover:text-primary"
|
||||
href="https://support.linea.build/"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
Contact Support
|
||||
</Link>
|
||||
<Link
|
||||
className="flex items-center hover:text-primary"
|
||||
href="https://linea.build/privacy-policy"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
className="flex items-center hover:text-primary"
|
||||
href="https://linea.build/terms-of-service"
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
Terms of service
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
19
bridge-ui/src/components/layouts/footer/SocialLinks.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export const SocialLinks = () => (
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<Link href={"https://linea.mirror.xyz/"} passHref target="_blank" rel="noopener noreferrer">
|
||||
<Image src={"/images/logo/sidebar/linea-mirror.svg"} alt="Linea logo" width={19} height={18} />
|
||||
</Link>
|
||||
<Link href={"https://x.com/LineaBuild"} passHref target="_blank" rel="noopener noreferrer">
|
||||
<Image src={"/images/logo/sidebar/twitter.svg"} alt="Linea logo" width={20} height={16} />
|
||||
</Link>
|
||||
<Link href={"https://www.youtube.com/@LineaBuild"} passHref target="_blank" rel="noopener noreferrer">
|
||||
<Image src={"/images/logo/sidebar/youtube.svg"} alt="Linea logo" width={24} height={16} />
|
||||
</Link>
|
||||
<Link href={"https://discord.gg/linea"} passHref target="_blank" rel="noopener noreferrer">
|
||||
<Image src={"/images/logo/sidebar/discord.svg"} alt="Linea logo" width={22} height={16} />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
2
bridge-ui/src/components/layouts/footer/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { FooterLinks } from "./FooterLinks";
|
||||
export { SocialLinks } from "./SocialLinks";
|
||||
@@ -1,97 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { switchChain } from "@wagmi/core";
|
||||
import log from "loglevel";
|
||||
import ChainLogo from "@/components/widgets/ChainLogo";
|
||||
import { NetworkType, wagmiConfig } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
export default function Chains() {
|
||||
// Context
|
||||
const { networkType, fromChain, toChain, resetToken } = useChainStore((state) => ({
|
||||
networkType: state.networkType,
|
||||
fromChain: state.fromChain,
|
||||
toChain: state.toChain,
|
||||
resetToken: state.resetToken,
|
||||
}));
|
||||
|
||||
const detailsRef = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
const switchNetwork = async () => {
|
||||
try {
|
||||
resetToken();
|
||||
toChain &&
|
||||
(await switchChain(wagmiConfig, {
|
||||
chainId: toChain.id,
|
||||
}));
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (detailsRef.current && !detailsRef.current.contains(e.target as Node)) {
|
||||
detailsRef.current.removeAttribute("open");
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (networkType === NetworkType.WRONG_NETWORK) {
|
||||
return (
|
||||
<details ref={detailsRef}>
|
||||
<summary>Wrong Network</summary>
|
||||
<ul className="right-0 z-10 bg-base-100 p-2">
|
||||
<li>
|
||||
<button id="switch-active-chain-btn" onClick={switchNetwork}>
|
||||
<Image
|
||||
src={"/images/logo/ethereum.svg"}
|
||||
alt="Linea"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "12px", height: "auto" }}
|
||||
/>{" "}
|
||||
{fromChain && fromChain.name}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button id="switch-alternative-chain-btn" onClick={switchNetwork}>
|
||||
<Image
|
||||
src={"/images/logo/linea-sepolia.svg"}
|
||||
alt="Linea"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>{" "}
|
||||
{toChain && toChain.name}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<details ref={detailsRef}>
|
||||
<summary className="rounded-full" id="chain-select">
|
||||
{fromChain && <ChainLogo chainId={fromChain.id} />}{" "}
|
||||
<span className="hidden md:block" id="active-chain-name">
|
||||
{fromChain && fromChain.name}
|
||||
</span>
|
||||
</summary>
|
||||
<ul className="right-0 z-10 bg-base-100 p-2">
|
||||
<li>
|
||||
<button id="switch-alternative-chain-btn" onClick={switchNetwork} className="min-w-max rounded-full">
|
||||
{toChain && <ChainLogo chainId={toChain.id} />} {toChain && toChain.name}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useAccount } from "wagmi";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MdMenu } from "react-icons/md";
|
||||
import Wallet from "./Wallet";
|
||||
import Chains from "./Chains";
|
||||
import MobileMenu from "../MobileMenu";
|
||||
import Button from "@/components/bridge/Button";
|
||||
import { HeaderLogo } from "./HeaderLogo";
|
||||
import { NavMenu } from "./NavMenu";
|
||||
|
||||
function formatPath(pathname: string): string {
|
||||
switch (pathname) {
|
||||
case "/":
|
||||
case "":
|
||||
return "Bridge";
|
||||
case "/transactions":
|
||||
return "Transactions";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
export function Header() {
|
||||
// Hooks
|
||||
const { isConnected } = useAccount();
|
||||
const pathname = usePathname();
|
||||
@@ -38,23 +26,15 @@ export default function Header() {
|
||||
}, [isMenuOpen]);
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between gap-3 p-10">
|
||||
<Image src={"/images/logo/linea.svg"} alt="Linea logo" width={95} height={45} className="md:hidden" />
|
||||
<h1 className="hidden text-4xl md:flex">{formatPath(pathname)}</h1>
|
||||
<header className="navbar flex items-center justify-between gap-3 p-3 md:p-10">
|
||||
<HeaderLogo pathname={pathname} />
|
||||
<NavMenu isConnected={isConnected} />
|
||||
|
||||
<div className="flex items-center">
|
||||
<ul className="menu menu-horizontal m-0">
|
||||
{isConnected && (
|
||||
<li>
|
||||
<Chains />
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<Wallet />
|
||||
</li>
|
||||
</ul>
|
||||
<button onClick={toggleMenu} className="btn btn-circle btn-ghost md:hidden">
|
||||
<div className="m-0"></div>
|
||||
<Button onClick={toggleMenu} className="btn-circle btn-ghost md:hidden">
|
||||
<MdMenu size="2em" />
|
||||
</button>
|
||||
</Button>
|
||||
{isMenuOpen && <MobileMenu toggleMenu={toggleMenu} />}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
32
bridge-ui/src/components/layouts/header/HeaderLogo.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import Image from "next/image";
|
||||
|
||||
type HeaderLogoProps = {
|
||||
pathname: string;
|
||||
};
|
||||
|
||||
function formatPath(pathname: string): string {
|
||||
switch (pathname) {
|
||||
case "/":
|
||||
case "":
|
||||
return "Bridge";
|
||||
case "/transactions":
|
||||
return "Transactions";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export const HeaderLogo: React.FC<HeaderLogoProps> = ({ pathname }) => (
|
||||
<div className="flex-1">
|
||||
<Image
|
||||
src={"/images/logo/linea.svg"}
|
||||
alt="Linea logo"
|
||||
width={0}
|
||||
height={0}
|
||||
className="md:hidden"
|
||||
priority
|
||||
style={{ width: "auto", height: "auto" }}
|
||||
/>
|
||||
<h1 className="hidden text-4xl md:flex">{formatPath(pathname)}</h1>
|
||||
</div>
|
||||
);
|
||||
20
bridge-ui/src/components/layouts/header/NavMenu.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Chains, Wallet } from "./dropdowns";
|
||||
|
||||
type NavMenuProps = {
|
||||
isConnected: boolean;
|
||||
};
|
||||
|
||||
export const NavMenu: React.FC<NavMenuProps> = ({ isConnected }) => (
|
||||
<div className="flex-none">
|
||||
<ul className="menu menu-horizontal gap-2 px-1">
|
||||
{isConnected && (
|
||||
<li>
|
||||
<Chains />
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<Wallet />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
80
bridge-ui/src/components/layouts/header/dropdowns/Chains.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { switchChain } from "@wagmi/core";
|
||||
import log from "loglevel";
|
||||
import { config, NetworkLayer, NetworkType, wagmiConfig } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import DropdownItem from "@/components/DropdownItem";
|
||||
|
||||
const networks = config.networks;
|
||||
|
||||
export function Chains() {
|
||||
const { networkType, networkLayer, resetToken } = useChainStore((state) => ({
|
||||
networkType: state.networkType,
|
||||
networkLayer: state.networkLayer,
|
||||
resetToken: state.resetToken,
|
||||
}));
|
||||
|
||||
const detailsRef = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (detailsRef.current && !detailsRef.current.contains(e.target as Node)) {
|
||||
detailsRef.current.removeAttribute("open");
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const switchNetworkHandler = async (chainId: number) => {
|
||||
if (networkLayer === NetworkLayer.UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resetToken();
|
||||
await switchChain(wagmiConfig, {
|
||||
chainId,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (networkType == NetworkType.SEPOLIA || networkType == NetworkType.MAINNET) {
|
||||
return (
|
||||
<details className="dropdown relative" ref={detailsRef}>
|
||||
<summary className="flex cursor-pointer items-center gap-2 rounded-full border-2 border-card p-2 px-3">
|
||||
<Image
|
||||
src={
|
||||
networkType === NetworkType.SEPOLIA ? "/images/logo/linea-sepolia.svg" : "/images/logo/linea-mainnet.svg"
|
||||
}
|
||||
alt="MetaMask"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
|
||||
<span className="hidden md:block">{networkType === NetworkType.SEPOLIA ? "Linea Sepolia" : "Linea"}</span>
|
||||
</summary>
|
||||
<ul className="menu dropdown-content absolute right-0 z-10 mt-2 min-w-max border-2 border-card bg-cardBg p-0 shadow">
|
||||
<DropdownItem
|
||||
title={config.networks.MAINNET.L2.name}
|
||||
iconPath="/images/logo/linea-mainnet.svg"
|
||||
onClick={() => switchNetworkHandler(networks.MAINNET.L1.chainId)}
|
||||
/>
|
||||
<DropdownItem
|
||||
title={config.networks.SEPOLIA.L2.name}
|
||||
iconPath="/images/logo/linea-sepolia.svg"
|
||||
onClick={() => switchNetworkHandler(networks.SEPOLIA.L1.chainId)}
|
||||
/>
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,21 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { useAccount, useDisconnect } from "wagmi";
|
||||
import { MdLogout } from "react-icons/md";
|
||||
import { formatAddress } from "@/utils/format";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import DropdownItem from "@/components/DropdownItem";
|
||||
import ConnectButton from "@/components/ConnectButton";
|
||||
import { formatAddress } from "@/utils/format";
|
||||
|
||||
export default function Wallet() {
|
||||
const detailsRef = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
export function Wallet() {
|
||||
const { address, isConnected } = useAccount();
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
const { fromChain } = useChainStore((state) => ({
|
||||
fromChain: state.fromChain,
|
||||
}));
|
||||
|
||||
const detailsRef = useRef<HTMLDetailsElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (detailsRef.current && !detailsRef.current.contains(e.target as Node)) {
|
||||
@@ -25,10 +30,20 @@ export default function Wallet() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!address) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(address);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
if (isConnected) {
|
||||
return (
|
||||
<details ref={detailsRef}>
|
||||
<summary className="rounded-full">
|
||||
<details className="dropdown relative" ref={detailsRef}>
|
||||
<summary className="flex cursor-pointer items-center gap-2 rounded-full border-2 border-card p-2 px-3">
|
||||
<Image
|
||||
src={"/images/logo/metamask.svg"}
|
||||
alt="MetaMask"
|
||||
@@ -36,18 +51,15 @@ export default function Wallet() {
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
|
||||
<span className="hidden md:block">{formatAddress(address)}</span>
|
||||
</summary>
|
||||
<ul className="right-0 z-10 bg-base-100 p-2">
|
||||
<li>
|
||||
<button id="wallet-disconnect-btn" onClick={() => disconnect()} className="rounded-full">
|
||||
<MdLogout className="text-xl" />
|
||||
Disconnect
|
||||
</button>
|
||||
</li>
|
||||
<ul className="menu dropdown-content absolute right-0 z-10 mt-2 min-w-max border-2 border-card bg-cardBg p-0 shadow">
|
||||
<DropdownItem title="Copy address" onClick={handleCopy} />
|
||||
<DropdownItem title="Explorer" externalLink={fromChain?.blockExplorers?.default.url} />
|
||||
<DropdownItem title="Logout" onClick={() => disconnect()} />
|
||||
</ul>
|
||||
</details>
|
||||
//
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { Chains } from "./Chains";
|
||||
export { Wallet } from "./Wallet";
|
||||
3
bridge-ui/src/components/layouts/header/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Header } from "./Header";
|
||||
export { HeaderLogo } from "./HeaderLogo";
|
||||
export { NavMenu } from "./NavMenu";
|
||||
17
bridge-ui/src/components/layouts/menu/Menu.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MENU_ITEMS } from "@/utils/constants";
|
||||
import { MenuItem } from "./MenuItem";
|
||||
|
||||
type MenuProps = {
|
||||
toggleMenu?: () => void;
|
||||
border?: boolean;
|
||||
};
|
||||
|
||||
export const Menu: React.FC<MenuProps> = ({ toggleMenu, border }) => {
|
||||
return (
|
||||
<ul className="space-y-2 font-medium">
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<MenuItem key={item.title} {...item} toggleMenu={toggleMenu} border={border} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
34
bridge-ui/src/components/layouts/menu/MenuItem.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { cn } from "@/utils/cn";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
type MenuItemProps = {
|
||||
title: string;
|
||||
href: string;
|
||||
external: boolean;
|
||||
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||
toggleMenu?: () => void;
|
||||
border?: boolean;
|
||||
};
|
||||
|
||||
export const MenuItem = ({ title, href, external, Icon, toggleMenu, border }: MenuItemProps) => {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<li key={title}>
|
||||
<Link
|
||||
href={href}
|
||||
passHref={external}
|
||||
target={external ? "_blank" : undefined}
|
||||
rel={external ? "noopener noreferrer" : undefined}
|
||||
className={cn("flex items-center gap-2 py-3", {
|
||||
"text-primary": pathname === href,
|
||||
"border-r-2 border-primary": pathname === href && border,
|
||||
})}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<Icon width={30} height={30} />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
2
bridge-ui/src/components/layouts/menu/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Menu } from "./Menu";
|
||||
export { MenuItem } from "./MenuItem";
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import classNames from "classnames";
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { cn } from "@/utils/cn";
|
||||
import Button from "../bridge/Button";
|
||||
|
||||
export default function TermsModal() {
|
||||
const termsModalRef = useRef<HTMLDivElement>(null);
|
||||
@@ -43,7 +44,7 @@ export default function TermsModal() {
|
||||
<div
|
||||
ref={termsModalRef}
|
||||
id="terms_modal"
|
||||
className={classNames(
|
||||
className={cn(
|
||||
"p-4 fixed right-2 left-2 md:left-auto md:right-5 md:max-w-[20rem] bg-white rounded text-black z-50 transition-all duration-500",
|
||||
!open ? "invisible -bottom-full" : "visible bottom-2 md:bottom-16",
|
||||
)}
|
||||
@@ -75,14 +76,16 @@ export default function TermsModal() {
|
||||
(Terms of Service | Linea )
|
||||
</Link>{" "}
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
id="agree-terms-btn"
|
||||
onClick={handleAgreeToTerms}
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm mt-3 w-full rounded-full font-medium uppercase"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="mt-3 w-full font-medium"
|
||||
>
|
||||
Got It
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
export type ToolTipProps = {
|
||||
text: string;
|
||||
position?: "top" | "bottom";
|
||||
align?: "left" | "right" | "center";
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ToolTip: React.FC<ToolTipProps> = ({ text, children, position = "top", align = "right", className }) => {
|
||||
return (
|
||||
<div className="group relative">
|
||||
{children}
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute px-2.5 py-2 bg-[#1D1D1D] text-[#C0C0C0] normal-case font-normal text-xs md:text-[0.8125rem] border rounded-sm border-primary min-w-40",
|
||||
"group-hover:scale-100 scale-0 transition-all duration-200 ease-in-out z-10 opacity-0 group-hover:opacity-100",
|
||||
{
|
||||
"bottom-full": position === "top",
|
||||
"top-full": position === "bottom",
|
||||
"right-0": align === "left",
|
||||
"left-full": align === "right",
|
||||
"left-1/2 -translate-x-1/2": align === "center",
|
||||
"origin-bottom-left": position === "top" && align === "right",
|
||||
"origin-bottom-right": position === "top" && align === "left",
|
||||
"origin-top-left": position === "bottom" && align === "right",
|
||||
"origin-top-right": position === "bottom" && align === "left",
|
||||
"origin-bottom": position === "top" && align === "center",
|
||||
"origin-top": position === "bottom" && align === "center",
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolTip;
|
||||
27
bridge-ui/src/components/tooltip/TooltipComponent.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/utils/cn";
|
||||
|
||||
interface TooltipProps {
|
||||
text: string;
|
||||
position?: "top" | "right" | "bottom" | "left";
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = ({ text, position = "top", children, className }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"tooltip",
|
||||
{
|
||||
"tooltip-top": position === "top",
|
||||
"tooltip-right": position === "right",
|
||||
"tooltip-bottom": position === "bottom",
|
||||
"tooltip-left": position === "left",
|
||||
},
|
||||
className,
|
||||
)}
|
||||
data-tip={text}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
1
bridge-ui/src/components/tooltip/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { Tooltip } from "./TooltipComponent";
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useContext } from "react";
|
||||
import { formatEther } from "viem";
|
||||
import { formatUnits } from "viem";
|
||||
import { OnChainMessageStatus } from "@consensys/linea-sdk";
|
||||
import { ModalContext } from "@/contexts/modal.context";
|
||||
import StatusText from "./StatusText";
|
||||
import TransactionProgressBar from "./TransactionProgressBar";
|
||||
@@ -22,26 +23,50 @@ type TransactionItemProps = {
|
||||
message: MessageWithStatus;
|
||||
};
|
||||
|
||||
function TransactionStatusSection({ status }: { status: OnChainMessageStatus }) {
|
||||
return (
|
||||
<div className="px-6 md:px-0">
|
||||
<div className="text-xs uppercase">Status</div>
|
||||
<StatusText status={status} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TransactionNetworkSection({ label, networkId }: { label: string; networkId: number }) {
|
||||
return (
|
||||
<div className="px-6 md:px-0">
|
||||
<div className="text-xs uppercase">{label}</div>
|
||||
<span>{NETWORK_ID_TO_NAME[networkId]}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TransactionAmountSection({ amount, decimals, symbol }: { amount: bigint; decimals: number; symbol: string }) {
|
||||
return (
|
||||
<div className="px-6 md:px-0">
|
||||
<div className="text-xs uppercase">Amount</div>
|
||||
<span className="font-bold text-white">
|
||||
{formatUnits(amount, decimals)} {symbol}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TransactionItem({ transaction, message }: TransactionItemProps) {
|
||||
const { handleShow } = useContext(ModalContext);
|
||||
const { handleShow, handleClose } = useContext(ModalContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-1 items-center gap-0 rounded-lg bg-[#2D2D2D] p-4 text-[#C0C0C0] hover:cursor-pointer hover:outline hover:outline-1 hover:outline-primary sm:grid-cols-1 md:grid-cols-6 md:gap-4"
|
||||
onClick={() => {
|
||||
handleShow(<TransactionDetailsModal transaction={transaction} message={message} />);
|
||||
handleShow(<TransactionDetailsModal transaction={transaction} message={message} handleClose={handleClose} />, {
|
||||
showCloseButton: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4 border-b border-card py-4 md:col-span-2 md:border-none md:p-0">
|
||||
<div className="px-6 md:px-0">
|
||||
<div className="text-xs uppercase">Status</div>
|
||||
<StatusText status={message.status} />
|
||||
</div>
|
||||
|
||||
<div className="px-6 md:px-0">
|
||||
<div className="text-xs uppercase">From</div>
|
||||
<span>{NETWORK_ID_TO_NAME[transaction.fromChain.id]}</span>
|
||||
</div>
|
||||
<TransactionStatusSection status={message.status} />
|
||||
<TransactionNetworkSection label="From" networkId={transaction.fromChain.id} />
|
||||
</div>
|
||||
|
||||
<div className="hidden px-6 md:col-span-2 md:block md:border-x md:border-card">
|
||||
@@ -53,17 +78,12 @@ export default function TransactionItem({ transaction, message }: TransactionIte
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 items-center gap-4 border-b border-card py-4 md:col-span-2 md:border-none md:p-0">
|
||||
<div className="px-6 md:px-0">
|
||||
<div className="text-xs uppercase">To</div>
|
||||
<span>{NETWORK_ID_TO_NAME[transaction.toChain.id]}</span>
|
||||
</div>
|
||||
|
||||
<div className="px-6 md:px-0">
|
||||
<div className="text-xs uppercase">Amount</div>
|
||||
<span className="font-bold text-white">
|
||||
{formatEther(transaction.amount)} {transaction.token.symbol}
|
||||
</span>
|
||||
</div>
|
||||
<TransactionNetworkSection label="To" networkId={transaction.toChain.id} />
|
||||
<TransactionAmountSection
|
||||
amount={transaction.amount}
|
||||
decimals={transaction.token.decimals}
|
||||
symbol={transaction.token.symbol}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pt-4 md:hidden md:pt-0">
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useBlockNumber } from "wagmi";
|
||||
import { toast } from "react-toastify";
|
||||
import TransactionItem from "./TransactionItem";
|
||||
import { TransactionHistory } from "@/models/history";
|
||||
import { formatDate, fromUnixTime } from "date-fns";
|
||||
import { NoTransactions } from "./NoTransaction";
|
||||
import useFetchHistory from "@/hooks/useFetchHistory";
|
||||
import { useFetchHistory } from "@/hooks";
|
||||
import Button from "../bridge/Button";
|
||||
|
||||
const groupByDay = (transactions: TransactionHistory[]): Record<string, TransactionHistory[]> => {
|
||||
return transactions.reduce(
|
||||
@@ -23,6 +23,77 @@ const groupByDay = (transactions: TransactionHistory[]): Record<string, Transact
|
||||
);
|
||||
};
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 border-2 border-card bg-cardBg p-4">
|
||||
{Array.from({ length: 3 }).map((_, groupIndex) => (
|
||||
<div key={groupIndex} className="flex flex-col gap-4">
|
||||
<div className="skeleton h-6 w-1/3 bg-card"></div>
|
||||
{Array.from({ length: 2 }).map((_, itemIndex) => (
|
||||
<div
|
||||
key={itemIndex}
|
||||
className="grid grid-cols-1 items-center gap-0 rounded-lg bg-[#2D2D2D] p-4 text-[#C0C0C0] sm:grid-cols-1 md:grid-cols-6 md:gap-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4 border-b border-card py-4 md:col-span-2 md:border-none md:p-0">
|
||||
<div className="skeleton h-4 w-1/2 bg-card"></div>
|
||||
<div className="skeleton h-4 w-1/2 bg-card"></div>
|
||||
</div>
|
||||
<div className="hidden px-6 md:col-span-2 md:block md:border-x md:border-card">
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center gap-4 border-b border-card py-4 md:col-span-2 md:border-none md:p-0">
|
||||
<div className="skeleton h-4 w-1/2 bg-card"></div>
|
||||
<div className="skeleton h-4 w-1/2 bg-card"></div>
|
||||
</div>
|
||||
<div className="px-6 pt-4 md:hidden md:pt-0">
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReloadHistoryButton({ clearHistory }: { clearHistory: () => void }) {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
id="reload-history-btn"
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="font-light normal-case text-gray-200 no-underline opacity-60 hover:text-primary hover:opacity-100"
|
||||
onClick={() => {
|
||||
clearHistory();
|
||||
}}
|
||||
>
|
||||
Reload history
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TransactionGroup({ date, transactions }: { date: string; transactions: TransactionHistory[] }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="block text-base-content">{formatDate(date, "PPP")}</span>
|
||||
{transactions.map((transaction, transactionIndex) => {
|
||||
if (transaction.messages && transaction.messages.length > 0 && transaction.messages[0].status) {
|
||||
const { messages, ...bridgingTransaction } = transaction;
|
||||
return (
|
||||
<TransactionItem
|
||||
key={`transaction-group-${date}-item-${transactionIndex}`}
|
||||
transaction={bridgingTransaction}
|
||||
message={messages[0]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Transactions() {
|
||||
const { data: blockNumber } = useBlockNumber({
|
||||
watch: true,
|
||||
@@ -40,17 +111,7 @@ export function Transactions() {
|
||||
const groupedTransactions = useMemo(() => groupByDay(transactions), [transactions]);
|
||||
|
||||
if (isLoading && transactions.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 border-2 border-card bg-cardBg p-4">
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
<div className="skeleton h-4 w-full bg-card"></div>
|
||||
</div>
|
||||
);
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
if (transactions.length === 0) {
|
||||
@@ -59,34 +120,9 @@ export function Transactions() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 rounded-lg border-2 border-card bg-cardBg p-4">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
id="reload-history-btn"
|
||||
className="btn-link btn-sm font-light normal-case text-gray-200 no-underline opacity-60 hover:text-primary hover:opacity-100"
|
||||
onClick={() => {
|
||||
clearHistory();
|
||||
toast.success("History cleared");
|
||||
}}
|
||||
>
|
||||
Reload history
|
||||
</button>
|
||||
</div>
|
||||
{Object.keys(groupedTransactions).map((date, groupIndex) => (
|
||||
<div key={`transaction-group-${groupIndex}`} className="flex flex-col gap-2">
|
||||
<span className="block text-base-content">{formatDate(date, "PPP")}</span>
|
||||
{groupedTransactions[date].map((transaction, transactionIndex) => {
|
||||
if (transaction.messages && transaction.messages.length > 0 && transaction.messages[0].status) {
|
||||
const { messages, ...bridgingTransaction } = transaction;
|
||||
return (
|
||||
<TransactionItem
|
||||
key={`transaction-group-${groupIndex}-item-${transactionIndex}`}
|
||||
transaction={bridgingTransaction}
|
||||
message={messages[0]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<ReloadHistoryButton clearHistory={clearHistory} />
|
||||
{Object.keys(groupedTransactions).map((date) => (
|
||||
<TransactionGroup key={`transaction-group-${date}`} date={date} transactions={groupedTransactions[date]} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { formatHex } from "@/utils/format";
|
||||
import Link from "next/link";
|
||||
import { BsDashLg } from "react-icons/bs";
|
||||
|
||||
type BlockExplorerLinkProps = {
|
||||
transactionHash?: string;
|
||||
blockExplorer?: string;
|
||||
};
|
||||
|
||||
const BlockExplorerLink: React.FC<BlockExplorerLinkProps> = ({ transactionHash, blockExplorer }) => {
|
||||
if (!transactionHash || !blockExplorer) {
|
||||
return <BsDashLg />;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
href={`${blockExplorer}/tx/${transactionHash}`}
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link text-primary"
|
||||
>
|
||||
{formatHex(transactionHash)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockExplorerLink;
|
||||
@@ -1,25 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useSwitchNetwork } from "@/hooks";
|
||||
import { Transaction } from "@/models";
|
||||
import { TransactionHistory } from "@/models/history";
|
||||
import { useWaitForTransactionReceipt } from "wagmi";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import Button from "@/components/bridge/Button";
|
||||
import useTransactionManagement, { MessageWithStatus } from "@/hooks/useTransactionManagement";
|
||||
import { ModalContext } from "@/contexts/modal.context";
|
||||
import TransactionConfirmationModal from "@/components/bridge/modals/TransactionConfirmationModal";
|
||||
|
||||
interface Props {
|
||||
message: MessageWithStatus;
|
||||
transaction: TransactionHistory;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export default function TransactionClaimButton({ message, transaction }: Props) {
|
||||
export default function TransactionClaimButton({ message, transaction, handleClose }: Props) {
|
||||
const [waitingTransaction, setWaitingTransaction] = useState<Transaction | undefined>();
|
||||
const [isClaimingLoading, setIsClaimingLoading] = useState<boolean>(false);
|
||||
|
||||
// Context
|
||||
const toChain = useChainStore((state) => state.toChain);
|
||||
|
||||
const { handleShow: handleShowConfirmationModal, handleClose: handleCloseConfirmationModal } =
|
||||
useContext(ModalContext);
|
||||
// Hooks
|
||||
const { switchChainById } = useSwitchNetwork(toChain?.id);
|
||||
const { writeClaimMessage, isLoading: isTxLoading, transaction: claimTx } = useTransactionManagement();
|
||||
@@ -48,10 +52,19 @@ export default function TransactionClaimButton({ message, transaction }: Props)
|
||||
|
||||
useEffect(() => {
|
||||
if (isWaitingSuccess) {
|
||||
toast.success(`Funds claimed on ${transaction.toChain.name}.`);
|
||||
handleClose();
|
||||
handleShowConfirmationModal(<TransactionConfirmationModal handleClose={handleCloseConfirmationModal} />, {
|
||||
showCloseButton: true,
|
||||
});
|
||||
setWaitingTransaction(undefined);
|
||||
}
|
||||
}, [isWaitingSuccess, transaction.toChain.name]);
|
||||
}, [
|
||||
handleClose,
|
||||
handleCloseConfirmationModal,
|
||||
handleShowConfirmationModal,
|
||||
isWaitingSuccess,
|
||||
transaction.toChain.name,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWaitingError) {
|
||||
@@ -77,17 +90,15 @@ export default function TransactionClaimButton({ message, transaction }: Props)
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
id={!claimBusy ? "claim-funds-btn" : "claim-funds-btn-disabled"}
|
||||
onClick={() => !claimBusy && onClaimMessage()}
|
||||
className={classNames("btn btn-primary w-full rounded-full uppercase", {
|
||||
"cursor-wait": claimBusy,
|
||||
})}
|
||||
variant="primary"
|
||||
loading={claimBusy}
|
||||
type="button"
|
||||
disabled={claimBusy}
|
||||
>
|
||||
{claimBusy && <span className="loading loading-spinner loading-xs"></span>}
|
||||
Claim
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,108 @@
|
||||
import Link from "next/link";
|
||||
import { OnChainMessageStatus } from "@consensys/linea-sdk";
|
||||
import { formatHex, formatTimestamp } from "@/utils/format";
|
||||
import { PiApproximateEqualsBold } from "react-icons/pi";
|
||||
import { formatTimestamp } from "@/utils/format";
|
||||
import { NETWORK_ID_TO_NAME } from "@/utils/constants";
|
||||
import { MessageWithStatus } from "@/hooks";
|
||||
import { TransactionHistory } from "@/models/history";
|
||||
import TransactionClaimButton from "./TransactionClaimButton";
|
||||
import TransactionDetailRow from "./TransactionDetailsRow";
|
||||
import BlockExplorerLink from "./BlockExplorerLink";
|
||||
import { useTransactionReceipt } from "wagmi";
|
||||
import { formatEther, zeroAddress } from "viem";
|
||||
import useTokenPrices from "@/hooks/useTokenPrices";
|
||||
|
||||
type TransactionDetailsModalProps = {
|
||||
transaction: TransactionHistory;
|
||||
message: MessageWithStatus;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const BlockExplorerLink: React.FC<{
|
||||
transactionHash?: string;
|
||||
blockExplorer?: string;
|
||||
}> = ({ transactionHash, blockExplorer }) => {
|
||||
if (!transactionHash || !blockExplorer) {
|
||||
return <span>N/A</span>;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
href={`${blockExplorer}/tx/${transactionHash}`}
|
||||
passHref
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link text-primary"
|
||||
>
|
||||
{formatHex(transactionHash)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
const TransactionDetailsModal: React.FC<TransactionDetailsModalProps> = ({ transaction, message, handleClose }) => {
|
||||
const { data: tokenPrices } = useTokenPrices([zeroAddress], transaction.fromChain.id);
|
||||
const { data: initialTransactionReceipt } = useTransactionReceipt({
|
||||
hash: transaction.transactionHash,
|
||||
chainId: transaction.fromChain.id,
|
||||
query: {
|
||||
enabled: message.status === OnChainMessageStatus.CLAIMED,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: claimingTransactionReceipt } = useTransactionReceipt({
|
||||
hash: message.claimingTransactionHash as `0x${string}`,
|
||||
chainId: transaction.toChain.id,
|
||||
query: {
|
||||
enabled: !!message.claimingTransactionHash && message.status === OnChainMessageStatus.CLAIMED,
|
||||
},
|
||||
});
|
||||
|
||||
const initialTransactionFee =
|
||||
initialTransactionReceipt?.gasUsed && initialTransactionReceipt?.effectiveGasPrice
|
||||
? initialTransactionReceipt.gasUsed * initialTransactionReceipt.effectiveGasPrice
|
||||
: 0n;
|
||||
|
||||
console.log(formatEther(initialTransactionFee || 0n));
|
||||
const claimingTransactionFee =
|
||||
claimingTransactionReceipt?.gasUsed && claimingTransactionReceipt?.effectiveGasPrice
|
||||
? claimingTransactionReceipt.gasUsed * claimingTransactionReceipt.effectiveGasPrice
|
||||
: 0n;
|
||||
|
||||
const totalFee =
|
||||
initialTransactionFee && claimingTransactionFee ? initialTransactionFee + claimingTransactionFee : 0n;
|
||||
|
||||
const TransactionDetailsModal: React.FC<TransactionDetailsModalProps> = ({ transaction, message }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-8 px-4">
|
||||
<h2 className="text-xl">Transaction details</h2>
|
||||
<div className="space-y-2">
|
||||
<div className="flex">
|
||||
<label className="w-44 text-[#C0C0C0]">Date & Time</label>
|
||||
<span className="text-[#C0C0C0]">{formatTimestamp(Number(transaction.timestamp), "h:mma d MMMM yyyy")}</span>
|
||||
</div>
|
||||
<TransactionDetailRow
|
||||
label="Date & Time"
|
||||
value={formatTimestamp(Number(transaction.timestamp), "h:mma d MMMM yyyy")}
|
||||
/>
|
||||
|
||||
<div className="flex">
|
||||
<label className="w-44 text-[#C0C0C0]">{NETWORK_ID_TO_NAME[transaction.fromChain.id]} Tx Hash</label>
|
||||
<BlockExplorerLink
|
||||
blockExplorer={transaction.fromChain.blockExplorers?.default.url}
|
||||
transactionHash={transaction.transactionHash}
|
||||
/>
|
||||
</div>
|
||||
<TransactionDetailRow
|
||||
label={`${NETWORK_ID_TO_NAME[transaction.fromChain.id]} Tx Hash`}
|
||||
value={
|
||||
<BlockExplorerLink
|
||||
blockExplorer={transaction.fromChain.blockExplorers?.default.url}
|
||||
transactionHash={transaction.transactionHash}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex">
|
||||
<label className="w-44 text-[#C0C0C0]">{NETWORK_ID_TO_NAME[transaction.toChain.id]} Tx Hash</label>
|
||||
<BlockExplorerLink
|
||||
blockExplorer={transaction.toChain.blockExplorers?.default.url}
|
||||
transactionHash={message.claimingTransactionHash}
|
||||
<TransactionDetailRow
|
||||
label={`${NETWORK_ID_TO_NAME[transaction.toChain.id]} Tx Hash`}
|
||||
value={
|
||||
<BlockExplorerLink
|
||||
blockExplorer={transaction.toChain.blockExplorers?.default.url}
|
||||
transactionHash={message.claimingTransactionHash}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{message.status === OnChainMessageStatus.CLAIMED && (
|
||||
<TransactionDetailRow
|
||||
label="Fee"
|
||||
value={
|
||||
<div>
|
||||
{transaction.fromChain.id === 1 || transaction.toChain.id === 1
|
||||
? `${formatEther(totalFee)} ETH ${(<PiApproximateEqualsBold />)}
|
||||
$${
|
||||
tokenPrices?.[zeroAddress]?.usd.toLocaleString("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}) || ""
|
||||
}`
|
||||
: `${formatEther(totalFee)} ETH`}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<label className="w-44 text-[#C0C0C0]">Fee</label>
|
||||
<span className="text-[#C0C0C0]">5 ETH ~$15000</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{message.status === OnChainMessageStatus.CLAIMABLE && (
|
||||
<TransactionClaimButton key={message.messageHash} message={message} transaction={transaction} />
|
||||
<TransactionClaimButton
|
||||
key={message.messageHash}
|
||||
message={message}
|
||||
transaction={transaction}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
type TransactionDetailRowProps = {
|
||||
label: string;
|
||||
value: React.ReactNode | string;
|
||||
};
|
||||
|
||||
const TransactionDetailRow: React.FC<TransactionDetailRowProps> = ({ label, value }) => (
|
||||
<div className="flex items-center">
|
||||
<label className="w-44 text-[#C0C0C0]">{label}</label>
|
||||
<span className="text-[#C0C0C0]">{value}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TransactionDetailRow;
|
||||
@@ -1,50 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { linea, lineaSepolia } from "viem/chains";
|
||||
|
||||
type ChainLogoProps = {
|
||||
chainId: number;
|
||||
};
|
||||
|
||||
const ChainLogo = ({ chainId }: ChainLogoProps) => {
|
||||
if (chainId === lineaSepolia.id) {
|
||||
return (
|
||||
<span>
|
||||
<Image
|
||||
src={"/images/logo/linea-sepolia.svg"}
|
||||
alt="Linea Sepolia"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (chainId === linea.id) {
|
||||
return (
|
||||
<span>
|
||||
<Image
|
||||
src={"/images/logo/linea-mainnet.svg"}
|
||||
alt="Linea"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "18px", height: "auto" }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Image
|
||||
src={"/images/logo/ethereum.svg"}
|
||||
alt="Ethereum"
|
||||
width={0}
|
||||
height={0}
|
||||
style={{ width: "12px", height: "auto" }}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChainLogo;
|
||||
@@ -1,45 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { switchChain } from "@wagmi/core";
|
||||
import { NetworkLayer, NetworkType, config, wagmiConfig } from "@/config";
|
||||
import log from "loglevel";
|
||||
import { useAccount } from "wagmi";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
export default function SwitchNetwork() {
|
||||
const { networkType, networkLayer } = useChainStore((state) => ({
|
||||
networkType: state.networkType,
|
||||
networkLayer: state.networkLayer,
|
||||
}));
|
||||
const { isConnected } = useAccount();
|
||||
|
||||
const networks = config.networks;
|
||||
|
||||
const switchNetworkHandler = async () => {
|
||||
if (networkLayer === NetworkLayer.UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (networkType === NetworkType.SEPOLIA) {
|
||||
await switchChain(wagmiConfig, {
|
||||
chainId: networks.MAINNET.L1.chainId,
|
||||
});
|
||||
} else {
|
||||
await switchChain(wagmiConfig, {
|
||||
chainId: networks.SEPOLIA.L1.chainId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isConnected) return null;
|
||||
|
||||
return (
|
||||
<button id="try-network-btn" className="btn btn-info uppercase" onClick={() => switchNetworkHandler()}>
|
||||
Try {networkType === NetworkType.SEPOLIA ? "Mainnet" : "Testnet"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -8,12 +8,16 @@ export const configSchema = Joi.object({
|
||||
networks: Joi.object({
|
||||
MAINNET: Joi.object({
|
||||
L1: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
iconPath: Joi.string().required(),
|
||||
chainId: Joi.number().required(),
|
||||
messageServiceAddress: Joi.string().required(),
|
||||
tokenBridgeAddress: Joi.string().required(),
|
||||
usdcBridgeAddress: Joi.string().required(),
|
||||
}),
|
||||
L2: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
iconPath: Joi.string().required(),
|
||||
chainId: Joi.number().required(),
|
||||
messageServiceAddress: Joi.string().required(),
|
||||
tokenBridgeAddress: Joi.string().required(),
|
||||
@@ -25,12 +29,16 @@ export const configSchema = Joi.object({
|
||||
}),
|
||||
SEPOLIA: Joi.object({
|
||||
L1: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
iconPath: Joi.string().required(),
|
||||
chainId: Joi.number().required(),
|
||||
messageServiceAddress: Joi.string().required(),
|
||||
tokenBridgeAddress: Joi.string().required(),
|
||||
usdcBridgeAddress: Joi.string().required(),
|
||||
}),
|
||||
L2: Joi.object({
|
||||
name: Joi.string().required(),
|
||||
iconPath: Joi.string().required(),
|
||||
chainId: Joi.number().required(),
|
||||
messageServiceAddress: Joi.string().required(),
|
||||
tokenBridgeAddress: Joi.string().required(),
|
||||
|
||||
@@ -33,6 +33,8 @@ export enum TokenType {
|
||||
}
|
||||
|
||||
interface LayerConfig {
|
||||
name: string;
|
||||
iconPath: string;
|
||||
chainId: number;
|
||||
messageServiceAddress: Address;
|
||||
tokenBridgeAddress: Address;
|
||||
@@ -85,6 +87,8 @@ export const config: Config = {
|
||||
networks: {
|
||||
MAINNET: {
|
||||
L1: {
|
||||
name: "Ethereum Mainnet",
|
||||
iconPath: "/images/logo/ethereum-rounded.svg",
|
||||
chainId: 1,
|
||||
messageServiceAddress: process.env.NEXT_PUBLIC_MAINNET_L1_MESSAGE_SERVICE
|
||||
? (process.env.NEXT_PUBLIC_MAINNET_L1_MESSAGE_SERVICE as Address)
|
||||
@@ -97,7 +101,9 @@ export const config: Config = {
|
||||
: ({} as Address),
|
||||
},
|
||||
L2: {
|
||||
name: "Linea",
|
||||
chainId: 59144,
|
||||
iconPath: "/images/logo/linea-mainnet.svg",
|
||||
messageServiceAddress: process.env.NEXT_PUBLIC_MAINNET_LINEA_MESSAGE_SERVICE
|
||||
? (process.env.NEXT_PUBLIC_MAINNET_LINEA_MESSAGE_SERVICE as Address)
|
||||
: ({} as Address),
|
||||
@@ -121,6 +127,8 @@ export const config: Config = {
|
||||
|
||||
SEPOLIA: {
|
||||
L1: {
|
||||
name: "Sepolia",
|
||||
iconPath: "/images/logo/ethereum-rounded.svg",
|
||||
chainId: 11155111,
|
||||
messageServiceAddress: process.env.NEXT_PUBLIC_SEPOLIA_L1_MESSAGE_SERVICE
|
||||
? (process.env.NEXT_PUBLIC_SEPOLIA_L1_MESSAGE_SERVICE as Address)
|
||||
@@ -133,6 +141,8 @@ export const config: Config = {
|
||||
: ({} as Address),
|
||||
},
|
||||
L2: {
|
||||
name: "Linea Sepolia",
|
||||
iconPath: "/images/logo/linea-sepolia.svg",
|
||||
chainId: 59141,
|
||||
messageServiceAddress: process.env.NEXT_PUBLIC_SEPOLIA_LINEA_MESSAGE_SERVICE
|
||||
? (process.env.NEXT_PUBLIC_SEPOLIA_LINEA_MESSAGE_SERVICE as Address)
|
||||
|
||||
@@ -19,6 +19,8 @@ interface ModalProviderProps {
|
||||
|
||||
interface ModalOptions {
|
||||
width?: string;
|
||||
showCloseButton?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ModalProvider: React.FC<ModalProviderProps> = ({ children }) => {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import React, { createContext, useState, useCallback } from "react";
|
||||
|
||||
interface UIContextProps {
|
||||
showBridge: boolean;
|
||||
toggleShowBridge: (value?: boolean) => void;
|
||||
}
|
||||
|
||||
export const UIContext = createContext<UIContextProps>({
|
||||
showBridge: false,
|
||||
toggleShowBridge: () => {
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const UIProvider: React.FC<Props> = ({ children }) => {
|
||||
const [showBridge, setShowBridge] = useState(false);
|
||||
|
||||
const toggleShowBridge = useCallback((value?: boolean) => {
|
||||
if (typeof value === "boolean") {
|
||||
return setShowBridge(value);
|
||||
}
|
||||
setShowBridge((prevShowBridge) => !prevShowBridge);
|
||||
}, []);
|
||||
|
||||
return <UIContext.Provider value={{ showBridge, toggleShowBridge }}>{children}</UIContext.Provider>;
|
||||
};
|
||||
@@ -1,13 +1,19 @@
|
||||
export { default as useAllowance } from "./useAllowance";
|
||||
export { default as useApprove } from "./useApprove";
|
||||
export { default as useBridge } from "./useBridge";
|
||||
export { default as useERC20Storage } from "./useERC20Storage";
|
||||
export { default as useExecutionFee } from "./useExecutionFee";
|
||||
export { default as useFetchAnchoringEvents } from "./useFetchAnchoringEvents";
|
||||
export { default as useFetchBridgeTransactions } from "./useFetchBridgeTransactions";
|
||||
export { default as useMinimumFee } from "./useMinimumFee";
|
||||
export { default as useTransactionManagement } from "./useTransactionManagement";
|
||||
export { default as useFetchHistory } from "./useFetchHistory";
|
||||
export { default as useGasEstimation } from "./useGasEstimation";
|
||||
export { default as useInitialiseChain } from "./useInitialiseChain";
|
||||
export { default as useInitialiseToken } from "./useInitialiseToken";
|
||||
export { default as useLineaSDK } from "./useLineaSDK";
|
||||
export { default as useMessageStatus } from "./useMessageStatus";
|
||||
export { default as useMinimumFee } from "./useMinimumFee";
|
||||
export { default as useSwitchNetwork } from "./useSwitchNetwork";
|
||||
export { default as useTokenFetch } from "./useTokenFetch";
|
||||
export { default as useTransactionManagement } from "./useTransactionManagement";
|
||||
|
||||
export type { MessageWithStatus } from "./useTransactionManagement";
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { readContract } from "@wagmi/core";
|
||||
import { useAccount } from "wagmi";
|
||||
import { Address } from "viem";
|
||||
import ERC20Abi from "@/abis/ERC20.json";
|
||||
import log from "loglevel";
|
||||
import { wagmiConfig } from "@/config";
|
||||
import { useAccount, useReadContract } from "wagmi";
|
||||
import { erc20Abi } from "viem";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
|
||||
type UseAllowance = {
|
||||
allowance: bigint | null;
|
||||
fetchAllowance: () => Promise<void>;
|
||||
};
|
||||
|
||||
const useAllowance = (): UseAllowance => {
|
||||
const [allowance, setAllowance] = useState<bigint | null>(null);
|
||||
|
||||
const useAllowance = () => {
|
||||
// Wagmi
|
||||
const { address } = useAccount();
|
||||
|
||||
@@ -26,35 +14,22 @@ const useAllowance = (): UseAllowance => {
|
||||
fromChain: state.fromChain,
|
||||
}));
|
||||
|
||||
const fetchAllowance = useCallback(async () => {
|
||||
if (!address || !token || !networkLayer || !token[networkLayer]) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
data: allowance,
|
||||
queryKey,
|
||||
refetch,
|
||||
} = useReadContract({
|
||||
abi: erc20Abi,
|
||||
functionName: "allowance",
|
||||
args: [address ?? "0x", tokenBridgeAddress ?? "0x"],
|
||||
address: token?.[networkLayer] ?? "0x",
|
||||
query: {
|
||||
enabled: !!token && !!address && !!networkLayer && !!tokenBridgeAddress,
|
||||
},
|
||||
chainId: fromChain?.id,
|
||||
});
|
||||
|
||||
// Here we need to specify the chain because we want to be able
|
||||
// to read a contract on both chains without having to connect
|
||||
// to one or the other
|
||||
try {
|
||||
const allowance = (await readContract(wagmiConfig, {
|
||||
address: token[networkLayer] as Address,
|
||||
abi: ERC20Abi,
|
||||
functionName: "allowance",
|
||||
args: [address, tokenBridgeAddress],
|
||||
chainId: fromChain?.id,
|
||||
})) as bigint;
|
||||
|
||||
setAllowance(allowance);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
log.error("Unable to fetch allowance", { address });
|
||||
}
|
||||
}, [address, tokenBridgeAddress, token, networkLayer, fromChain]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllowance();
|
||||
}, [fetchAllowance]);
|
||||
|
||||
return { allowance, fetchAllowance };
|
||||
return { allowance, queryKey, refetchAllowance: refetch };
|
||||
};
|
||||
|
||||
export default useAllowance;
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { writeContract, getPublicClient, readContract, simulateContract } from "@wagmi/core";
|
||||
import { useAccount, useBlockNumber, useEstimateFeesPerGas, useWaitForTransactionReceipt } from "wagmi";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Address, Chain, parseUnits, zeroAddress } from "viem";
|
||||
import { writeContract, simulateContract } from "@wagmi/core";
|
||||
import { useAccount, useWaitForTransactionReceipt } from "wagmi";
|
||||
import { Address, parseUnits } from "viem";
|
||||
import log from "loglevel";
|
||||
import USDCBridge from "@/abis/USDCBridge.json";
|
||||
import TokenBridge from "@/abis/TokenBridge.json";
|
||||
import MessageService from "@/abis/MessageService.json";
|
||||
import { TokenInfo, TokenType, config } from "@/config/config";
|
||||
import { TokenType } from "@/config/config";
|
||||
import { BridgeError, BridgeErrors, Transaction } from "@/models";
|
||||
import { getChainNetworkLayer } from "@/utils/chainsUtil";
|
||||
import { FieldErrors, FieldValues } from "react-hook-form";
|
||||
import { wagmiConfig } from "@/config";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
import useMinimumFee from "./useMinimumFee";
|
||||
import { isEmptyObject } from "@/utils/utils";
|
||||
|
||||
type UseBridge = {
|
||||
hash: Address | undefined;
|
||||
isLoading: boolean;
|
||||
bridge: (amount: string, userMinimumFee?: bigint, to?: Address) => Promise<void>;
|
||||
estimateGasBridge: (amount: string, userMinimumFee?: bigint, to?: Address) => Promise<bigint | undefined>;
|
||||
isError: boolean;
|
||||
error: BridgeError | null;
|
||||
bridgeEnabled: (amount: string, allowance: bigint, errors: FieldErrors<FieldValues>) => boolean;
|
||||
fetchBridgedToken: (fromChain: Chain, toChain: Chain, nativeToken: Address) => Promise<Address | undefined>;
|
||||
fetchNativeToken: (chain: Chain, bridgedToken: Address) => Promise<Address | undefined>;
|
||||
fillMissingTokenAddress: (token: TokenInfo) => Promise<void>;
|
||||
};
|
||||
|
||||
const useBridge = (): UseBridge => {
|
||||
@@ -34,24 +29,17 @@ const useBridge = (): UseBridge => {
|
||||
const [error, setError] = useState<BridgeError | null>(null);
|
||||
|
||||
// Context
|
||||
const { token, tokenBridgeAddress, messageServiceAddress, networkLayer, networkType, fromChain, toChain } =
|
||||
useChainStore((state) => ({
|
||||
token: state.token,
|
||||
tokenBridgeAddress: state.tokenBridgeAddress,
|
||||
messageServiceAddress: state.messageServiceAddress,
|
||||
networkLayer: state.networkLayer,
|
||||
networkType: state.networkType,
|
||||
fromChain: state.fromChain,
|
||||
toChain: state.toChain,
|
||||
}));
|
||||
const { token, tokenBridgeAddress, messageServiceAddress, networkLayer, fromChain } = useChainStore((state) => ({
|
||||
token: state.token,
|
||||
tokenBridgeAddress: state.tokenBridgeAddress,
|
||||
messageServiceAddress: state.messageServiceAddress,
|
||||
networkLayer: state.networkLayer,
|
||||
fromChain: state.fromChain,
|
||||
}));
|
||||
|
||||
const { minimumFee } = useMinimumFee();
|
||||
const { address, isConnected } = useAccount();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: blockNumber } = useBlockNumber({ watch: true });
|
||||
const { data: feeData, queryKey } = useEstimateFeesPerGas({ chainId: fromChain?.id, type: "legacy" });
|
||||
|
||||
const {
|
||||
isLoading: isTxLoading,
|
||||
isSuccess: isTxSuccess,
|
||||
@@ -61,13 +49,6 @@ const useBridge = (): UseBridge => {
|
||||
chainId: transaction?.chainId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (blockNumber && blockNumber % 5n === 0n) {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockNumber, queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTxSuccess || isTxError) {
|
||||
setTransaction(null);
|
||||
@@ -183,180 +164,6 @@ const useBridge = (): UseBridge => {
|
||||
[minimumFee, token, tokenBridgeAddress, address, getWriteConfig, fromChain],
|
||||
);
|
||||
|
||||
const estimateGasBridge = useCallback(
|
||||
async (_amount: string, _userMinimumFee?: bigint, to?: Address) => {
|
||||
if (!address || !tokenBridgeAddress || !token || !messageServiceAddress) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!feeData?.gasPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const publicClient = getPublicClient(wagmiConfig, {
|
||||
chainId: fromChain?.id,
|
||||
});
|
||||
|
||||
if (!publicClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const amountBigInt = parseUnits(_amount, token.decimals);
|
||||
const sendTo = to ? to : address;
|
||||
let estimatedGasFee;
|
||||
|
||||
// If amount negative, return
|
||||
if (amountBigInt <= BigInt(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (token.type) {
|
||||
case TokenType.USDC:
|
||||
estimatedGasFee = await publicClient.estimateContractGas({
|
||||
abi: USDCBridge.abi,
|
||||
functionName: "depositTo",
|
||||
address: tokenBridgeAddress,
|
||||
args: [amountBigInt, sendTo],
|
||||
value: _userMinimumFee,
|
||||
account: address,
|
||||
});
|
||||
break;
|
||||
case TokenType.ERC20:
|
||||
estimatedGasFee = await publicClient.estimateContractGas({
|
||||
abi: TokenBridge.abi,
|
||||
functionName: "bridgeToken",
|
||||
address: tokenBridgeAddress,
|
||||
args: [token[networkLayer], amountBigInt, sendTo],
|
||||
value: _userMinimumFee,
|
||||
account: address,
|
||||
});
|
||||
break;
|
||||
case TokenType.ETH:
|
||||
estimatedGasFee = await publicClient.estimateContractGas({
|
||||
abi: MessageService.abi,
|
||||
functionName: "sendMessage",
|
||||
address: messageServiceAddress,
|
||||
args: [sendTo, _userMinimumFee, "0x"],
|
||||
value: _userMinimumFee ? _userMinimumFee + amountBigInt : amountBigInt,
|
||||
account: address,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
return estimatedGasFee * feeData.gasPrice;
|
||||
} catch (error) {
|
||||
// log.error(error);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[address, token, tokenBridgeAddress, feeData, messageServiceAddress, networkLayer, fromChain],
|
||||
);
|
||||
|
||||
const fetchBridgedToken = useCallback(
|
||||
async (fromChain: Chain, toChain: Chain, nativeToken: Address) => {
|
||||
const fromLayer = getChainNetworkLayer(fromChain);
|
||||
const toLayer = getChainNetworkLayer(toChain);
|
||||
if (!toLayer || !fromLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _tokenBridgeAddress = config.networks[networkType][toLayer].tokenBridgeAddress;
|
||||
|
||||
if (!_tokenBridgeAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const bridgedToken = (await readContract(wagmiConfig, {
|
||||
address: _tokenBridgeAddress,
|
||||
abi: TokenBridge.abi,
|
||||
functionName: "nativeToBridgedToken",
|
||||
args: [fromChain.id, nativeToken],
|
||||
chainId: toChain.id,
|
||||
})) as Address;
|
||||
|
||||
return bridgedToken;
|
||||
} catch (error) {
|
||||
log.warn("Error fetching bridged token address");
|
||||
}
|
||||
},
|
||||
[networkType],
|
||||
);
|
||||
|
||||
const fetchNativeToken = useCallback(
|
||||
async (chain: Chain, bridgedToken: Address) => {
|
||||
const layer = getChainNetworkLayer(chain);
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _tokenBridgeAddress = config.networks[networkType][layer].tokenBridgeAddress;
|
||||
|
||||
if (!_tokenBridgeAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const nativeToken = (await readContract(wagmiConfig, {
|
||||
address: _tokenBridgeAddress,
|
||||
abi: TokenBridge.abi,
|
||||
functionName: "bridgedToNativeToken",
|
||||
args: [bridgedToken],
|
||||
chainId: chain.id,
|
||||
})) as Address;
|
||||
|
||||
return nativeToken;
|
||||
} catch (error) {
|
||||
log.warn("Error fetching native token address");
|
||||
}
|
||||
},
|
||||
[networkType],
|
||||
);
|
||||
|
||||
const fillMissingTokenAddress = useCallback(
|
||||
async (token: TokenInfo) => {
|
||||
if (!fromChain || !toChain) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we don't if a token is native or bridged for a chain we try all the combinations
|
||||
// possible to find its counterpart on the other chain
|
||||
if (!token.L1 && token.L2) {
|
||||
token.L1 = (await fetchNativeToken(fromChain, token.L2)) || null;
|
||||
if (!token.L1 || token.L1 !== zeroAddress) return;
|
||||
|
||||
token.L1 = (await fetchNativeToken(toChain, token.L2)) || null;
|
||||
if (!token.L1 || token.L1 !== zeroAddress) return;
|
||||
|
||||
token.L1 = (await fetchBridgedToken(fromChain, toChain, token.L2)) || null;
|
||||
if (!token.L1 || token.L1 !== zeroAddress) return;
|
||||
|
||||
token.L1 = (await fetchBridgedToken(toChain, fromChain, token.L2)) || null;
|
||||
} else if (token.L1) {
|
||||
token.L2 = (await fetchNativeToken(fromChain, token.L1)) || null;
|
||||
if (!token.L2 || token.L2 !== zeroAddress) return;
|
||||
|
||||
token.L2 = (await fetchNativeToken(toChain, token.L1)) || null;
|
||||
if (!token.L2 || token.L2 !== zeroAddress) return;
|
||||
|
||||
token.L2 = (await fetchBridgedToken(fromChain, toChain, token.L1)) || null;
|
||||
if (!token.L2 || token.L2 !== zeroAddress) return;
|
||||
|
||||
token.L2 = (await fetchBridgedToken(toChain, fromChain, token.L1)) || null;
|
||||
}
|
||||
|
||||
if (token.L1 === zeroAddress) token.L1 = null;
|
||||
if (token.L2 === zeroAddress) token.L2 = null;
|
||||
},
|
||||
[fromChain, toChain, fetchBridgedToken, fetchNativeToken],
|
||||
);
|
||||
|
||||
const isEmptyObject = (obj: object): boolean => {
|
||||
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
};
|
||||
|
||||
const bridgeEnabled = useCallback(
|
||||
(amount: string, allowance: bigint, errors: FieldErrors<FieldValues>) => {
|
||||
if (!token || !amount || isLoading || isTxLoading || !isConnected) {
|
||||
@@ -385,11 +192,7 @@ const useBridge = (): UseBridge => {
|
||||
bridge,
|
||||
isError: error !== null,
|
||||
error,
|
||||
estimateGasBridge,
|
||||
bridgeEnabled,
|
||||
fetchBridgedToken,
|
||||
fetchNativeToken,
|
||||
fillMissingTokenAddress,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useBlockNumber, useEstimateFeesPerGas } from "wagmi";
|
||||
import { formatEther } from "viem";
|
||||
import { config, NetworkLayer, NetworkType, TokenInfo, TokenType } from "@/config";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useChainStore } from "@/stores/chainStore";
|
||||
@@ -14,9 +13,8 @@ type useExecutionFeeProps = {
|
||||
};
|
||||
|
||||
const useExecutionFee = ({ token, claim, networkLayer, networkType, minimumFee }: useExecutionFeeProps) => {
|
||||
const [minFees, setMinFees] = useState<string>();
|
||||
const [minFees, setMinFees] = useState<bigint>(0n);
|
||||
const toChain = useChainStore((state) => state.toChain);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: blockNumber } = useBlockNumber({ watch: true });
|
||||
const { data: feeData, queryKey } = useEstimateFeesPerGas({ chainId: toChain?.id, type: "legacy" });
|
||||
@@ -25,68 +23,89 @@ const useExecutionFee = ({ token, claim, networkLayer, networkType, minimumFee }
|
||||
if (blockNumber && blockNumber % 5n === 0n) {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [blockNumber, queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
setMinFees(undefined);
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
setMinFees(0n);
|
||||
if (!token) return;
|
||||
|
||||
const isETH = token.type === TokenType.ETH;
|
||||
const isL1 = networkLayer === NetworkLayer.L1;
|
||||
const isL2 = networkLayer === NetworkLayer.L2;
|
||||
const isAutoClaim = claim === "auto";
|
||||
const isManualClaim = claim === "manual";
|
||||
const isERC20orUSDC = token.type === TokenType.ERC20 || token.type === TokenType.USDC;
|
||||
// postman fee
|
||||
if (isETH && isL1 && isAutoClaim && feeData?.gasPrice) {
|
||||
const postmanFee = calculatePostmanFee(feeData.gasPrice, networkType);
|
||||
postmanFee && setMinFees(formatEther(postmanFee));
|
||||
return;
|
||||
}
|
||||
const fee = calculateFee({
|
||||
token,
|
||||
claim,
|
||||
networkLayer,
|
||||
networkType,
|
||||
minimumFee,
|
||||
gasPrice: feeData?.gasPrice,
|
||||
});
|
||||
|
||||
// 0
|
||||
if (isETH && isL1 && isManualClaim) {
|
||||
setMinFees(formatEther(BigInt(0)));
|
||||
return;
|
||||
if (fee !== undefined) {
|
||||
setMinFees(fee);
|
||||
}
|
||||
|
||||
// anti-DDoS fee + postman fee
|
||||
if (isETH && isL2 && isAutoClaim && feeData?.gasPrice) {
|
||||
const postmanFee = calculatePostmanFee(feeData.gasPrice, networkType);
|
||||
postmanFee && setMinFees(formatEther(postmanFee + minimumFee));
|
||||
return;
|
||||
}
|
||||
|
||||
// anti-DDoS fee
|
||||
if (isETH && isL2 && isManualClaim) {
|
||||
setMinFees(formatEther(minimumFee));
|
||||
return;
|
||||
}
|
||||
|
||||
// 0
|
||||
if (isERC20orUSDC && isL1) {
|
||||
setMinFees(formatEther(BigInt(0)));
|
||||
return;
|
||||
}
|
||||
|
||||
// anti-DDoS fee
|
||||
if (isERC20orUSDC && isL2) {
|
||||
setMinFees(formatEther(minimumFee));
|
||||
return;
|
||||
}
|
||||
}, [claim, networkLayer, token, minimumFee, feeData, networkType]);
|
||||
|
||||
const calculatePostmanFee = (gasPrice: bigint, networkType: NetworkType) =>
|
||||
config.networks[networkType] &&
|
||||
gasPrice *
|
||||
(config.networks[networkType].gasEstimated + config.networks[networkType].gasLimitSurplus) *
|
||||
config.networks[networkType].profitMargin;
|
||||
}, [claim, networkLayer, token, minimumFee, networkType, feeData?.gasPrice]);
|
||||
|
||||
return minFees;
|
||||
};
|
||||
|
||||
export default useExecutionFee;
|
||||
|
||||
const calculateFee = ({
|
||||
token,
|
||||
claim,
|
||||
networkLayer,
|
||||
networkType,
|
||||
minimumFee,
|
||||
gasPrice,
|
||||
}: {
|
||||
token: TokenInfo;
|
||||
claim: string | undefined;
|
||||
networkLayer: NetworkLayer | undefined;
|
||||
networkType: NetworkType;
|
||||
minimumFee: bigint;
|
||||
gasPrice: bigint | undefined;
|
||||
}): bigint | undefined => {
|
||||
const isETH = token.type === TokenType.ETH;
|
||||
const isL1 = networkLayer === NetworkLayer.L1;
|
||||
const isL2 = networkLayer === NetworkLayer.L2;
|
||||
const isAutoClaim = claim === "auto";
|
||||
const isManualClaim = claim === "manual";
|
||||
const isERC20orUSDC = token.type === TokenType.ERC20 || token.type === TokenType.USDC;
|
||||
|
||||
// postman fee
|
||||
if (isETH && isL1 && isAutoClaim && gasPrice) {
|
||||
return calculatePostmanFee(gasPrice, networkType);
|
||||
}
|
||||
|
||||
// 0
|
||||
if (isETH && isL1 && isManualClaim) {
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
// anti-DDoS fee + postman fee
|
||||
if (isETH && isL2 && isAutoClaim && gasPrice) {
|
||||
return calculatePostmanFee(gasPrice, networkType) + minimumFee;
|
||||
}
|
||||
|
||||
// anti-DDoS fee
|
||||
if (isETH && isL2 && isManualClaim) {
|
||||
return minimumFee;
|
||||
}
|
||||
|
||||
// 0
|
||||
if (isERC20orUSDC && isL1) {
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
// anti-DDoS fee
|
||||
if (isERC20orUSDC && isL2) {
|
||||
return minimumFee;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const calculatePostmanFee = (gasPrice: bigint, networkType: NetworkType) =>
|
||||
config.networks[networkType] &&
|
||||
gasPrice *
|
||||
(config.networks[networkType].gasEstimated + config.networks[networkType].gasLimitSurplus) *
|
||||
config.networks[networkType].profitMargin;
|
||||
|
||||