diff --git a/.env.local b/.env.local index 008bb05..f20b6c7 100644 --- a/.env.local +++ b/.env.local @@ -1,3 +1,5 @@ -NEXT_PUBLIC_WC_PROJECT_ID=your_project_id_here +NEXT_PUBLIC_WC_PROJECT_ID=1ac26acf6371d43254aa7c9d285252e2 NEXT_PUBLIC_ENABLE_TESTNETS=false -NEXT_PUBLIC_BETA_ACCESS_CODE=your_code \ No newline at end of file +NEXT_PUBLIC_BETA_ACCESS_CODE=your_code +JWT_SECRET=your_super_secret_jwt_key +SERVER_SECRET_ACCESS_CODE=yieldsogood \ No newline at end of file diff --git a/next.config.js b/next.config.js index bea0abf..36c2313 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,10 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + transpilePackages: ['@vanilla-extract', '@rainbow-me'], reactStrictMode: true, + experimental: { + esmExternals: 'loose', + }, webpack: (config) => { config.externals.push('pino-pretty', 'lokijs', 'encoding'); return config; diff --git a/package-lock.json b/package-lock.json index 99374db..02fdb68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,29 +10,34 @@ "dependencies": { "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", - "@rainbow-me/rainbowkit": "^2.2.4", + "@rainbow-me/rainbowkit": "^2.2.8", "@tanstack/react-query": "^5.55.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cookie": "^1.0.2", "ethers": "^6.13.5", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.484.0", "next": "^15.1.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^5.0.0", "react-icons": "^5.5.0", - "tailwind-merge": "^3.0.2", + "recharts": "^2.15.3", + "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "viem": "2.23.3", "wagmi": "^2.14.11" }, "devDependencies": { "@shadcn/ui": "^0.0.4", + "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.14.8", "@types/react": "^19.0.6", "autoprefixer": "^10.4.21", "eslint-config-next": "^15.1.7", "postcss": "^8.4.35", + "postinstall-postinstall": "^2.1.0", "tailwindcss": "^3.4.1", "typescript": "5.5.4" } @@ -138,8 +143,7 @@ "node_modules/@emotion/hash": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" }, "node_modules/@eslint-community/eslint-utils": { "version": "4.5.1", @@ -1996,16 +2000,15 @@ "license": "MIT" }, "node_modules/@rainbow-me/rainbowkit": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@rainbow-me/rainbowkit/-/rainbowkit-2.2.4.tgz", - "integrity": "sha512-LUYBcB5bzLf6/BMdnW3dEFHVqoPkTGcFN3u6WamaIHXuqD9HT+HVAeNlcYvKENBXldN2zNBs1Bt3k8Oy7y5bTw==", - "license": "MIT", + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@rainbow-me/rainbowkit/-/rainbowkit-2.2.8.tgz", + "integrity": "sha512-EdNIK2cdAT6GJ9G11wx7nCVfjqBfxh7dx/1DhPYrB+yg+VFrII6cM1PiMFVC9evD4mqVHe9mmLAt3nvlwDdiPQ==", "dependencies": { - "@vanilla-extract/css": "1.15.5", - "@vanilla-extract/dynamic": "2.1.2", - "@vanilla-extract/sprinkles": "1.6.3", + "@vanilla-extract/css": "1.17.3", + "@vanilla-extract/dynamic": "2.1.4", + "@vanilla-extract/sprinkles": "1.6.4", "clsx": "2.1.1", - "qrcode": "1.5.4", + "cuer": "0.0.2", "react-remove-scroll": "2.6.2", "ua-parser-js": "^1.0.37" }, @@ -2020,23 +2023,6 @@ "wagmi": "^2.9.0" } }, - "node_modules/@rainbow-me/rainbowkit/node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2236,6 +2222,60 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2268,6 +2308,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2694,13 +2745,12 @@ ] }, "node_modules/@vanilla-extract/css": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.15.5.tgz", - "integrity": "sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng==", - "license": "MIT", + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.17.3.tgz", + "integrity": "sha512-jHivr1UPoJTX5Uel4AZSOwrCf4mO42LcdmnhJtUxZaRWhW4FviFbIfs0moAWWld7GOT+2XnuVZjjA/K32uUnMQ==", "dependencies": { "@emotion/hash": "^0.9.0", - "@vanilla-extract/private": "^1.0.6", + "@vanilla-extract/private": "^1.0.8", "css-what": "^6.1.0", "cssesc": "^3.0.0", "csstype": "^3.0.7", @@ -2714,25 +2764,22 @@ } }, "node_modules/@vanilla-extract/dynamic": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.2.tgz", - "integrity": "sha512-9BGMciD8rO1hdSPIAh1ntsG4LPD3IYKhywR7VOmmz9OO4Lx1hlwkSg3E6X07ujFx7YuBfx0GDQnApG9ESHvB2A==", - "license": "MIT", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/dynamic/-/dynamic-2.1.4.tgz", + "integrity": "sha512-7+Ot7VlP3cIzhJnTsY/kBtNs21s0YD7WI1rKJJKYP56BkbDxi/wrQUWMGEczKPUDkJuFcvbye+E2ub1u/mHH9w==", "dependencies": { - "@vanilla-extract/private": "^1.0.6" + "@vanilla-extract/private": "^1.0.8" } }, "node_modules/@vanilla-extract/private": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.6.tgz", - "integrity": "sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw==", - "license": "MIT" + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.9.tgz", + "integrity": "sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==" }, "node_modules/@vanilla-extract/sprinkles": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@vanilla-extract/sprinkles/-/sprinkles-1.6.3.tgz", - "integrity": "sha512-oCHlQeYOBIJIA2yWy2GnY5wE2A7hGHDyJplJo4lb+KEIBcJWRnDJDg8ywDwQS5VfWJrBBO3drzYZPFpWQjAMiQ==", - "license": "MIT", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@vanilla-extract/sprinkles/-/sprinkles-1.6.4.tgz", + "integrity": "sha512-lW3MuIcdIeHKX81DzhTnw68YJdL1ial05exiuvTLJMdHXQLKcVB93AncLPajMM6mUhaVVx5ALZzNHMTrq/U9Hg==", "peerDependencies": { "@vanilla-extract/css": "^1.0.0" } @@ -3815,6 +3862,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bufferutil": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", @@ -4131,6 +4184,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cookie-es": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", @@ -4188,10 +4250,9 @@ } }, "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "license": "BSD-2-Clause", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "engines": { "node": ">= 6" }, @@ -4217,6 +4278,140 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/cuer": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/cuer/-/cuer-0.0.2.tgz", + "integrity": "sha512-MG1BYnnSLqBnO0dOBS1Qm/TEc9DnFa9Sz2jMA24OF4hGzs8UuPjpKBMkRPF3lrpC+7b3EzULwooX9djcvsM8IA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "qr": "~0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -4330,6 +4525,11 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -4340,10 +4540,9 @@ } }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "license": "MIT", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -4364,14 +4563,12 @@ "node_modules/deep-object-diff": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", - "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", - "license": "MIT" + "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==" }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4489,6 +4686,15 @@ "node": ">=0.10.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4521,6 +4727,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/eciesjs": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.14.tgz", @@ -5612,6 +5827,14 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -6352,6 +6575,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -6897,7 +7128,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -6989,6 +7219,28 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -7005,6 +7257,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keccak": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", @@ -7148,6 +7421,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7156,6 +7470,12 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", @@ -7190,7 +7510,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -7227,7 +7546,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", - "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" } @@ -7347,8 +7665,7 @@ "node_modules/modern-ahocorasick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz", - "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==", - "license": "MIT" + "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==" }, "node_modules/motion": { "version": "10.16.2", @@ -8317,6 +8634,13 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postinstall-postinstall": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz", + "integrity": "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==", + "dev": true, + "hasInstallScript": true + }, "node_modules/preact": { "version": "10.26.4", "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.4.tgz", @@ -8368,7 +8692,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -8403,6 +8726,17 @@ "node": ">=6" } }, + "node_modules/qr": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/qr/-/qr-0.5.0.tgz", + "integrity": "sha512-LtnyJsepKCMzfmHBZKVNo1g29kS+8ZbuxE9294EsRhHgVVpy4x8eFw9o4J9SIolDHoDYuaEIY+z8UjiCv/eudA==", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/qrcode": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", @@ -8517,7 +8851,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-remove-scroll": { @@ -8567,6 +8900,20 @@ } } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -8589,6 +8936,21 @@ } } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -8643,6 +9005,46 @@ "node": ">= 12.13.0" } }, + "node_modules/recharts": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz", + "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -9639,10 +10041,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz", - "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==", - "license": "MIT", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -9800,6 +10201,11 @@ "real-require": "^0.1.0" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tinyglobby": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", @@ -10350,6 +10756,27 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/viem": { "version": "2.23.3", "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.3.tgz", diff --git a/package.json b/package.json index 370e2c6..a8b2b1f 100644 --- a/package.json +++ b/package.json @@ -11,29 +11,34 @@ "dependencies": { "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", - "@rainbow-me/rainbowkit": "^2.2.4", + "@rainbow-me/rainbowkit": "^2.2.8", "@tanstack/react-query": "^5.55.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cookie": "^1.0.2", "ethers": "^6.13.5", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.484.0", "next": "^15.1.4", "react": "^19.0.0", "react-dom": "^19.0.0", "react-error-boundary": "^5.0.0", "react-icons": "^5.5.0", - "tailwind-merge": "^3.0.2", + "recharts": "^2.15.3", + "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "viem": "2.23.3", "wagmi": "^2.14.11" }, "devDependencies": { "@shadcn/ui": "^0.0.4", + "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.14.8", "@types/react": "^19.0.6", "autoprefixer": "^10.4.21", "eslint-config-next": "^15.1.7", "postcss": "^8.4.35", + "postinstall-postinstall": "^2.1.0", "tailwindcss": "^3.4.1", "typescript": "5.5.4" } diff --git a/public/images/icons/sUSDS.svg b/public/images/icons/sUSDS.svg new file mode 100644 index 0000000..66835d0 --- /dev/null +++ b/public/images/icons/sUSDS.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/images/icons/usds.svg b/public/images/icons/usds.svg new file mode 100644 index 0000000..d3d5fca --- /dev/null +++ b/public/images/icons/usds.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/icons/usdt.svg b/public/images/icons/usdt.svg new file mode 100644 index 0000000..4174696 --- /dev/null +++ b/public/images/icons/usdt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/logo/logo.svg b/public/images/logo/logo.svg new file mode 100644 index 0000000..063daa6 --- /dev/null +++ b/public/images/logo/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b73b7d5..5b3fa6b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,24 @@ import { TooltipProvider } from "@/components/ui/tooltip" import { ReactNode } from "react" +export const metadata = { + title: 'Advanced Yield Platform', + description: 'Advanced Yield Platform for DeFi', + icons: { + icon: '/images/logo/logo.svg', + shortcut: '/images/logo/logo.svg', + apple: '/images/logo/logo.svg', + }, +} + export default function RootLayout({ children }: { children: ReactNode }) { return ( - + + + + + + {children} diff --git a/src/components/deposit-view.tsx b/src/components/deposit-view.tsx index 7760186..2f3f23b 100644 --- a/src/components/deposit-view.tsx +++ b/src/components/deposit-view.tsx @@ -14,6 +14,8 @@ import { useWatchContractEvent, useTransaction, useReadContracts, + useSwitchChain, + useChainId } from "wagmi"; import { USD_STRATEGIES, BTC_STRATEGIES, ETH_STRATEGIES } from "../config/env"; import { @@ -24,6 +26,7 @@ import { http, parseUnits, } from "viem"; +import { RATE_PROVIDER_ABI } from "../config/abi/rateProvider"; type DurationType = "30_DAYS" | "60_DAYS" | "180_DAYS" | "PERPETUAL_DURATION"; type StrategyType = "STABLE" | "INCENTIVE"; @@ -31,20 +34,41 @@ type StrategyType = "STABLE" | "INCENTIVE"; interface StrategyConfig { network: string; contract: string; - deposit_token: string; - deposit_contract: string; - deposit_token_contract?: string; // Optional field for backward compatibility - deposit_token_image?: string; // Optional field for deposit token image - deposit_token_decimal?: number; // Optional field for deposit token decimal + base: { + tokens: Array<{ + name: string; + contract: string; + decimal: number; + image: string; + }>; + }; + ethereum: { + tokens: Array<{ + name: string; + contract: string; + decimal: number; + image: string; + }>; + }; + arbitrum: { + tokens: Array<{ + name: string; + contract: string; + decimal: number; + image: string; + }>; + }; description: string; apy: string; incentives: string; tvl: string; - rpc?: string; // Optional field for backward compatibility + rpc?: string; show_cap: boolean; filled_cap: string; cap_limit: string; - boringVaultAddress?: string; // Optional field for boring vault address + boringVaultAddress?: string; + rateProvider: string; + shareAddress: string; } // ERC20 ABI for token operations @@ -136,10 +160,37 @@ const VAULT_ABI = [ inputs: [ { name: "depositAsset", type: "address" }, { name: "depositAmount", type: "uint256" }, - { name: "minimumMint", type: "uint256" } + { name: "minimumMint", type: "uint256" }, ], outputs: [{ name: "shares", type: "uint256" }], }, + { + name: "depositAndBridge", + type: "function", + stateMutability: "payable", + inputs: [ + { name: "depositAsset", type: "address" }, + { name: "depositAmount", type: "uint256" }, + { name: "minimumMint", type: "uint256" }, + { name: "to", type: "address" }, + { name: "bridgeWildCard", type: "bytes" }, + { name: "feeToken", type: "address" }, + { name: "maxFee", type: "uint256" }, + ], + outputs: [{ name: "shares", type: "uint256" }], + }, + { + name: "previewFee", + type: "function", + stateMutability: "view", + inputs: [ + { name: "shareAmount", type: "uint96" }, + { name: "to", type: "address" }, + { name: "bridgeWildCard", type: "bytes" }, + { name: "feeToken", type: "address" }, + ], + outputs: [{ name: "fee", type: "uint256" }], + }, { name: "totalAssets", type: "function", @@ -220,6 +271,12 @@ const DepositView: React.FC = ({ | "depositing" | "idle" >("idle"); + const [isMultiChain, setIsMultiChain] = useState(false); + const [bridgeFee, setBridgeFee] = useState("0"); + const [isLoadingFee, setIsLoadingFee] = useState(false); + const [targetChain, setTargetChain] = useState("arbitrum"); // Default target chain + const { switchChain } = useSwitchChain(); + const { chain } = useAccount(); // Get connected chain info // Get strategy config based on asset type const strategyConfigs = { @@ -229,13 +286,39 @@ const DepositView: React.FC = ({ }; // Explicitly access the strategies for the selected asset first - const assetStrategies = strategyConfigs[selectedAsset as keyof typeof strategyConfigs]; + const assetStrategies = + strategyConfigs[selectedAsset as keyof typeof strategyConfigs]; // Now access the specific duration and strategy type - const strategyConfig = (assetStrategies as any)[duration][ // Cast assetStrategies to any for indexing + const strategyConfig = (assetStrategies as any)[duration][ strategy === "stable" ? "STABLE" : "INCENTIVE" ] as StrategyConfig; + // Get the appropriate network tokens based on the selected target chain + const getNetworkTokens = () => { + switch (targetChain) { + case "arbitrum": + return strategyConfig.arbitrum.tokens; + case "ethereum": + return strategyConfig.ethereum.tokens; + case "base": + default: + return strategyConfig.base.tokens; + } + }; + + // Parse all available deposit assets from strategyConfig, filtered by targetChain + const assetOptions = useMemo(() => { + return getNetworkTokens(); + }, [strategyConfig, targetChain]); + + const [selectedAssetIdx, setSelectedAssetIdx] = useState(0); + const selectedAssetOption = assetOptions[selectedAssetIdx] || assetOptions[0]; + + // Update token contract address and decimals + const tokenContractAddress = selectedAssetOption.contract; + const depositTokenDecimals = selectedAssetOption.decimal; + // Calculate deposit cap values from env config const showDepositCap = strategyConfig.show_cap; const depositCap = useMemo( @@ -262,14 +345,6 @@ const DepositView: React.FC = ({ const { address } = useAccount(); - // Get deposit token name, image, and decimals (only if defined in config) - const depositToken = strategyConfig.deposit_token; - const depositTokenImage = strategyConfig.deposit_token_image; - const depositTokenDecimals = strategyConfig.deposit_token_decimal || 6; - - // USDC token contract for approvals - const tokenContractAddress = - strategyConfig.deposit_token_contract || strategyConfig.deposit_contract; // Vault contract for deposits const vaultContractAddress = strategyConfig.contract; @@ -297,9 +372,14 @@ const DepositView: React.FC = ({ allowance: allowance?.toString(), hasAllowance: !!allowance, amount: amount ? parseUnits(amount, depositTokenDecimals).toString() : "0", - needsApproval: amount ? (BigInt(allowance?.toString() || "0") < parseUnits(amount, depositTokenDecimals)) : false, - currentAllowanceFormatted: allowance ? formatUnits(BigInt(allowance.toString()), depositTokenDecimals) : "0", - requestedAmountFormatted: amount || "0" + needsApproval: amount + ? BigInt(allowance?.toString() || "0") < + parseUnits(amount, depositTokenDecimals) + : false, + currentAllowanceFormatted: allowance + ? formatUnits(BigInt(allowance.toString()), depositTokenDecimals) + : "0", + requestedAmountFormatted: amount || "0", }); // Watch approve transaction @@ -324,7 +404,7 @@ const DepositView: React.FC = ({ useEffect(() => { const checkTokenContract = async () => { if (!tokenContractAddress || !address) return; - + try { const client = createPublicClient({ transport: http(strategyConfig.rpc || "https://base.llamarpc.com"), @@ -338,29 +418,39 @@ const DepositView: React.FC = ({ symbol: "ETH", }, rpcUrls: { - default: { http: [strategyConfig.rpc || "https://base.llamarpc.com"] }, - public: { http: [strategyConfig.rpc || "https://base.llamarpc.com"] }, + default: { + http: [strategyConfig.rpc || "https://base.llamarpc.com"], + }, + public: { + http: [strategyConfig.rpc || "https://base.llamarpc.com"], + }, }, }, }); // Try to read basic token info const [name, symbol, decimals] = await Promise.all([ - client.readContract({ - address: tokenContractAddress as Address, - abi: ERC20_ABI, - functionName: "name", - }).catch(() => "Error reading name"), - client.readContract({ - address: tokenContractAddress as Address, - abi: ERC20_ABI, - functionName: "symbol", - }).catch(() => "Error reading symbol"), - client.readContract({ - address: tokenContractAddress as Address, - abi: ERC20_ABI, - functionName: "decimals", - }).catch(() => "Error reading decimals"), + client + .readContract({ + address: tokenContractAddress as Address, + abi: ERC20_ABI, + functionName: "name", + }) + .catch(() => "Error reading name"), + client + .readContract({ + address: tokenContractAddress as Address, + abi: ERC20_ABI, + functionName: "symbol", + }) + .catch(() => "Error reading symbol"), + client + .readContract({ + address: tokenContractAddress as Address, + abi: ERC20_ABI, + functionName: "decimals", + }) + .catch(() => "Error reading decimals"), ]); console.log("Token contract debug info:", { @@ -418,10 +508,10 @@ const DepositView: React.FC = ({ console.log("Deposit successful!", { hash: transactionHash, amount, - token: depositToken, + token: selectedAssetOption.name, }); } - }, [isDepositSuccess, transactionHash, amount, depositToken]); + }, [isDepositSuccess, transactionHash, amount, selectedAssetOption.name]); useEffect(() => { const checkApproval = async () => { @@ -475,6 +565,85 @@ const DepositView: React.FC = ({ console.log("Status changed:", status); }, [status]); + // Add preview fee function + const previewBridgeFee = async (amount: bigint) => { + if (!address || !amount || !isMultiChain) return; + + setIsLoadingFee(true); + try { + const client = createPublicClient({ + transport: http(strategyConfig.rpc || "https://base.llamarpc.com"), + chain: { + id: 8453, + name: "Base", + network: "base", + nativeCurrency: { + decimals: 18, + name: "Ethereum", + symbol: "ETH", + }, + rpcUrls: { + default: { + http: [strategyConfig.rpc || "https://base.llamarpc.com"], + }, + public: { + http: [strategyConfig.rpc || "https://base.llamarpc.com"], + }, + }, + }, + }); + + // Get bridge wildcard based on target chain + const bridgeWildCard = getBridgeWildCard(targetChain); + + // Convert amount to uint96 for previewFee + const shareAmount = amount as unknown as bigint; + + // Call previewFee function with exact parameters from your example + const fee = await client.readContract({ + address: vaultContractAddress as Address, + abi: VAULT_ABI, + functionName: "previewFee", + args: [ + shareAmount, // shareAmount (uint96) + address as Address, // to address + bridgeWildCard, // bridgeWildCard bytes + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as Address, // feeToken (ETH address) + ], + }); + + console.log("Bridge fee calculation:", { + shareAmount: shareAmount.toString(), + to: address, + bridgeWildCard, + feeToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + calculatedFee: fee.toString(), + }); + + setBridgeFee(formatUnits(fee as bigint, 18)); + } catch (error) { + console.error("Error previewing bridge fee:", error); + setBridgeFee("0"); + } finally { + setIsLoadingFee(false); + } + }; + + // Helper function to get bridge wildcard + const getBridgeWildCard = (chain: string): `0x${string}` => { + switch (chain) { + case "arbitrum": + return "0x000000000000000000000000000000000000000000000000000000000000759e"; + case "optimism": + return "0x000000000000000000000000000000000000000000000000000000000000759f"; + case "ethereum": + return "0x000000000000000000000000000000000000000000000000000000000000759d"; + default: + return "0x000000000000000000000000000000000000000000000000000000000000759e"; + } + }; + + // Modify handleDeposit function const handleDeposit = async () => { console.log("Deposit clicked", { address, @@ -485,6 +654,8 @@ const DepositView: React.FC = ({ duration, currentAllowance: allowance?.toString(), isApproved, + isMultiChain, + targetChain, }); if (!address || !amount || !approve || !deposit) { @@ -504,8 +675,63 @@ const DepositView: React.FC = ({ throw new Error("Invalid amount"); } - const roundedAmount = Math.round(amountFloat * Math.pow(10, depositTokenDecimals)) / Math.pow(10, depositTokenDecimals); - const amountInWei = parseUnits(roundedAmount.toFixed(depositTokenDecimals), depositTokenDecimals); + const roundedAmount = + Math.round(amountFloat * Math.pow(10, depositTokenDecimals)) / + Math.pow(10, depositTokenDecimals); + const amountInWei = parseUnits( + roundedAmount.toFixed(depositTokenDecimals), + depositTokenDecimals + ); + + // Get rate from rate provider + const rateProviderAddress = strategyConfig.rateProvider; + + const client = createPublicClient({ + transport: http(strategyConfig.rpc || "https://base.llamarpc.com"), + chain: { + id: 8453, + name: "Base", + network: "base", + nativeCurrency: { + decimals: 18, + name: "Ethereum", + symbol: "ETH", + }, + rpcUrls: { + default: { + http: [strategyConfig.rpc || "https://base.llamarpc.com"], + }, + public: { + http: [strategyConfig.rpc || "https://base.llamarpc.com"], + }, + }, + }, + }); + + // Get rate from rate provider using the deposit token address + const rate = await client.readContract({ + address: rateProviderAddress as Address, + abi: RATE_PROVIDER_ABI, + functionName: "getRateInQuote", + args: [selectedAssetOption.contract as Address], + }); + + console.log("Raw rate from contract:", rate.toString()); + + // Calculate minimum mint amount in 6 decimals + // First multiply by rate, then divide by 1e18 to get 6 decimals + const minimumMint = (amountInWei * BigInt(rate)) / BigInt(1e18); + + // Convert to exactly 6 decimals by multiplying by 1e6 and dividing by 1e18 + const minimumMintIn6Decimals = (minimumMint * BigInt(1e6)) / BigInt(1e18); + + console.log("Minimum mint calculation details:", { + amountInWei: amountInWei.toString(), + rate: rate.toString(), + minimumMint: minimumMint.toString(), + minimumMintIn6Decimals: minimumMintIn6Decimals.toString(), + minimumMintLength: minimumMintIn6Decimals.toString().length, + }); // First approve USDS for the boring vault const boringVaultAddress = strategyConfig.boringVaultAddress; @@ -514,36 +740,22 @@ const DepositView: React.FC = ({ } // Check if we need approval - const currentAllowance = allowance ? BigInt(allowance.toString()) : BigInt(0); + const currentAllowance = allowance + ? BigInt(allowance.toString()) + : BigInt(0); const needsApproval = currentAllowance < amountInWei; - console.log("Approval check:", { - currentAllowance: currentAllowance.toString(), - amountInWei: amountInWei.toString(), - needsApproval, - isApproved, - isApproving - }); - if (allowance === undefined) { - setErrorMessage('Unable to fetch allowance. Please check your network and try again.'); + setErrorMessage( + "Unable to fetch allowance. Please check your network and try again." + ); setIsWaitingForSignature(false); return; } // Step 1: Approve USDS for boring vault if needed if (needsApproval && !isApproved && !isApproving) { - console.log('Calling approve function...'); - console.log("Sending approve transaction for boring vault...", { - tokenContract: tokenContractAddress, - spender: boringVaultAddress, - amount: amountInWei.toString(), - functionName: "approve", - abi: ERC20_ABI, - chainId: 8453, - account: address - }); - + console.log("Calling approve function..."); setIsApproving(true); try { const approveTx = await approve({ @@ -555,23 +767,11 @@ const DepositView: React.FC = ({ account: address as Address, }); - console.log("Approval transaction sent:", { - hash: approveTx, - tokenContract: tokenContractAddress, - spender: boringVaultAddress, - amount: amountInWei.toString() - }); - if (typeof approveTx === "string" && approveTx.startsWith("0x")) { setApprovalHash(approveTx as `0x${string}`); } } catch (error: any) { - console.error("Approval transaction failed:", { - error: error.message, - tokenContract: tokenContractAddress, - spender: boringVaultAddress, - amount: amountInWei.toString() - }); + console.error("Approval transaction failed:", error); setIsApproving(false); setErrorMessage(error.message || "Approval failed"); } @@ -587,34 +787,90 @@ const DepositView: React.FC = ({ // Only proceed with deposit if we have sufficient allowance if (!needsApproval || isApproved) { - // Step 2: Proceed with deposit to LayerZeroTeller - console.log("Proceeding with deposit to LayerZeroTeller"); setIsDepositing(true); - // Calculate minimum mint amount based on slippage - const slippageAmount = amountInWei * BigInt(Math.floor(parseFloat(slippage) * 10000)) / BigInt(10000); - const minimumMint = amountInWei - slippageAmount; + if (isMultiChain) { + // Preview bridge fee before proceeding + await previewBridgeFee(amountInWei); - // Proceed with deposit using the LayerZeroTeller contract - console.log("Sending deposit transaction to LayerZeroTeller:", { - contract: vaultContractAddress, - token: tokenContractAddress, - amount: amountInWei.toString(), - minimumMint: minimumMint.toString(), - }); + // Get bridge wildcard + const bridgeWildCard = getBridgeWildCard(targetChain); - const tx = await deposit({ - address: vaultContractAddress as Address, - abi: VAULT_ABI, - functionName: "deposit", - args: [tokenContractAddress as Address, amountInWei, minimumMint], - chainId: 8453, - account: address as Address, - }); + // Convert bridge fee to wei + const bridgeFeeWei = parseEther(bridgeFee); + + // Proceed with multi-chain deposit + console.log("Sending multi-chain deposit transaction:", { + contract: vaultContractAddress, + token: tokenContractAddress, + amount: amountInWei.toString(), + minimumMint: "0", // Set minimumMint to 0 for multi-chain + bridgeWildCard, + bridgeFee: bridgeFeeWei.toString(), + }); + + try { + const tx = await deposit({ + address: vaultContractAddress as Address, + abi: VAULT_ABI, + functionName: "depositAndBridge", + args: [ + tokenContractAddress as Address, + amountInWei, + BigInt(0), // Set minimumMint to 0 for multi-chain + address as Address, + bridgeWildCard, + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as Address, // ETH address + bridgeFeeWei, // Use the calculated bridge fee + ], + chainId: 8453, + account: address as Address, + value: bridgeFeeWei, // Include the calculated bridge fee in ETH + }); + + if (tx && typeof tx === "string" && tx.startsWith("0x")) { + setTransactionHash(tx as `0x${string}`); + console.log("Multi-chain deposit transaction sent:", tx); + } else { + throw new Error("Invalid transaction response"); + } + } catch (error: any) { + console.error("Multi-chain deposit failed:", error); + setErrorMessage(error.message || "Multi-chain deposit failed"); + setIsDepositing(false); + return; + } + } else { + // Regular single-chain deposit + console.log("Sending deposit transaction:", { + contract: vaultContractAddress, + token: tokenContractAddress, + amount: amountInWei.toString(), + minimumMint: minimumMintIn6Decimals.toString(), + }); - if (tx && typeof tx === "string" && tx.startsWith("0x")) { - setTransactionHash(tx as `0x${string}`); - setIsDepositing(true); + try { + const tx = await deposit({ + address: vaultContractAddress as Address, + abi: VAULT_ABI, + functionName: "deposit", + args: [tokenContractAddress as Address, amountInWei, minimumMintIn6Decimals], + chainId: 8453, + account: address as Address, + }); + + if (tx && typeof tx === "string" && tx.startsWith("0x")) { + setTransactionHash(tx as `0x${string}`); + console.log("Deposit transaction sent:", tx); + } else { + throw new Error("Invalid transaction response"); + } + } catch (error: any) { + console.error("Deposit failed:", error); + setErrorMessage(error.message || "Deposit failed"); + setIsDepositing(false); + return; + } } } else { console.log("Insufficient allowance, approval needed first"); @@ -630,101 +886,90 @@ const DepositView: React.FC = ({ } }; + // Add effect to preview fee when amount changes + useEffect(() => { + if (isMultiChain && amount) { + const amountInWei = parseUnits(amount, depositTokenDecimals); + previewBridgeFee(amountInWei); + } + }, [amount, isMultiChain, targetChain]); + + // Helper to get correct RPC and chain config for each chain + const getChainConfig = (chain: string) => { + switch (chain) { + case "arbitrum": + return { + rpcUrl: "https://arbitrum.drpc.org", + chain: { + id: 42161, + name: "Arbitrum", + network: "arbitrum", + nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" }, + rpcUrls: { + default: { http: ["https://arbitrum.drpc.org"] }, + public: { http: ["https://arbitrum.drpc.org"] }, + }, + }, + }; + case "ethereum": + return { + rpcUrl: "https://eth.llamarpc.com", + chain: { + id: 1, + name: "Ethereum", + network: "ethereum", + nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" }, + rpcUrls: { + default: { http: ["https://eth.llamarpc.com"] }, + public: { http: ["https://eth.llamarpc.com"] }, + }, + }, + }; + case "base": + default: + return { + rpcUrl: "https://base.llamarpc.com", + chain: { + id: 8453, + name: "Base", + network: "base", + nativeCurrency: { decimals: 18, name: "Ethereum", symbol: "ETH" }, + rpcUrls: { + default: { http: ["https://base.llamarpc.com"] }, + public: { http: ["https://base.llamarpc.com"] }, + }, + }, + }; + } + }; + const fetchBalance = async () => { - if (!address || selectedAsset !== "USD") return; + if (!address) return; setIsLoadingBalance(true); try { - // Use correct USDS token contract address on Base - const tokenContractAddress = "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc" as Address; - const rpcUrl = "https://base.llamarpc.com"; - - console.log("Fetching balance for:", { - tokenContract: tokenContractAddress, - userAddress: address, - network: "base", - rpc: rpcUrl - }); + const tokenContractAddress = selectedAssetOption.contract as Address; + const decimals = Number(selectedAssetOption.decimal); + const { rpcUrl, chain } = getChainConfig(targetChain); const client = createPublicClient({ transport: http(rpcUrl), - chain: { - id: 8453, - name: "Base", - network: "base", - nativeCurrency: { - decimals: 18, - name: "Ethereum", - symbol: "ETH", - }, - rpcUrls: { - default: { http: [rpcUrl] }, - public: { http: [rpcUrl] }, - }, - }, + chain, }); - // First try to get token info to verify contract - try { - const [name, symbol, decimals] = await Promise.all([ - client.readContract({ - address: tokenContractAddress, - abi: ERC20_ABI, - functionName: "name", - }).catch(() => "Error reading name"), - client.readContract({ - address: tokenContractAddress, - abi: ERC20_ABI, - functionName: "symbol", - }).catch(() => "Error reading symbol"), - client.readContract({ - address: tokenContractAddress, - abi: ERC20_ABI, - functionName: "decimals", - }).catch(() => "Error reading decimals"), - ]); - - console.log("Token contract info:", { - address: tokenContractAddress, - name, - symbol, - decimals, - }); - } catch (error) { - console.error("Error reading token info:", error); - } - - // Then try to get balance - const [balanceResult, decimalsResult] = await Promise.all([ - client.readContract({ - address: tokenContractAddress, - abi: ERC20_ABI, - functionName: "balanceOf", - args: [address as Address], - }).catch(error => { - console.error("Error reading balance:", error); - return BigInt(0); - }), - client.readContract({ - address: tokenContractAddress, - abi: ERC20_ABI, - functionName: "decimals", - }).catch(error => { - console.error("Error reading decimals:", error); - return 6; // Default to 6 decimals for USDS - }), - ]); - - console.log("Balance results:", { - rawBalance: balanceResult?.toString(), - decimals: decimalsResult, + const balanceResult = await client.readContract({ + address: tokenContractAddress, + abi: ERC20_ABI, + functionName: "balanceOf", + args: [address as Address], }); - const formattedBalance = formatUnits( - balanceResult as bigint, - decimalsResult as number - ); - console.log("Formatted balance:", formattedBalance); + const formattedBalance = Number( + formatUnits(balanceResult as bigint, decimals) + ).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); setBalance(formattedBalance); } catch (error) { console.error("Error fetching balance:", error); @@ -734,12 +979,54 @@ const DepositView: React.FC = ({ } }; - // Initial balance fetch + // Add effect to switch network when target chain changes useEffect(() => { - if (address && selectedAsset === "USD") { + if (switchChain) { + if (isMultiChain && targetChain) { + const chainId = getChainId(targetChain); + if (chainId) { + switchChain({ chainId }); + } + } else if (!isMultiChain) { + // Always switch to Base when multi-chain is off + switchChain({ chainId: 8453 }); + } + } + }, [targetChain, isMultiChain, switchChain]); + + // Helper function to get chain ID + const getChainId = (chain: string): number | undefined => { + switch (chain) { + case "arbitrum": + return 42161; + case "optimism": + return 10; + case "ethereum": + return 1; + default: + return undefined; + } + }; + + useEffect(() => { + if (address) { fetchBalance(); } - }, [address, selectedAsset, duration, strategy]); + }, [ + address, + selectedAssetIdx, + selectedAsset, + duration, + strategy, + targetChain, + ]); + + // Update targetChain when connected chain changes + useEffect(() => { + if (chain) { + setTargetChain(chain.name.toLowerCase()); + } + }, [chain]); const handleMaxClick = () => { setAmount(balance); @@ -754,7 +1041,7 @@ const DepositView: React.FC = ({ }; return ( -
+
{depositSuccess ? (
@@ -829,7 +1116,7 @@ const DepositView: React.FC = ({
Amount - {amount} {depositToken} + {amount} {selectedAssetOption.name}
@@ -843,28 +1130,84 @@ const DepositView: React.FC = ({
) : (
-
-
+
+
{/* Left Card - Deposit Input */} -
+
- {depositTokenImage && ( + {selectedAssetOption.image && ( {depositToken} )} - Deposit {depositToken} + Deposit {selectedAssetOption.name} +0.00 in 1 year
+ {/* --- Asset Dropdown & Multi-chain Toggle --- */} + {assetOptions.length > 1 && ( +
+ + +
+ )} + {/* Multi-chain Toggle (always shown) */} +
+ + Multi-chain Deposit + + +
+ {/* Target Chain Selection - Only shown when multi-chain is enabled */} + {isMultiChain && ( +
+ + +
+ )} + {/* --- End Asset Dropdown & Multi-chain Toggle --- */}
= ({ )}
+ {/* Bridge Fee Display */} + {isMultiChain && ( +
+ + Bridge Fee:{" "} + {isLoadingFee ? ( + + + + + + Loading... + + ) : ( + {bridgeFee} ETH + )} + +
+
+ + + + + You need to have enough ETH in your wallet to cover + the bridge fee. The fee will be paid in ETH along + with your deposit. + +
+
+
+ )}
{/* Right Card - Strategy Info */} -
+
{/* Background gradient effect - top */}
@@ -979,7 +1382,7 @@ const DepositView: React.FC = ({ {/* Deposit Cap Progress Bar - Only shown if show_cap is true */} {showDepositCap && ( -
+
${remainingSpace} Remaining @@ -1036,7 +1439,7 @@ const DepositView: React.FC = ({ diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 3a96d3c..5f8bd0d 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -6,6 +6,7 @@ import { TooltipTrigger, TooltipProvider, } from "@/components/ui/tooltip"; +import { InfoIcon } from "lucide-react"; type DurationType = "30_DAYS" | "60_DAYS" | "180_DAYS" | "PERPETUAL_DURATION"; @@ -26,26 +27,9 @@ interface CustomCardProps { disableHover?: boolean; onReset?: () => void; isComingSoon?: boolean; + availableDurations?: DurationType[]; } -const InfoIcon = () => ( - - - -); - const formatDuration = (duration: string) => { if (duration === "PERPETUAL_DURATION") return "Perpetual"; const [number, period] = duration.split("_"); @@ -66,6 +50,7 @@ const CustomCard: React.FC = ({ disableHover, onReset, isComingSoon, + availableDurations, ...props }) => { const handleDurationClick = (duration: DurationType) => { @@ -78,6 +63,13 @@ const CustomCard: React.FC = ({ e.stopPropagation(); // Stop event from bubbling up to parent card }; + // Helper function to check if a duration is available + const isDurationAvailable = (duration: DurationType) => { + // If availableDurations is not provided, assume all are available + if (!availableDurations) return true; + return availableDurations.includes(duration); + }; + return (
= ({ {...props} > {isStrategyCard ? ( -
+
+ {/* Coming Soon Overlay */} + {isComingSoon && ( +
+ Coming Soon +
+ )} {/* Image */}
= ({ Select Duration

- - - - + {/* 30 Days Button */} + + + + + + {!isDurationAvailable("30_DAYS") && ( + +

Coming Soon

+
+ )} +
+
+ + {/* 60 Days Button */} + + + + + + {!isDurationAvailable("60_DAYS") && ( + +

Coming Soon

+
+ )} +
+
+ + {/* 180 Days Button */} + + + + + + {!isDurationAvailable("180_DAYS") && ( + +

Coming Soon

+
+ )} +
+
+ + {/* Perpetual Button */} + + + + + + {!isDurationAvailable("PERPETUAL_DURATION") && ( + +

Coming Soon

+
+ )} +
+
)} diff --git a/src/components/ui/header.tsx b/src/components/ui/header.tsx index 064d2e3..6830b7d 100644 --- a/src/components/ui/header.tsx +++ b/src/components/ui/header.tsx @@ -42,7 +42,7 @@ export function Header({
{shouldShowBanner && (
@@ -112,7 +112,7 @@ export function Header({
)} -
+
{children}
diff --git a/src/components/ui/markets-table.tsx b/src/components/ui/markets-table.tsx index b3fc489..c34c69d 100644 --- a/src/components/ui/markets-table.tsx +++ b/src/components/ui/markets-table.tsx @@ -145,7 +145,7 @@ const MarketsTable: React.FC = ({ return (
{/* Table Header */} -
+
= ({
onRowClick && onRowClick(item)} - className="cursor-pointer pl-[32px]" + className="cursor-pointer px-[16px] sm:pl-[32px]" >
= ({ {/* Toggle buttons */} -
+
@@ -250,16 +250,16 @@ const YieldDetailsView: React.FC = ({ ); return ( -
+
-
-

+
+

{name}

-
+
@@ -267,30 +267,38 @@ const YieldDetailsView: React.FC = ({
{/* Stats */} -
-
-
TVL
-
{tvl}
+
+ {/* TVL */} +
+
TVL
+
{tvl}
-
-
Base APY
-
{baseApy}
+ + {/* Base APY */} +
+
Base APY
+
{baseApy}
-
-
Contract Address
-
+ + {/* Contract Address */} +
+
Contract Address
+
{contractAddress}
-
-
Network
-
{network}
+ + {/* Network */} +
+
Network
+
{network}
+ {/* Tabs */}
{ const getAssetStrategies = (asset: AssetType) => { - const strategies: StrategyAsset = { - USD: USD_STRATEGIES, - BTC: BTC_STRATEGIES, - ETH: ETH_STRATEGIES, - }[asset]; + const strategies: Record>> = { + USD: USD_STRATEGIES as unknown as Partial>, + BTC: BTC_STRATEGIES as unknown as Partial>, + ETH: ETH_STRATEGIES as unknown as Partial>, + }; - // Use the duration as is since it matches the keys in env.ts - const durationKey = duration; - const strategy = strategies[durationKey]; + const strategy = strategies[asset][duration]; if (!strategy) { console.error( - `No strategy found for ${asset} with duration ${durationKey}` + `No strategy found for ${asset} with duration ${duration}` ); return { stable: { @@ -92,6 +123,7 @@ const getStrategyInfo = (duration: DurationType): StrategyData => { value: "0%", info: "-", }, + comingSoon: true, }, }; } @@ -110,6 +142,7 @@ const getStrategyInfo = (duration: DurationType): StrategyData => { value: strategy.INCENTIVE.apy, info: strategy.INCENTIVE.incentives, }, + comingSoon: strategy.INCENTIVE.comingSoon, }, }; }; @@ -117,45 +150,41 @@ const getStrategyInfo = (duration: DurationType): StrategyData => { return { stable: { USD: getAssetStrategies("USD").stable, - BTC: getAssetStrategies("BTC").stable, ETH: getAssetStrategies("ETH").stable, + BTC: getAssetStrategies("BTC").stable, }, incentives: { USD: getAssetStrategies("USD").incentives, - BTC: getAssetStrategies("BTC").incentives, ETH: getAssetStrategies("ETH").incentives, + BTC: getAssetStrategies("BTC").incentives, }, }; }; const YieldSubpage: React.FC = ({ depositParams }) => { const [selectedAsset, setSelectedAsset] = useState(null); - const [selectedStrategy, setSelectedStrategy] = useState(null); + const [selectedStrategy, setSelectedStrategy] = useState( + null + ); - // Add effect to handle URL parameters or parent navigation useEffect(() => { - if (depositParams?.asset && depositParams?.duration) { + if (depositParams) { + const apy = + getStrategyInfo(depositParams.duration as DurationType)[ + depositParams.strategy === "stable" ? "stable" : "incentives" + ][depositParams.asset as AssetType].apy.value; + setSelectedAsset({ asset: depositParams.asset, duration: depositParams.duration as DurationType, }); - if (depositParams.strategy) { - const strategyInfo = getStrategyInfo( - depositParams.duration as DurationType - ); - const apy = - strategyInfo[ - depositParams.strategy === "stable" ? "stable" : "incentives" - ][depositParams.asset as AssetType].apy.value; - - setSelectedStrategy({ - type: depositParams.strategy as "stable" | "incentive", - asset: depositParams.asset, - duration: depositParams.duration as DurationType, - apy, - }); - } + setSelectedStrategy({ + type: depositParams.strategy as "stable" | "incentive", + asset: depositParams.asset, + duration: depositParams.duration as DurationType, + apy, + }); } }, [depositParams]); @@ -187,7 +216,7 @@ const YieldSubpage: React.FC = ({ depositParams }) => { // Always render the main content, assuming verification is handled by parent return (
= ({ depositParams }) => { /> ) : selectedAsset ? (
-

Select a Yield Source

-
+

Select a Yield Source

+
= ({ depositParams }) => { selectedDuration={selectedAsset.duration} onReset={handleReset} disableHover={true} - className="h-[311px]" + className="h-[311px] w-full" /> -
+
handleStrategySelect( @@ -256,13 +285,25 @@ const YieldSubpage: React.FC = ({ depositParams }) => { />
- handleStrategySelect( - "incentive", + {...(getStrategyInfo(selectedAsset.duration).incentives[ + selectedAsset.asset as AssetType + ].comingSoon + ? {} + : { + onClick: () => + handleStrategySelect( + "incentive", + selectedAsset.asset as AssetType + ), + })} + className={ + "group " + + (getStrategyInfo(selectedAsset.duration).incentives[ selectedAsset.asset as AssetType - ) + ].comingSoon + ? "pointer-events-none opacity-60" + : "cursor-pointer") } - className="cursor-pointer" > = ({ depositParams }) => { } isStrategyCard={true} disableHover={true} + isComingSoon={ + getStrategyInfo(selectedAsset.duration).incentives[ + selectedAsset.asset as AssetType + ].comingSoon === true + } />
@@ -288,10 +334,10 @@ const YieldSubpage: React.FC = ({ depositParams }) => {
) : (
-

+

Select a asset you want yield on

-
+
= ({ depositParams }) => { onDurationSelect={(duration: DurationType) => handleDurationSelect("USD", duration) } + availableDurations={["PERPETUAL_DURATION"]} /> typeof window !== "undefined" && window.innerWidth < 640; type AssetType = "ALL" | "USD" | "ETH" | "BTC"; @@ -108,6 +111,7 @@ const MarketsSubpage: React.FC = () => { } ], }; + const router = useRouter(); // Fill the "ALL" category marketData.ALL = [...marketData.ETH, ...marketData.BTC, ...marketData.USD]; @@ -124,7 +128,21 @@ const MarketsSubpage: React.FC = () => { // Handler for row clicks const handleRowClick = (item: MarketItem) => { - setSelectedItem(item); + console.log("Row clicked:", item); + if (isMobile()) { + router.push({ + pathname: `/yield/${item.id}`, + query: { + name: item.name, + tvl: item.tvl, + baseApy: item.baseYield, + contractAddress: item.contractAddress || "", + network: item.network || "" + }, + }); + } else { + setSelectedItem(item); + } }; // Get sorted data @@ -172,10 +190,10 @@ const MarketsSubpage: React.FC = () => { }; return ( -
+
{/* Left side - 50% */} -
-
+
+
{
{/* Asset Selection */} -
-
- - - - +
+
+ + + +
@@ -251,7 +253,7 @@ const MarketsSubpage: React.FC = () => {
{/* Right side - 50% */} -
+
{selectedItem ? ( { - if (code === process.env.NEXT_PUBLIC_BETA_ACCESS_PUBLIC_CODE) { - setIsVerified(true); - setIsCodePopupOpen(false); - setVerificationError(""); - } else { - setVerificationError("Incorrect code. Please try again."); + // Function to check for an existing session (will need a backend API to read the cookie) + const checkSession = async () => { + try { + const response = await fetch('/api/check-session'); // We will need to create this API route + if (response.ok) { + const data = await response.json(); + if (data.isValid) { + setIsVerified(true); + setIsCodePopupOpen(false); + return true; + } + } + } catch (error) { + console.error("Error checking session:", error); + } + setIsVerified(false); + setIsCodePopupOpen(true); + return false; + }; + + // useEffect to check session on component mount + useEffect(() => { + checkSession(); + }, []); // Run only once on mount + + const handleVerifyCode = async (code: string) => { + try { + const response = await fetch('/api/verify-code', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ accessCode: code }), + }); + + if (response.ok) { + setIsVerified(true); + setIsCodePopupOpen(false); + setVerificationError(""); // Clear any previous errors + } else { + const data = await response.json(); + setVerificationError(data.message || "Incorrect code. Please try again."); + setIsVerified(false); + setIsCodePopupOpen(true); + } + } catch (error) { + console.error("Error verifying code:", error); + setVerificationError("An error occurred during verification."); + setIsVerified(false); + setIsCodePopupOpen(true); } }; @@ -73,79 +117,147 @@ export default function Page() { return (
-
-
-
{ - setSelectedSubPage(SubPage.Yield); - setDepositParams(null); - }} - > - Lucidity Logo -
-
-
-
- +
+ +
+
+ + +
- + +
+ {isMobileMenuOpen && ( +
+ + + + + +
+ )} +
+
{renderSubPage()}
diff --git a/src/pages/portfolio-subpage.tsx b/src/pages/portfolio-subpage.tsx index 6aa65f9..123f66e 100644 --- a/src/pages/portfolio-subpage.tsx +++ b/src/pages/portfolio-subpage.tsx @@ -7,6 +7,8 @@ import { useWriteContract, } from "wagmi"; import { USD_STRATEGIES, BTC_STRATEGIES, ETH_STRATEGIES } from "../config/env"; +import { ERC20_ABI } from "../config/abi/erc20"; +import { SOLVER_ABI } from "../config/abi/solver"; import { type Address, createPublicClient, @@ -14,6 +16,18 @@ import { formatUnits, parseUnits, } from "viem"; +import { useRouter } from "next/router"; + +const isMobile = () => typeof window !== "undefined" && window.innerWidth < 640; +// import { +// BarChart, +// Bar, +// XAxis, +// YAxis, +// Tooltip, +// ResponsiveContainer, +// CartesianGrid, +// } from "recharts"; interface StrategyConfig { network: string; @@ -38,50 +52,75 @@ type StrategyAsset = { [K in DurationType]?: StrategyDuration; } -const ERC20_ABI = [ - { - name: "balanceOf", - type: "function", - stateMutability: "view", - inputs: [{ name: "owner", type: "address" }], - outputs: [{ name: "", type: "uint256" }], - }, - { - name: "decimals", - type: "function", - stateMutability: "view", - inputs: [], - outputs: [{ name: "", type: "uint8" }], - }, -] as const; +// Mock data for stacked chart +const chartData = [ + { date: "FEB 24", base: 2, incentive: 1 }, + { date: "FEB 24", base: 2.5, incentive: 0.8 }, + { date: "FEB 24", base: 3, incentive: 1.2 }, -const VAULT_ABI = [ - { - stateMutability: "nonpayable", - type: "function", - name: "redeem", - inputs: [ - { name: "shares", type: "uint256" }, - { name: "receiver", type: "address" }, - { name: "owner", type: "address" }, - ], - outputs: [{ name: "", type: "uint256" }], - }, + { date: "MAR 24", base: 4, incentive: 1.4 }, + { date: "MAR 24", base: 4.5, incentive: 1.6 }, + { date: "MAR 24", base: 5, incentive: 1.8 }, + + { date: "APR 24", base: 6.2, incentive: 2 }, + { date: "APR 24", base: 6.8, incentive: 2.5 }, + { date: "APR 24", base: 7.1, incentive: 2.7 }, + + { date: "MAY 24", base: 8.2, incentive: 3 }, + { date: "MAY 24", base: 9.4, incentive: 2.8 }, + + { date: "JUN 24", base: 10.2, incentive: 3.4 }, + { date: "JUN 24", base: 10.8, incentive: 3.7 }, + + { date: "JUL 24", base: 12, incentive: 4.2 }, + { date: "JUL 24", base: 13, incentive: 4.8 }, + + { date: "AUG 24", base: 14.5, incentive: 5.5 }, + { date: "AUG 24", base: 15.5, incentive: 6.2 }, + + { date: "SEP 24", base: 18, incentive: 6.8 }, + { date: "SEP 24", base: 19.5, incentive: 7 }, + + { date: "OCT 24", base: 21, incentive: 8 }, + { date: "OCT 24", base: 23, incentive: 8.4 }, + + { date: "NOV 24", base: 27, incentive: 8.6 }, + { date: "NOV 24", base: 28, incentive: 9 }, + + { date: "DEC 24", base: 30, incentive: 9.6 }, + { date: "DEC 24", base: 32, incentive: 10 }, + + { date: "JAN 24", base: 34, incentive: 10.4 }, + { date: "JAN 24", base: 36, incentive: 10.9 }, +]; + +// Mock data for the table rows +export const tableData = [ { - name: "balanceOf", - type: "function", - stateMutability: "view", - inputs: [{ name: "addr", type: "address" }], - outputs: [{ name: "", type: "uint256" }], + id: 1, + name: "Base Yield ETH", + expiry: "29th March 2025", + expiresIn: "20 days to Expire", + apy: "6.64%", + currentBalance: "$115,447.00", + change: "+$100.00 (10%)", + changeColor: "text-green-400", + period: "+0.00 in 1 year", + icon: "/icons/eth-icon.svg", // Use appropriate path or emoji }, { - name: "decimals", - type: "function", - stateMutability: "view", - inputs: [], - outputs: [{ name: "", type: "uint8" }], + id: 2, + name: "Incentive Maxi ETH", + expiry: "16th February 2025", + expiresIn: "7 days to Expire", + apy: "23.43%", + currentBalance: "$343,504,807.10", + change: "-$100.00 (10%)", + changeColor: "text-red-400", + period: "+0.00 in 1 year", + icon: "/icons/eth-icon.svg", }, -] as const; +]; const PortfolioSubpage: React.FC = () => { const { address, isConnected } = useAccount(); @@ -91,8 +130,8 @@ const PortfolioSubpage: React.FC = () => { ); const [isDepositing, setIsDepositing] = useState(false); const [isApproved, setIsApproved] = useState(false); - const [approvalHash, setApprovalHash] = useState<`0x${string}` | null>(null); const [isApproving, setIsApproving] = useState(false); + const [approvalHash, setApprovalHash] = useState<`0x${string}` | null>(null); const [strategiesWithBalance, setStrategiesWithBalance] = useState([]); const [selectedStrategy, setSelectedStrategy] = useState(null); const [withdrawAmount, setWithdrawAmount] = useState(""); @@ -104,6 +143,8 @@ const PortfolioSubpage: React.FC = () => { const [isRefreshingBalance, setIsRefreshingBalance] = useState(false); const [errorMessage, setErrorMessage] = useState(null); + const router = useRouter(); + // Watch deposit transaction const { isLoading: isWaitingForDeposit, isSuccess: isDepositSuccess } = useTransaction({ @@ -205,13 +246,13 @@ const PortfolioSubpage: React.FC = () => { const [balance, decimals] = await Promise.all([ client.readContract({ address: strategy.boringVaultAddress as Address, - abi: VAULT_ABI, + abi: ERC20_ABI, functionName: "balanceOf", args: [address as Address], }), client.readContract({ address: strategy.boringVaultAddress as Address, - abi: VAULT_ABI, + abi: ERC20_ABI, functionName: "decimals", }), ]); @@ -239,7 +280,7 @@ const PortfolioSubpage: React.FC = () => { try { setIsRefreshingBalance(true); const allStrategies = [ - ...Object.entries(USD_STRATEGIES as StrategyAsset).flatMap(([duration, strategies]) => + ...Object.entries(USD_STRATEGIES as unknown as StrategyAsset).flatMap(([duration, strategies]) => Object.entries(strategies as StrategyDuration).map(([type, strategy]) => ({ ...strategy, duration, @@ -247,7 +288,7 @@ const PortfolioSubpage: React.FC = () => { asset: "USD" })) ), - ...Object.entries(BTC_STRATEGIES as StrategyAsset).flatMap(([duration, strategies]) => + ...Object.entries(BTC_STRATEGIES as unknown as StrategyAsset).flatMap(([duration, strategies]) => Object.entries(strategies as StrategyDuration).map(([type, strategy]) => ({ ...strategy, duration, @@ -255,7 +296,7 @@ const PortfolioSubpage: React.FC = () => { asset: "BTC" })) ), - ...Object.entries(ETH_STRATEGIES as StrategyAsset).flatMap(([duration, strategies]) => + ...Object.entries(ETH_STRATEGIES as unknown as StrategyAsset).flatMap(([duration, strategies]) => Object.entries(strategies as StrategyDuration).map(([type, strategy]) => ({ ...strategy, duration, @@ -311,17 +352,22 @@ const PortfolioSubpage: React.FC = () => { // Use wagmi's useWriteContract hook const { writeContractAsync: writeContract } = useWriteContract(); - const handleWithdraw = async () => { + const handleApprove = async () => { if (!selectedStrategy || !withdrawAmount || !address) return; try { - setIsWithdrawing(true); + setIsApproving(true); setErrorMessage(null); - // Get the contract address from the selected strategy - const contractAddress = selectedStrategy.contract as Address; + const solverAddress = selectedStrategy.solverAddress as Address; + const vaultAddress = selectedStrategy.boringVaultAddress as Address; + + console.log("Approval details:", { + solverAddress, + vaultAddress, + address + }); - // Create a client to interact with the contract const client = createPublicClient({ transport: http(selectedStrategy.rpc), chain: { @@ -340,58 +386,165 @@ const PortfolioSubpage: React.FC = () => { }, }); - // Get the decimals from the contract + // Get decimals from vault const decimals = (await client.readContract({ - address: contractAddress, - abi: VAULT_ABI, + address: vaultAddress, + abi: ERC20_ABI, functionName: "decimals", })) as number; - console.log(`Contract decimals: ${decimals}`); - - // Parse the withdraw amount with proper decimals const sharesAmount = parseUnits(withdrawAmount, decimals); - console.log("Withdrawing from contract:", { - contract: contractAddress, - shares: sharesAmount.toString(), - receiver: address, - owner: address, - decimals, + console.log("Requesting approval for amount:", sharesAmount.toString()); + + // Approve the solver to spend the vault tokens + const approveTx = await writeContract({ + address: vaultAddress, + abi: ERC20_ABI, + functionName: "approve", + args: [solverAddress, sharesAmount], + chainId: 8453, + account: address, }); - try { - // Call the redeem function on the contract - const tx = await writeContract({ - address: contractAddress, - abi: VAULT_ABI, - functionName: "redeem", - args: [sharesAmount, address, address], - chainId: 8453, // Base chain ID - account: address, + if (approveTx && typeof approveTx === "string" && approveTx.startsWith("0x")) { + console.log("Approval transaction submitted:", approveTx); + setApprovalHash(approveTx as `0x${string}`); + + // Wait for approval transaction to complete + const { isSuccess: isApprovalSuccess } = await useTransaction({ + hash: approveTx as `0x${string}`, }); - if (tx && typeof tx === "string" && tx.startsWith("0x")) { - console.log("Withdrawal transaction submitted:", tx); - setWithdrawTxHash(tx as `0x${string}`); + if (isApprovalSuccess) { + setIsApproved(true); + console.log("Approval successful"); } else { - throw new Error("Failed to get transaction hash"); + throw new Error("Approval failed"); } - } catch (error) { - console.error("Contract call failed:", error); - setIsWithdrawing(false); - setErrorMessage("Transaction failed. Please try again."); + } else { + throw new Error("Failed to get approval transaction hash"); } } catch (error) { - console.error("Error withdrawing:", error); + console.error("Approval failed:", error); + setErrorMessage("Approval failed. Please try again."); + } finally { + setIsApproving(false); + } + }; + + const handleWithdraw = async () => { + if (!selectedStrategy || !withdrawAmount || !address || !isApproved) return; + + try { + setIsWithdrawing(true); + setErrorMessage(null); + + const solverAddress = selectedStrategy.solverAddress as Address; + const vaultAddress = selectedStrategy.boringVaultAddress as Address; + const assetOutAddress = "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc" as Address; + + const client = createPublicClient({ + transport: http(selectedStrategy.rpc), + chain: { + id: 8453, + name: "Base", + network: "base", + nativeCurrency: { + decimals: 18, + name: "Ether", + symbol: "ETH", + }, + rpcUrls: { + default: { http: ["https://mainnet.base.org"] }, + public: { http: ["https://mainnet.base.org"] }, + }, + }, + }); + + // Get decimals from vault contract + const decimals = (await client.readContract({ + address: vaultAddress, + abi: ERC20_ABI, + functionName: "decimals", + })) as number; + + const sharesAmount = parseUnits(withdrawAmount, decimals); + // Convert to uint128 + const amountOfShares = BigInt(sharesAmount.toString()); + const discount = 100; // uint16 - hardcoded + const secondsToDeadline = 3600; // uint24 - hardcoded (1 hour) + + console.log("Debug - Contract call parameters:", { + functionName: "requestOnChainWithdraw", + contractAddress: solverAddress, + args: { + assetOut: assetOutAddress, + amountOfShares: amountOfShares.toString(), + discount: discount.toString(), + secondsToDeadline: secondsToDeadline.toString() + }, + types: { + assetOut: typeof assetOutAddress, + amountOfShares: typeof amountOfShares, + discount: typeof discount, + secondsToDeadline: typeof secondsToDeadline + } + }); + + const tx = await writeContract({ + address: solverAddress, + abi: SOLVER_ABI, + functionName: "requestOnChainWithdraw", + args: [ + assetOutAddress, + amountOfShares, + discount, + secondsToDeadline + ], + chainId: 8453, + account: address, + }); + + if (tx && typeof tx === "string" && tx.startsWith("0x")) { + console.log("Withdrawal transaction submitted:", tx); + setWithdrawTxHash(tx as `0x${string}`); + } else { + throw new Error("Failed to get transaction hash"); + } + } catch (error) { + console.error("Contract call failed:", error); + setErrorMessage("Transaction failed. Please try again."); + } finally { setIsWithdrawing(false); - setErrorMessage("Error preparing withdrawal. Please try again."); } }; + // Handler for row clicks const handleStrategySelect = (strategy: any) => { - setSelectedStrategy(strategy); - setWithdrawAmount(strategy.balance.toString()); + console.log("strategy",strategy) + if (isMobile()) { + router.push({ + pathname: `/portfolio/${strategy.contract}`, + query: { + strategy: strategy.contract, + asset: strategy.asset, + balance: strategy.balance, + duration: strategy.duration, + type: strategy.type, + apy: strategy.apy, + SolverAddress: strategy.solverAddress, + boringVaultAddress: strategy.boringVaultAddress, + // tvl: strategy.tvl, + // baseApy: strategy.baseYield, + // contractAddress: strategy.contractAddress || "", + // network: strategy.network || "" + }, + }); + } else { + setSelectedStrategy(strategy); + setWithdrawAmount(strategy.balance.toString()); + } }; const handleAmountChange = (e: React.ChangeEvent) => { @@ -414,10 +567,23 @@ const PortfolioSubpage: React.FC = () => { } }; + // const CustomXAxisTick = ({ x, y, payload, index, data }) => { + // const currentLabel = payload.value; + // const prevLabel = index > 0 ? data[index - 1]?.date : null; + + // const showLabel = currentLabel !== prevLabel; + + // return showLabel ? ( + // + // {currentLabel} + // + // ) : null; + // }; + return (
{/* Top Section - Portfolio Value, PNL, and Wallet */} -
+
@@ -487,11 +653,11 @@ const PortfolioSubpage: React.FC = () => {
-
+
Wallet Address
-
+
{isConnected ? address : "Not connected"}
@@ -500,19 +666,54 @@ const PortfolioSubpage: React.FC = () => { {/* Main Content - Split View */}
{/* Left Side - Assets Table */} -
+
Total Portfolio Value
- {/* Column Headers */} -
-
+ {/* Graph */} + {/*
+ + + + } + /> + `$${value}`} + domain={[0, 100]} + fontSize={12} + axisLine={false} + /> + [`$${value.toFixed(2)}`, "Value"]} + /> + + + + +
*/} + +
+
Available Yields
-
+
Expiry {
-
+
Base APY {
-
+
Current Balance @@ -675,7 +876,7 @@ const PortfolioSubpage: React.FC = () => {
{/* Right Side - Withdraw Form or Info */} -
+
{selectedStrategy ? (

@@ -772,19 +973,44 @@ const PortfolioSubpage: React.FC = () => { {errorMessage && ( diff --git a/src/pages/portfolio/[contract].tsx b/src/pages/portfolio/[contract].tsx new file mode 100644 index 0000000..dae2f5d --- /dev/null +++ b/src/pages/portfolio/[contract].tsx @@ -0,0 +1,1116 @@ +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import { YieldDetailsView } from "@/components/yield-details-view"; +import Image from "next/image"; +import { ArrowLeft } from "lucide-react"; +import { + type Address, + createPublicClient, + http, + formatUnits, + parseUnits, + getAddress, +} from "viem"; +import { + useAccount, + useTransaction, + useWriteContract, + useChainId, +} from "wagmi"; +import { USD_STRATEGIES, BTC_STRATEGIES, ETH_STRATEGIES } from "../../config/env"; +import { ERC20_ABI } from "../../config/abi/erc20"; +import { SOLVER_ABI } from "../../config/abi/solver"; + +interface StrategyConfig { + network: string; + contract: string; + deposit_token: string; + deposit_token_contract: string; + description: string; + apy: string; + incentives: string; + tvl: string; + rpc: string; +} + +type DurationType = "30_DAYS" | "60_DAYS" | "180_DAYS" | "PERPETUAL_DURATION"; +type StrategyType = "STABLE" | "INCENTIVE"; + +type StrategyDuration = { + [K in StrategyType]: StrategyConfig; +} + +type StrategyAsset = { + [K in DurationType]?: StrategyDuration; +} + +const assetOptions = [ + { + name: "USDC", + contract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + image: "/images/icons/usdc.svg", + }, + { + name: "USDS", + contract: "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc", + image: "/images/icons/usds.svg", + }, + { + name: "sUSDS", + contract: "0x5875eEE11Cf8398102FdAd704C9E96607675467a", + image: "/images/icons/sUSDS.svg", + }, +]; + +const requests = [ + { + date: "18th May’25", + fromAmount: "1,000,000", + toAmount: "1,004,000", + canCancel: true, + }, + { + date: "19th May’25", + fromAmount: "100", + toAmount: "104", + canCancel: true, + }, + { + date: "19th May’25", + fromAmount: "900", + toAmount: "909", + canCancel: true, + }, + { + date: "19th May’25", + fromAmount: "1,092", + toAmount: "1,200", + canCancel: true, + }, +]; + +const ExternalLinkIcon = () => ( + + + +); + +const strategy = USD_STRATEGIES.PERPETUAL_DURATION.STABLE; +console.log("Strategy Config:", strategy); +const chainConfigs = { + base: { + rpc: "https://mainnet.base.org", + chainId: 8453, + image: "/images/logo/base.svg", + chainObject: { tokens: strategy.base.tokens } + }, + ethereum: { + rpc: "https://ethereum.llamarpc.com", + chainId: 1, + image: "/images/logo/eth.svg", + chainObject: { tokens: strategy.ethereum.tokens } + }, + arbitrum: { + rpc: "https://arb1.arbitrum.io/rpc", + chainId: 42161, + image: "/images/logo/arb.svg", + chainObject: { tokens: strategy.arbitrum.tokens } + }, +}; +console.log("chain configs:", chainConfigs); +const PortfolioDetailedPage = () => { + const { address, isConnected } = useAccount(); + const router = useRouter(); + const { contract ,asset ,type ,balance ,duration , solverAddress, boringVaultAddress , rpc } = router.query; + + const [depositSuccess, setDepositSuccess] = useState(false); + const [transactionHash, setTransactionHash] = useState<`0x${string}` | null>( + null + ); + const [isDepositing, setIsDepositing] = useState(false); + const [isApproved, setIsApproved] = useState(false); + const [isApproving, setIsApproving] = useState(false); + const [approvalHash, setApprovalHash] = useState<`0x${string}` | null>(null); + const [strategiesWithBalance, setStrategiesWithBalance] = useState([]); + const [withdrawAmount, setWithdrawAmount] = useState(""); + const [slippage, setSlippage] = useState("0.03"); + const [isWithdrawing, setIsWithdrawing] = useState(false); + const [withdrawTxHash, setWithdrawTxHash] = useState<`0x${string}` | null>( + null + ); + const [isRefreshingBalance, setIsRefreshingBalance] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [activeTab, setActiveTab] = useState<"withdraw" | "request">("withdraw"); + const [requestTab, setRequestTab] = useState<"pending" | "completed">("pending"); + const [amountOut, setAmountOut] = useState(null); + const [selectedAssetIdx, setSelectedAssetIdx] = useState(0); + // Add state for custom dropdown + const [isAssetDropdownOpen, setIsAssetDropdownOpen] = useState(false); + const [depositedChains, setDepositedChains] = useState([]); + + const chainId = useChainId(); + const isBase = chainId === 8453; + + // Watch deposit transaction + const { isLoading: isWaitingForDeposit, isSuccess: isDepositSuccess } = + useTransaction({ + hash: transactionHash || undefined, + }); + + // Watch for deposit completion + useEffect(() => { + if (!isWaitingForDeposit && isDepositing) { + setIsDepositing(false); + setIsApproved(false); + if (isDepositSuccess && transactionHash) { + setDepositSuccess(true); + } + } + }, [isWaitingForDeposit, isDepositing, isDepositSuccess, transactionHash]); + + // Watch approval transaction + const { isLoading: isWaitingForApproval, isSuccess: isApprovalSuccess } = + useTransaction({ + hash: approvalHash || undefined, + }); + + // Watch withdraw transaction + const { isLoading: isWaitingForWithdraw, isSuccess: isWithdrawSuccess } = + useTransaction({ + hash: withdrawTxHash || undefined, + }); + + const chainIconMap: Record = { + ethereum: { + src: "/images/logo/eth.svg", + label: "Ethereum", + }, + base: { + src: "/images/logo/base.svg", + label: "Base", + }, + arbitrum: { + src: "/images/logo/arb.svg", + label: "Arbitrum", + }, + }; + + useEffect(() => { + if (!isWaitingForWithdraw && isWithdrawing) { + setIsWithdrawing(false); + if (isWithdrawSuccess && withdrawTxHash) { + // Handle successful withdrawal + // Refresh balances with loading state + setIsRefreshingBalance(true); + checkAllBalances() + .then(() => { + setIsRefreshingBalance(false); + }) + .catch((error) => { + // console.error("Error refreshing balances:", error); + setErrorMessage("Failed to refresh balances."); + setIsRefreshingBalance(false); + }); + setWithdrawAmount(""); + } + } + }, [isWaitingForWithdraw, isWithdrawing, isWithdrawSuccess, withdrawTxHash]); + + useEffect(() => { + if (approvalHash && isApprovalSuccess) { + setIsApproved(true); + setIsApproving(false); + console.log("Approval successful:"); + } else if(approvalHash && !isWaitingForApproval && !isApprovalSuccess) { + setErrorMessage("Approval transaction failed"); + setIsApproving(false); + } + }, [isWaitingForApproval, isApproving, isApprovalSuccess]); + + // Function to check balance for a strategy + const checkStrategyBalance = async (strategy: any) => { + if (!address) return 0; + + try { + // Validate boring vault address + if ( + !strategy.boringVaultAddress || + strategy.boringVaultAddress === "0x0000000000000000000000000000000000000000" + ) { + console.warn("Invalid boring vault address for strategy:", strategy); + return 0; + } + + const client = createPublicClient({ + transport: http(strategy.rpc), + chain: { + id: 8453, + name: "Base", + network: "base", + nativeCurrency: { + decimals: 18, + name: "Ether", + symbol: "ETH", + }, + rpcUrls: { + default: { http: [strategy.rpc] }, + public: { http: [strategy.rpc] }, + }, + }, + }); + + try { + const [balance, decimals] = await Promise.all([ + client.readContract({ + address: strategy.boringVaultAddress as Address, + abi: ERC20_ABI, + functionName: "balanceOf", + args: [address as Address], + }), + client.readContract({ + address: strategy.boringVaultAddress as Address, + abi: ERC20_ABI, + functionName: "decimals", + }), + ]); + console.log(`Checking chain balance: ${balance}`); + + const formattedBalance = parseFloat(formatUnits(balance as bigint, decimals as number)); + + return formattedBalance; + } catch (error) { + // console.error("Error reading boring vault:", error); + setErrorMessage("Failed to read vault balance."); + return 0; + } + } catch (error) { + // console.error("Error checking balance for strategy:", strategy, error); + setErrorMessage("Failed to check strategy balance."); + return 0; + } + }; + + const checkAllBalances = async () => { + if (!address) return; + + try { + setIsRefreshingBalance(true); + const allStrategies = [ + ...Object.entries(USD_STRATEGIES as unknown as StrategyAsset).flatMap(([duration, strategies]) => + Object.entries(strategies as StrategyDuration).map(([type, strategy]) => ({ + ...strategy, + duration, + type: type.toLowerCase(), + asset: "USD" + })) + ), + ...Object.entries(BTC_STRATEGIES as unknown as StrategyAsset).flatMap(([duration, strategies]) => + Object.entries(strategies as StrategyDuration).map(([type, strategy]) => ({ + ...strategy, + duration, + type: type.toLowerCase(), + asset: "BTC" + })) + ), + ...Object.entries(ETH_STRATEGIES as unknown as StrategyAsset).flatMap(([duration, strategies]) => + Object.entries(strategies as StrategyDuration).map(([type, strategy]) => ({ + ...strategy, + duration, + type: type.toLowerCase(), + asset: "ETH" + })) + ), + ]; + + const balances = await Promise.all( + allStrategies.map(async (strategy) => { + const balance = await checkStrategyBalance(strategy); + return { ...strategy, balance }; + }) + ); + setStrategiesWithBalance(balances.filter((s) => s.balance > 0)); + } catch (error) { + // console.error("Error checking all balances:", error); + setErrorMessage("Failed to fetch all balances."); + } finally { + setIsRefreshingBalance(false); + } + }; + + // Check balances for all strategies + useEffect(() => { + checkAllBalances(); + }, [address]); + + // Use wagmi's useWriteContract hook + const { writeContractAsync: writeContract } = useWriteContract(); + + const handleApprove = async () => { + if (!contract || !withdrawAmount || !address) return; + + try { + setIsApproving(true); + setErrorMessage(null); + setApprovalHash(null); + + console.log("Approval details:", { + solverAddress, + boringVaultAddress, + address + }); + + const client = createPublicClient({ + transport: http(Array.isArray(rpc) ? rpc[0] : rpc || "https://base.llamarpc.com"), + chain: { + id: 8453, + name: "Base", + network: "base", + nativeCurrency: { + decimals: 18, + name: "Ether", + symbol: "ETH", + }, + rpcUrls: { + default: { http: ["https://mainnet.base.org"] }, + public: { http: ["https://mainnet.base.org"] }, + }, + }, + }); + + // Get decimals from vault + const decimals = (await client.readContract({ + address: boringVaultAddress as Address, + abi: ERC20_ABI, + functionName: "decimals", + })) as number; + + const sharesAmount = parseUnits(withdrawAmount, decimals); + + console.log("Requesting approval for amount:", sharesAmount.toString()); + + // Approve the solver to spend the vault tokens + const approveTx = await writeContract({ + address: boringVaultAddress as Address, + abi: ERC20_ABI, + functionName: "approve", + args: [solverAddress as `0x${string}`, sharesAmount], + chainId: 8453, + account: address, + }); + + if (approveTx && typeof approveTx === "string" && approveTx.startsWith("0x")) { + setApprovalHash(approveTx as `0x${string}`); + console.log("Approval transaction submitted:", approveTx); + } else { + throw new Error("Failed to get approval transaction hash"); + } + } catch (error: any) { + console.error("Approval failed:", error); + if (error.code === 4001) { + setErrorMessage("Approval cancelled by user."); + } else { + setErrorMessage(error.message || "Approval transaction failed"); + } + setIsApproving(false); + } + }; + + const handleWithdraw = async () => { + if (!contract || !withdrawAmount || !address || !isApproved) return; + + try { + setIsWithdrawing(true); + setErrorMessage(null); + const assetOutAddress = "0x820C137fa70C8691f0e44Dc420a5e53c168921Dc" as Address; + + const client = createPublicClient({ + transport: http(Array.isArray(rpc) ? rpc[0] : rpc || "https://base.llamarpc.com"), + chain: { + id: 8453, + name: "Base", + network: "base", + nativeCurrency: { + decimals: 18, + name: "Ether", + symbol: "ETH", + }, + rpcUrls: { + default: { http: ["https://mainnet.base.org"] }, + public: { http: ["https://mainnet.base.org"] }, + }, + }, + }); + + // Get decimals from vault contract + const decimals = (await client.readContract({ + address: boringVaultAddress as Address, + abi: ERC20_ABI, + functionName: "decimals", + })) as number; + + const sharesAmount = parseUnits(withdrawAmount, decimals); + // Convert to uint128 + const amountOfShares = BigInt(sharesAmount.toString()); + const discount = 100; // uint16 - hardcoded + const secondsToDeadline = 3600; // uint24 - hardcoded (1 hour) + + console.log("Debug - Contract call parameters:", { + functionName: "requestOnChainWithdraw", + contractAddress: solverAddress, + args: { + assetOut: assetOutAddress, + amountOfShares: amountOfShares.toString(), + discount: discount.toString(), + secondsToDeadline: secondsToDeadline.toString() + }, + types: { + assetOut: typeof assetOutAddress, + amountOfShares: typeof amountOfShares, + discount: typeof discount, + secondsToDeadline: typeof secondsToDeadline + } + }); + + const tx = await writeContract({ + address: solverAddress as Address, + abi: SOLVER_ABI, + functionName: "requestOnChainWithdraw", + args: [ + assetOutAddress, + amountOfShares, + discount, + secondsToDeadline + ], + chainId: 8453, + account: address, + }); + + if (tx && typeof tx === "string" && tx.startsWith("0x")) { + console.log("Withdrawal transaction submitted:", tx); + setWithdrawTxHash(tx as `0x${string}`); + } else { + throw new Error("Failed to get transaction hash"); + } + } catch (error: any) { + if (error.code === 4001) { + setErrorMessage("Withdrawal cancelled by user."); + } else { + setErrorMessage("Withdrawal failed. Please try again."); + } + } finally { + setIsWithdrawing(false); + } + }; + + const handleAmountChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (/^\d*\.?\d*$/.test(value)) { + setWithdrawAmount(value); + } + }; + + const handlePercentageClick = (percentage: number) => { + if (contract) { + const amount = balance !== undefined ? (Number(balance) * percentage).toFixed(6) : "0.000000"; + setWithdrawAmount(amount); + } + }; + + const handleMaxClick = () => { + if (contract) { + if (balance !== undefined) { + setWithdrawAmount(balance.toString()); + } + } + }; + + const getDepositedChainsViem = async ({ + userAddress, + strategy, + chainConfigs, + }: { + userAddress: Address; + strategy: any; + chainConfigs: Record< + string, + { + rpc: string; + chainId: number; + image: string; + chainObject: any; + } + >; + }): Promise => { + const depositedChains: string[] = []; + + for (const [chainKey, chain] of Object.entries(chainConfigs)) { + try { + const client = createPublicClient({ + transport: http(chain.rpc), + chain: chain.chainObject, + }); + + const decimals = strategy.shareAddress_token_decimal ?? 18; + + const balance = await client.readContract({ + address: strategy.shareAddress as Address, + abi: ERC20_ABI, + functionName: "balanceOf", + args: [userAddress], + }); + + const formatted = Number(formatUnits(balance as bigint, decimals)); + + console.log(`[${chainKey}] Balance: ${formatted}`); + + if (formatted > 0) { + depositedChains.push(chainKey); + } + } catch (err) { + console.error(`Error on ${chainKey}:`, err); + } + } + + return depositedChains; + }; + + useEffect(() => { + const fetchDeposits = async () => { + if (!address || !strategy || !chainConfigs) { + console.log("Missing required data:", { address, strategy, chainConfigs }); + return; + } + console.log("Fetching deposits with:", { address, strategy, chainConfigs }); + + const depositedOn = await getDepositedChainsViem({ + userAddress: address as Address, + strategy, + chainConfigs, + }); + + setDepositedChains(depositedOn); + console.log("Deposited on:", depositedOn); + }; + fetchDeposits(); + }, [address,strategy,chainConfigs]); + + useEffect(() => { + const fetchAmountOut = async () => { + if (!contract || !withdrawAmount) return; + + try { + const selectedAssetAddress = getAddress(assetOptions[selectedAssetIdx].contract); + + const client = createPublicClient({ + transport: http(Array.isArray(rpc) ? rpc[0] : rpc || "https://base.llamarpc.com"), + chain: { + id: 8453, + name: "Base", + network: "base", + nativeCurrency: { + decimals: 18, + name: "Ether", + symbol: "ETH", + }, + rpcUrls: { + default: { http: ["https://mainnet.base.org"] }, + public: { http: ["https://mainnet.base.org"] }, + }, + }, + }); + + //Get decimals of the vault + const decimals = (await client.readContract({ + address: boringVaultAddress as Address, + abi: ERC20_ABI, + functionName: "decimals", + })) as number; + + //Convert withdrawAmount to uint128 (BigInt) + const shares = parseUnits(withdrawAmount, decimals); + const discount = 0; + + //Call the previewAssetsOut + const result = (await client.readContract({ + address: solverAddress as Address, + abi: SOLVER_ABI, + functionName: "previewAssetsOut", + args: [ + selectedAssetAddress, + shares, + discount, + ], + })); + + setAmountOut(result.toString()); + } catch (err) { + console.error("Error reading previewAssetsOut:", err); + setAmountOut(null); + } + }; + + fetchAmountOut(); + }, [contract, withdrawAmount]); + + + return ( + <> + +
+
+
+ + +
+ {activeTab === "withdraw" && ( + <> +
+ {/* Header with strategy info and balance */} +
+
+ {typeof +
+
+ {type === "stable" + ? "Base Yield" + : "Incentive Maxi"}{" "} + {asset} +
+
+ +0.00 in 1 year +
+
+
+
+ Balance:{" "} + + {typeof balance === "string" ? parseFloat(balance).toFixed(4) : ""} + +
+
+ + {/* Input field and percentage buttons in same row */} +
+ {/* Input field on the left with no borders */} +
+
+ +
+
+ + {/* Percentage buttons on the right */} +
+ + + + +
+
+ +
+
+ You Will Receive +
+
+
+ {formatUnits(amountOut ? BigInt(amountOut) : BigInt(0), 6)}{" "} +
+ {assetOptions.length > 1 && ( +
+
+ + + {isAssetDropdownOpen && ( +
+ {assetOptions.map((opt, idx) => ( + + ))} +
+ )} +
+
+ )} +
+
+ + + {errorMessage && ( +
+
+ {errorMessage} +
+
+ # + {withdrawTxHash + ? withdrawTxHash.substring(0, 8) + "..." + : ""} +
+
+ )} + {!errorMessage && withdrawTxHash && isWithdrawSuccess && ( +
+
+ Transaction Successful +
+ + #{withdrawTxHash.substring(0, 8)}... + +
+ )} +
+
+
+ Note: By withdrawing, your vault shares will + be converted into the underlying asset, subject to the current + market rates. Withdrawal amounts are calculated based on the + latest market rates and may vary slightly due to price + fluctuations. +
+
+ + )} + + {activeTab === "request" && ( +
+ {/* Tabs */} +
+ + +
+ + {/* Requests List */} + {requestTab === "pending" && ( +
+ {requests.map((req, idx) => ( +
+
+
+ {/* Calendar Icon + Date */} +
+ + {req.date} +
+ {/* Cancel Button */} + {req.canCancel && ( + + )} +
+ + {/* Amounts */} +
+ {/* From Amount */} +
+ {typeof + {req.fromAmount} +
+ + {/* Arrow */} + + + {/* To Amount */} +
+ {req.toAmount} + {typeof +
+
+
+
+ ))} +
+ )} + + {requestTab === "completed" && ( +
+ {requests.map((req, idx) => ( +
+
+
+ {/* Calendar Icon + Date */} +
+ + {req.date} +
+
+ + {/* Amounts */} +
+ {/* From Amount */} +
+ {typeof + {req.fromAmount} +
+ + {/* Arrow */} + + + {/* To Amount */} +
+ {req.toAmount} + {typeof +
+
+
+
+ ))} +
+ )} + +
+ )} + +
+
+ + ); +}; + +export default PortfolioDetailedPage; \ No newline at end of file diff --git a/src/pages/yield/[id].tsx b/src/pages/yield/[id].tsx new file mode 100644 index 0000000..de5733e --- /dev/null +++ b/src/pages/yield/[id].tsx @@ -0,0 +1,28 @@ +import { useRouter } from "next/router"; +import { YieldDetailsView } from "@/components/yield-details-view"; +import React from "react"; +import { ArrowLeft } from "lucide-react"; + +const YieldDetailPage = () => { + const router = useRouter(); + const { name, tvl, baseApy, contractAddress, network } = router.query; + + if (!name) return
Loading...
; + + return ( +
+ + +
+ ); +}; + +export default YieldDetailPage; \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index 2334816..136f4ac 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2,6 +2,9 @@ @tailwind components; @tailwind utilities; +/* Import Inter font */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + /* Your custom global styles */ html, body { @@ -9,8 +12,10 @@ body { padding: 0; margin: 0; background-color: #080b17; - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, - Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } a { @@ -24,7 +29,7 @@ a { h1 { color: #d7e3ef; - font-family: Inter; + font-family: 'Inter', sans-serif; font-size: 40px; font-style: normal; font-weight: 600; @@ -34,7 +39,7 @@ h1 { h2 { color: #d7e3ef; - font-family: Inter; + font-family: 'Inter', sans-serif; font-size: 32px; font-style: normal; font-weight: 500; @@ -43,6 +48,7 @@ h2 { h3 { @apply text-lg font-medium; + font-family: 'Inter', sans-serif; } @layer components { diff --git a/src/types/tailwind-merge.d.ts b/src/types/tailwind-merge.d.ts new file mode 100644 index 0000000..4ff47d6 --- /dev/null +++ b/src/types/tailwind-merge.d.ts @@ -0,0 +1,3 @@ +declare module 'tailwind-merge' { + export function twMerge(...classLists: string[]): string; + } \ No newline at end of file diff --git a/src/utils/balanceCalculator.ts b/src/utils/balanceCalculator.ts new file mode 100644 index 0000000..65eefa7 --- /dev/null +++ b/src/utils/balanceCalculator.ts @@ -0,0 +1,182 @@ +import { ethers } from 'ethers'; +import { USD_STRATEGIES } from '../config/env'; + +interface TokenBalance { + chain: string; + token: string; + balance: number; + usdValue: number; +} + +interface ChainConfig { + tokens: { + name: string; + contract: string; + decimal: number; + image: string; + }[]; + rpc: string; +} + +interface StrategyConfig { + network: string; + contract: string; + boringVaultAddress: string; + solverAddress: string; + shareAddress: string; + shareAddress_token_decimal: number; + base: ChainConfig; + ethereum: ChainConfig; + arbitrum: ChainConfig; + description: string; + apy: string; + incentives: string; + cap_limit: string; + filled_cap: string; + show_cap: boolean; + tvl: string; + rpc: string; +} + +// Cache for providers to avoid creating new ones for each request +const providerCache: { [key: string]: ethers.JsonRpcProvider } = {}; + +function getProvider(rpcUrl: string): ethers.JsonRpcProvider { + if (!providerCache[rpcUrl]) { + providerCache[rpcUrl] = new ethers.JsonRpcProvider(rpcUrl); + } + return providerCache[rpcUrl]; +} + +export async function calculateTotalBalanceInUSD( + address: string, + provider: ethers.Provider +): Promise { + const balances: TokenBalance[] = []; + const strategy = USD_STRATEGIES.PERPETUAL_DURATION.STABLE as unknown as StrategyConfig; + + // Check each chain configuration + const chains = ['base', 'ethereum', 'arbitrum'] as const; + + for (const chain of chains) { + const chainConfig = strategy[chain]; + if (!chainConfig?.tokens) continue; + + const chainProvider = getProvider(chainConfig.rpc); + + for (const token of chainConfig.tokens) { + try { + const tokenContract = new ethers.Contract( + token.contract, + ['function balanceOf(address) view returns (uint256)'], + chainProvider + ); + + const balance = await tokenContract.balanceOf(address); + const formattedBalance = Number(ethers.formatUnits(balance, token.decimal)); + + // For stablecoins, we assume 1:1 USD value + const usdValue = formattedBalance; + + balances.push({ + chain, + token: token.name, + balance: formattedBalance, + usdValue + }); + } catch (error) { + console.error(`Error fetching balance for ${token.name} on ${chain}:`, error); + } + } + } + + // Calculate total USD value + const totalUSDValue = balances.reduce((sum, token) => sum + token.usdValue, 0); + + return totalUSDValue; +} + +// Function to update filled_cap in env.ts +export async function updateFilledCap( + address: string, + provider: ethers.Provider +): Promise { + const totalBalance = await calculateTotalBalanceInUSD(address, provider); + const formattedBalance = totalBalance.toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }); + + // Update the filled_cap in the configuration + USD_STRATEGIES.PERPETUAL_DURATION.STABLE.filled_cap = formattedBalance; +} + +// Function to get real-time filled cap value +export async function getRealTimeFilledCap( + address: string, + provider: ethers.Provider +): Promise { + const totalBalance = await calculateTotalBalanceInUSD(address, provider); + return totalBalance.toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }); +} + +// Function to get real-time filled cap value with caching +let lastFetchedTime = 0; +let cachedFilledCap: string | null = null; +const CACHE_DURATION = 30000; // 30 seconds cache + +export async function getCachedFilledCap( + address: string, + provider: ethers.Provider +): Promise { + const now = Date.now(); + + // Return cached value if it's still valid + if (cachedFilledCap && (now - lastFetchedTime) < CACHE_DURATION) { + return cachedFilledCap; + } + + // Fetch new value + const newValue = await getRealTimeFilledCap(address, provider); + cachedFilledCap = newValue; + lastFetchedTime = now; + + return newValue; +} + +let updateInterval: NodeJS.Timeout | null = null; + +// Function to start periodic updates of filled_cap +export function startFilledCapUpdates( + address: string, + provider: ethers.Provider, + intervalMs: number = 30000 // Default to 30 seconds +): void { + // Clear any existing interval + if (updateInterval) { + clearInterval(updateInterval); + } + + // Initial update + updateFilledCap(address, provider).catch(console.error); + + // Set up periodic updates + updateInterval = setInterval(async () => { + try { + await updateFilledCap(address, provider); + } catch (error) { + console.error('Error updating filled_cap:', error); + } + }, intervalMs); +} + +// Function to stop periodic updates +export function stopFilledCapUpdates(): void { + if (updateInterval) { + clearInterval(updateInterval); + updateInterval = null; + } +} \ No newline at end of file