diff --git a/web/package.json b/web/package.json index 99d8405..b645f56 100644 --- a/web/package.json +++ b/web/package.json @@ -11,11 +11,13 @@ }, "dependencies": { "@ensdomains/thorin": "1.0.0-beta.9", + "@tanstack/react-form": "^0.35.0", "@tanstack/react-query": "^5.60.2", "@tanstack/react-router": "^1.81.9", "connectkit": "^1.8.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "ts-pattern": "^5.5.0", "viem": "2.x", "wagmi": "^2.12.32" }, diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 8fd6ffe..bf4ab7a 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@ensdomains/thorin': specifier: 1.0.0-beta.9 version: 1.0.0-beta.9(@vanilla-extract/css@1.16.0)(@vanilla-extract/dynamic@2.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-form': + specifier: ^0.35.0 + version: 0.35.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@tanstack/react-query': specifier: ^5.60.2 version: 5.60.2(react@18.3.1) @@ -19,13 +22,16 @@ importers: version: 1.81.9(@tanstack/router-generator@1.81.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) connectkit: specifier: ^1.8.2 - version: 1.8.2(@babel/core@7.26.0)(@tanstack/react-query@5.60.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.32(@tanstack/query-core@5.59.20)(@tanstack/react-query@5.60.2(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) + version: 1.8.2(@babel/core@7.26.0)(@tanstack/react-query@5.60.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.32(@tanstack/query-core@5.59.20)(@tanstack/react-query@5.60.2(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) react: specifier: ^18.3.1 version: 18.3.1 react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + ts-pattern: + specifier: ^5.5.0 + version: 5.5.0 viem: specifier: 2.x version: 2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8) @@ -1540,6 +1546,44 @@ packages: '@types/react': optional: true + '@remix-run/node@2.14.0': + resolution: {integrity: sha512-ou16LMJYv0ElIToZ6dDqaLjv1T3iBEwuJTBahveEA8NkkACIWODJ2fgUYf1UKLMKHVdHjNImLzS37HdSZY0Q6g==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@remix-run/router@1.21.0': + resolution: {integrity: sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==} + engines: {node: '>=14.0.0'} + + '@remix-run/server-runtime@2.14.0': + resolution: {integrity: sha512-9Th9UzDaoFFBD7zA5mRI1KT8JktFLN4ij9jPygrKBhG/kYmNIvhcMtq9VyjcbMvFK5natTyhOhrrKRIHtijD4w==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.1.0 + peerDependenciesMeta: + typescript: + optional: true + + '@remix-run/web-blob@3.1.0': + resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==} + + '@remix-run/web-fetch@4.4.2': + resolution: {integrity: sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==} + engines: {node: ^10.17 || >=12.3} + + '@remix-run/web-file@3.1.0': + resolution: {integrity: sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==} + + '@remix-run/web-form-data@3.1.0': + resolution: {integrity: sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==} + + '@remix-run/web-stream@1.1.0': + resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==} + '@rollup/rollup-android-arm-eabi@4.27.0': resolution: {integrity: sha512-e312hTjuM89YLqlcqEs7mSvwhxN5pgXqRobUob7Jsz1wDQlpAb2WTX4jzvrx5NrL1h2SE4fGdHSNyPxbLfzyeA==} cpu: [arm] @@ -1721,6 +1765,9 @@ packages: '@stablelib/x25519@1.0.3': resolution: {integrity: sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw==} + '@tanstack/form-core@0.35.0': + resolution: {integrity: sha512-WNgZ/Fo8OdsqvbHe/M4a9xjL4TU2Q//I2660YA88jeDyavHQXmOuos3vEuqvIW1+CYfONOAcz9AOI2bPBVId9w==} + '@tanstack/history@1.81.9': resolution: {integrity: sha512-9MPknhhnvZKifK4jSvva6NDqYQwsNaptrRzO4ejk6yCLyi4koVG4u3C4VCeClYZY5etLEQbO8wXU9knEFZpMeg==} engines: {node: '>=12'} @@ -1728,6 +1775,15 @@ packages: '@tanstack/query-core@5.59.20': resolution: {integrity: sha512-e8vw0lf7KwfGe1if4uPFhvZRWULqHjFcz3K8AebtieXvnMOz5FSzlZe3mTLlPuUBcydCnBRqYs2YJ5ys68wwLg==} + '@tanstack/react-form@0.35.0': + resolution: {integrity: sha512-9TkARKyzctQjg5yivxJiSb1Q3NoqGRKC7XZG0+UevWOjbIn+PR18eX8Nglx1CSxlj+BnzO+bWgVDO2evCugdtQ==} + peerDependencies: + '@tanstack/start': ^1.43.13 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@tanstack/start': + optional: true + '@tanstack/react-query@5.60.2': resolution: {integrity: sha512-JhpJNxIAPuE0YCpP1Py4zAsgx+zY0V531McRMtQbwVlJF8+mlZwcOPrzGmPV248K8IP+mPbsfxXToVNMNwjUcw==} peerDependencies: @@ -1796,6 +1852,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2022,6 +2081,12 @@ packages: '@walletconnect/window-metadata@1.0.1': resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} + '@web3-storage/multipart-parser@1.0.0': + resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} + + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + abitype@1.0.6: resolution: {integrity: sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==} peerDependencies: @@ -2371,6 +2436,14 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + core-js-compat@3.39.0: resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} @@ -2418,6 +2491,10 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-uri-to-buffer@3.0.1: + resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} + engines: {node: '>= 6'} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -2446,6 +2523,9 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decode-formdata@0.8.0: + resolution: {integrity: sha512-iUzDgnWsw5ToSkFY7VPFA5Gfph6ROoOxOB7Ybna4miUSzLZ4KaSJk6IAB2AdW6+C9vCVWhjjNA4gjT6wF3eZHQ==} + decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -3453,6 +3533,10 @@ packages: motion@10.16.2: resolution: {integrity: sha512-p+PurYqfUdcJZvtnmAqu5fJgV2kR0uLFQuBKtLeFVTrYEVllI99tiOTSefVNYuip9ELTEkepIIDftNdze76NAQ==} + mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -4056,6 +4140,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4122,6 +4209,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + split-on-first@1.1.0: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} @@ -4158,6 +4249,9 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + stream-slice@0.1.2: + resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -4305,6 +4399,9 @@ packages: ts-pattern@4.3.0: resolution: {integrity: sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==} + ts-pattern@5.5.0: + resolution: {integrity: sha512-jqbIpTsa/KKTJYWgPNsFNbLVpwCgzXfFJ1ukNn4I8hMwyQzHMJnk/BqWzggB0xpkILuKzaO/aMYhS0SkaJyKXg==} + tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -4316,6 +4413,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4354,6 +4454,10 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici@6.21.0: + resolution: {integrity: sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==} + engines: {node: '>=18.17'} + unenv@1.10.0: resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} @@ -4541,6 +4645,13 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webauthn-p256@0.0.10: resolution: {integrity: sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA==} @@ -6407,6 +6518,60 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@remix-run/node@2.14.0(typescript@5.6.3)': + dependencies: + '@remix-run/server-runtime': 2.14.0(typescript@5.6.3) + '@remix-run/web-fetch': 4.4.2 + '@web3-storage/multipart-parser': 1.0.0 + cookie-signature: 1.2.2 + source-map-support: 0.5.21 + stream-slice: 0.1.2 + undici: 6.21.0 + optionalDependencies: + typescript: 5.6.3 + + '@remix-run/router@1.21.0': {} + + '@remix-run/server-runtime@2.14.0(typescript@5.6.3)': + dependencies: + '@remix-run/router': 1.21.0 + '@types/cookie': 0.6.0 + '@web3-storage/multipart-parser': 1.0.0 + cookie: 0.6.0 + set-cookie-parser: 2.7.1 + source-map: 0.7.4 + turbo-stream: 2.4.0 + optionalDependencies: + typescript: 5.6.3 + + '@remix-run/web-blob@3.1.0': + dependencies: + '@remix-run/web-stream': 1.1.0 + web-encoding: 1.1.5 + + '@remix-run/web-fetch@4.4.2': + dependencies: + '@remix-run/web-blob': 3.1.0 + '@remix-run/web-file': 3.1.0 + '@remix-run/web-form-data': 3.1.0 + '@remix-run/web-stream': 1.1.0 + '@web3-storage/multipart-parser': 1.0.0 + abort-controller: 3.0.0 + data-uri-to-buffer: 3.0.1 + mrmime: 1.0.1 + + '@remix-run/web-file@3.1.0': + dependencies: + '@remix-run/web-blob': 3.1.0 + + '@remix-run/web-form-data@3.1.0': + dependencies: + web-encoding: 1.1.5 + + '@remix-run/web-stream@1.1.0': + dependencies: + web-streams-polyfill: 3.3.3 + '@rollup/rollup-android-arm-eabi@4.27.0': optional: true @@ -6599,10 +6764,25 @@ snapshots: '@stablelib/random': 1.0.2 '@stablelib/wipe': 1.0.1 + '@tanstack/form-core@0.35.0': + dependencies: + '@tanstack/store': 0.5.5 + '@tanstack/history@1.81.9': {} '@tanstack/query-core@5.59.20': {} + '@tanstack/react-form@0.35.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + dependencies: + '@remix-run/node': 2.14.0(typescript@5.6.3) + '@tanstack/form-core': 0.35.0 + '@tanstack/react-store': 0.5.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + decode-formdata: 0.8.0 + react: 18.3.1 + transitivePeerDependencies: + - react-dom + - typescript + '@tanstack/react-query@5.60.2(react@18.3.1)': dependencies: '@tanstack/query-core': 5.59.20 @@ -6694,6 +6874,8 @@ snapshots: dependencies: '@babel/types': 7.26.0 + '@types/cookie@0.6.0': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -7223,6 +7405,11 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 + '@web3-storage/multipart-parser@1.0.0': {} + + '@zxing/text-encoding@0.9.0': + optional: true + abitype@1.0.6(typescript@5.6.3)(zod@3.23.8): optionalDependencies: typescript: 5.6.3 @@ -7374,14 +7561,14 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-styled-components@2.1.4(@babel/core@7.26.0)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1))(supports-color@5.5.0): + babel-plugin-styled-components@2.1.4(@babel/core@7.26.0)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(supports-color@5.5.0): dependencies: '@babel/helper-annotate-as-pure': 7.25.9 '@babel/helper-module-imports': 7.25.9(supports-color@5.5.0) '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) lodash: 4.17.21 picomatch: 2.3.1 - styled-components: 5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1) + styled-components: 5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@babel/core' - supports-color @@ -7604,7 +7791,7 @@ snapshots: transitivePeerDependencies: - supports-color - connectkit@1.8.2(@babel/core@7.26.0)(@tanstack/react-query@5.60.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.32(@tanstack/query-core@5.59.20)(@tanstack/react-query@5.60.2(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)): + connectkit@1.8.2(@babel/core@7.26.0)(@tanstack/react-query@5.60.2(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.32(@tanstack/query-core@5.59.20)(@tanstack/react-query@5.60.2(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)): dependencies: '@tanstack/react-query': 5.60.2(react@18.3.1) buffer: 6.0.3 @@ -7616,7 +7803,7 @@ snapshots: react-transition-state: 1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-use-measure: 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) resize-observer-polyfill: 1.5.1 - styled-components: 5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1) + styled-components: 5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1) viem: 2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8) wagmi: 2.12.32(@tanstack/query-core@5.59.20)(@tanstack/react-query@5.60.2(react@18.3.1))(@types/react@18.3.12)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.2(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.21.45(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) transitivePeerDependencies: @@ -7629,6 +7816,10 @@ snapshots: cookie-es@1.2.2: {} + cookie-signature@1.2.2: {} + + cookie@0.6.0: {} + core-js-compat@3.39.0: dependencies: browserslist: 4.24.2 @@ -7680,6 +7871,8 @@ snapshots: csstype@3.1.3: {} + data-uri-to-buffer@3.0.1: {} + date-fns@2.30.0: dependencies: '@babel/runtime': 7.26.0 @@ -7698,6 +7891,8 @@ snapshots: decamelize@1.2.0: {} + decode-formdata@0.8.0: {} + decode-uri-component@0.2.2: {} dedent@1.5.3: {} @@ -8927,6 +9122,8 @@ snapshots: '@motionone/utils': 10.18.0 '@motionone/vue': 10.16.4 + mrmime@1.0.1: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -9557,6 +9754,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -9626,6 +9825,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.4: {} + split-on-first@1.1.0: {} split2@4.2.0: {} @@ -9650,6 +9851,8 @@ snapshots: stream-shift@1.0.3: {} + stream-slice@0.1.2: {} + strict-uri-encode@2.0.0: {} string-width@4.2.3: @@ -9691,19 +9894,19 @@ snapshots: hey-listen: 1.0.8 tslib: 2.8.1 - styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1): + styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1): dependencies: '@babel/helper-module-imports': 7.25.9(supports-color@5.5.0) '@babel/traverse': 7.25.9(supports-color@5.5.0) '@emotion/is-prop-valid': 1.3.1 '@emotion/stylis': 0.8.5 '@emotion/unitless': 0.7.5 - babel-plugin-styled-components: 2.1.4(@babel/core@7.26.0)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1))(supports-color@5.5.0) + babel-plugin-styled-components: 2.1.4(@babel/core@7.26.0)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(supports-color@5.5.0) css-to-react-native: 3.2.0 hoist-non-react-statics: 3.3.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-is: 16.13.1 + react-is: 18.3.1 shallowequal: 1.1.0 supports-color: 5.5.0 transitivePeerDependencies: @@ -9824,6 +10027,8 @@ snapshots: ts-pattern@4.3.0: {} + ts-pattern@5.5.0: {} + tslib@1.14.1: {} tslib@2.8.1: {} @@ -9835,6 +10040,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + turbo-stream@2.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -9866,6 +10073,8 @@ snapshots: undici-types@6.19.8: {} + undici@6.21.0: {} + unenv@1.10.0: dependencies: consola: 3.2.3 @@ -10030,6 +10239,14 @@ snapshots: dependencies: makeerror: 1.0.12 + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + + web-streams-polyfill@3.3.3: {} + webauthn-p256@0.0.10: dependencies: '@noble/curves': 1.6.0 diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx new file mode 100644 index 0000000..682e4d2 --- /dev/null +++ b/web/src/components/Header.tsx @@ -0,0 +1,15 @@ +import { Heading } from "@ensdomains/thorin"; +import { Link } from "@tanstack/react-router"; + +export const Header = () => { + return ( +
+ ENS Auto Renewal +
+ Home + View all actions + Create new action +
+
+ ); +}; diff --git a/web/src/hooks/enstate.tsx b/web/src/hooks/enstate.tsx new file mode 100644 index 0000000..56dd6c9 --- /dev/null +++ b/web/src/hooks/enstate.tsx @@ -0,0 +1,49 @@ +import { queryOptions } from "@tanstack/react-query"; + +// { +// "name": "helgesson.eth", +// "address": "0xd577D1322cB22eB6EAC1a008F62b18807921EFBc", +// "avatar": "https://ipfs.euc.li/ipfs/bafkreigiqg7bxushl3ogmdavtuk5jsh3g4xbyskn3blqu4kaw2wj4odgp4", +// "display": "helgesson.eth", +// "records": { +// "avatar": "ipfs://bafkreigiqg7bxushl3ogmdavtuk5jsh3g4xbyskn3blqu4kaw2wj4odgp4", +// "com.discord": "Svemat#5531", +// "com.github": "svemat01", +// "com.twitter": "Helgesson_", +// "email": "jakob@helgesson.dev", +// "org.telegram": "helgesson", +// "url": "https://jakobhelgesson.com" +// }, +// "chains": { +// "eth": "0xd577D1322cB22eB6EAC1a008F62b18807921EFBc" +// }, +// "fresh": 1731744359845, +// "resolver": "0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41", +// "errors": {} +// } +type EnstateResponse = { + name: string; + address: string; + avatar?: string; + errors?: Record; +}; + +const lookupEnsName = async (name: string): Promise => { + if (!name || !name.includes(".") || name.endsWith(".")) { + throw new Error("Invalid ENS name"); + } + + const res = await fetch(`https://sepolia.enstate.rs/n/${name}`); + + if (!res.ok) { + throw new Error("Failed to lookup ENS name"); + } + + return res.json() as Promise; +}; + +export const lookupEnsNameQueryOptions = (name: string) => + queryOptions({ + queryKey: ["enstate", "name", name], + queryFn: () => lookupEnsName(name), + }); diff --git a/web/src/index.css b/web/src/index.css index 9a397a6..aa3fc0b 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -17,12 +17,9 @@ a { font-weight: 500; - color: #646cff; + color: inherit; text-decoration: inherit; } -a:hover { - color: #535bf2; -} body { margin: 0; diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 9015bfa..18e0e47 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -16,16 +16,30 @@ import { Route as rootRoute } from './routes/__root' // Create Virtual Routes +const ListLazyImport = createFileRoute('/list')() const IndexLazyImport = createFileRoute('/')() +const ActionNewLazyImport = createFileRoute('/action/new')() // Create/Update Routes +const ListLazyRoute = ListLazyImport.update({ + id: '/list', + path: '/list', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/list.lazy').then((d) => d.Route)) + const IndexLazyRoute = IndexLazyImport.update({ id: '/', path: '/', getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) +const ActionNewLazyRoute = ActionNewLazyImport.update({ + id: '/action/new', + path: '/action/new', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/action/new.lazy').then((d) => d.Route)) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -37,6 +51,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexLazyImport parentRoute: typeof rootRoute } + '/list': { + id: '/list' + path: '/list' + fullPath: '/list' + preLoaderRoute: typeof ListLazyImport + parentRoute: typeof rootRoute + } + '/action/new': { + id: '/action/new' + path: '/action/new' + fullPath: '/action/new' + preLoaderRoute: typeof ActionNewLazyImport + parentRoute: typeof rootRoute + } } } @@ -44,32 +72,42 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexLazyRoute + '/list': typeof ListLazyRoute + '/action/new': typeof ActionNewLazyRoute } export interface FileRoutesByTo { '/': typeof IndexLazyRoute + '/list': typeof ListLazyRoute + '/action/new': typeof ActionNewLazyRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexLazyRoute + '/list': typeof ListLazyRoute + '/action/new': typeof ActionNewLazyRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: '/' | '/list' | '/action/new' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' + to: '/' | '/list' | '/action/new' + id: '__root__' | '/' | '/list' | '/action/new' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexLazyRoute: typeof IndexLazyRoute + ListLazyRoute: typeof ListLazyRoute + ActionNewLazyRoute: typeof ActionNewLazyRoute } const rootRouteChildren: RootRouteChildren = { IndexLazyRoute: IndexLazyRoute, + ListLazyRoute: ListLazyRoute, + ActionNewLazyRoute: ActionNewLazyRoute, } export const routeTree = rootRoute @@ -82,11 +120,19 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/" + "/", + "/list", + "/action/new" ] }, "/": { "filePath": "index.lazy.tsx" + }, + "/list": { + "filePath": "list.lazy.tsx" + }, + "/action/new": { + "filePath": "action/new.lazy.tsx" } } } diff --git a/web/src/routes/action/new.lazy.tsx b/web/src/routes/action/new.lazy.tsx new file mode 100644 index 0000000..aae2270 --- /dev/null +++ b/web/src/routes/action/new.lazy.tsx @@ -0,0 +1,272 @@ +import { createLazyFileRoute, useNavigate } from "@tanstack/react-router"; +import { + Button, + Card, + Helper, + Input, + PlusSVG, + TrashSVG, +} from "@ensdomains/thorin"; +import { useForm } from "@tanstack/react-form"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { lookupEnsNameQueryOptions } from "../../hooks/enstate"; +import { signMessage } from "wagmi/actions"; +import { useAccount, useConfig } from "wagmi"; +import { Header } from "../../components/Header"; +import { Action } from "../../types"; + +export const Route = createLazyFileRoute("/action/new")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { address } = useAccount(); + const navigate = useNavigate(); + const config = useConfig(); + const queryClient = useQueryClient(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { mutate } = useMutation({ + mutationKey: ["create-action"], + mutationFn: async (data: { names: string[]; reward: number }) => { + // TODO: Implement correct message here + const message = JSON.stringify(data); + const signature = await signMessage(config, { + message, + }); + + const response = await fetch("https://API/verify", { + method: "POST", + body: JSON.stringify({ + msg: data, + sig: signature, + signer: address, + }), + }); + + if (!response.ok) { + throw new Error("Failed to verify signature"); + } + + return response.json(); + }, + }); + + const form = useForm({ + defaultValues: { + names: [] as Array, + reward: 0, + }, + onSubmit({ value }) { + if (!address) return; + // TODO: run create action mutation and sign message + // Temporary adding to local storage + const actions = localStorage.getItem("actions") ?? "[]"; + localStorage.setItem( + "actions", + JSON.stringify([ + ...JSON.parse(actions), + { + ...value, + owner: address, + type: "RENEW_NAME", + } satisfies Action, + ]) + ); + + navigate({ to: "/" }); + }, + }); + + return ( +
+
+
+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > + { + if (value.length === 0) + return "At least one name is required"; + return undefined; + }, + }} + > + {(field) => { + return ( +
+ {field.state.value.map((_, i) => { + return ( + { + if (!value) + return "Name is required"; + // check if valid domain name + if ( + !value.includes(".") + ) + return "Invalid domain name"; + return undefined; + }, + onChangeAsync: async ({ + value, + }) => { + const result = + await queryClient + .fetchQuery( + lookupEnsNameQueryOptions( + value + ) + ) + .catch( + () => {} + ); + + if (!result) { + return "ENS name couldn't be resolved"; + } + }, + onChangeAsyncDebounceMs: 500, + }} + > + {(subField) => { + return ( +
+ + subField.handleChange( + e.target + .value + ) + } + width={ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + "100%" as any + } + actionIcon={ +
+ +
+ } + alwaysShowAction={ + true + } + onClickAction={() => + field.removeValue( + i + ) + } + error={ + subField + .state + .meta + .errors + .length > + 0 + ? subField.state.meta.errors.join( + ", " + ) + : null + } + /> +
+ ); + }} +
+ ); + })} + {field.state.value.length === 0 && ( + <> + + No names added + + + )} + +
+ ); + }} +
+
+ { + console.log(value); + if (!value) return "Reward is required"; + if (isNaN(Number(value))) + return "Must be a number"; + if (Number(value) <= 0) + return "Must be greater than 0"; + return undefined; + }, + }} + > + {(field) => ( +
+ + field.handleChange( + Number(e.target.value) + ) + } + error={ + field.state.meta.errors.length > 0 + ? field.state.meta.errors[0] + : undefined + } + /> + + The reward paid to executors for renewing + your names (in GWEI) + +
+ )} +
+ [ + state.canSubmit, + state.isSubmitting, + ]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> + +
+
+ ); +} diff --git a/web/src/routes/index.lazy.tsx b/web/src/routes/index.lazy.tsx index 6dab1e2..f45813b 100644 --- a/web/src/routes/index.lazy.tsx +++ b/web/src/routes/index.lazy.tsx @@ -1,22 +1,87 @@ -import { Card, Heading } from "@ensdomains/thorin"; +import { Button, Card, Helper, Tag, Typography } from "@ensdomains/thorin"; +import { useQuery } from "@tanstack/react-query"; import { createLazyFileRoute } from "@tanstack/react-router"; +import { match } from "ts-pattern"; +import { useAccount } from "wagmi"; +import { Header } from "../components/Header"; +import { Action } from "../types"; export const Route = createLazyFileRoute("/")({ component: Index, }); +/** + * list of all actions + * list of all actions by owner + * + * create new action | SIG_REQ + * delete action | SIG_REQ + */ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const getActions = async (owner: string): Promise => { + // Temporary storage of actions in local storage + const stored = localStorage.getItem("actions"); + if (!stored) { + return []; + } + + try { + const actions = JSON.parse(stored) as Action[]; + return actions.filter((action) => action.owner === owner); + } catch { + return []; + } +}; + +const ActionRow = ({ action }: { action: Action }) => { + return ( +
  • + + {match(action.type) + .with("RENEW_NAME", () => "Renew Name") + .otherwise(() => "Unknown")} + + +
    + + {action.names.join(", ")} + + + {action.reward} GWEI + +
    +
  • + ); +}; + function Index() { + const { address } = useAccount(); + const { data: actions } = useQuery({ + queryKey: ["actions", "all"], + queryFn: () => getActions(address ?? "0x0"), + }); + return (
    - ENS Auto Renewal +
    + + Add your ENS names to the list below to automatically renew them + when needed. +
    - - This is a simple app to help you renew your ENS names. - - - {Array.from({ length: 3 }).map((_, i) => ( -
    Pool {i}
    - ))} + +
      + {actions?.map((action) => ( + + ))} +
    + {actions?.length === 0 && ( + You have not created any actions yet + )} +
    ); diff --git a/web/src/routes/list.lazy.tsx b/web/src/routes/list.lazy.tsx new file mode 100644 index 0000000..a4cbf37 --- /dev/null +++ b/web/src/routes/list.lazy.tsx @@ -0,0 +1,83 @@ +import { Button, Card, Helper, Tag, Typography } from "@ensdomains/thorin"; +import { useQuery } from "@tanstack/react-query"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { match } from "ts-pattern"; +import { Header } from "../components/Header"; +import { Action } from "../types"; + +export const Route = createLazyFileRoute("/list")({ + component: Index, +}); + +/** + * list of all actions + * list of all actions by owner + * + * create new action | SIG_REQ + * delete action | SIG_REQ + */ + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const getAllActions = async (owner: string): Promise => { + // Temporary storage of actions in local storage + const stored = localStorage.getItem("actions"); + if (!stored) { + return []; + } + + try { + return JSON.parse(stored) as Action[]; + } catch { + return []; + } +}; + +const ActionRow = ({ action }: { action: Action }) => { + return ( +
  • + + {match(action.type) + .with("RENEW_NAME", () => "Renew Name") + .otherwise(() => "Unknown")} + + +
    + + {action.names.join(", ")} + + + {action.reward} GWEI + +
    +
  • + ); +}; + +function Index() { + const { data: actions } = useQuery({ + queryKey: ["actions", "all"], + queryFn: () => getAllActions("0x0"), + }); + + return ( +
    +
    + + Add your ENS names to the list below to automatically renew them + when needed. + +
    + +
      + {actions?.map((action) => ( + + ))} +
    + {actions?.length === 0 && No actions found} + +
    +
    + ); +} diff --git a/web/src/types.ts b/web/src/types.ts new file mode 100644 index 0000000..feb62af --- /dev/null +++ b/web/src/types.ts @@ -0,0 +1,10 @@ +export type Action = { + // List of ENS names + /** messages */ + names: string[]; + /** prefix */ + type: "RENEW_NAME"; + /** GWEI */ + reward: number; + owner: string; +};