diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 000000000..40a9a6315 --- /dev/null +++ b/.github/mergify.yml @@ -0,0 +1,9 @@ +pull_request_rules: + - name: backport patches to v2.x branch + conditions: + - base=master + - label=backport/v2.x + actions: + backport: + branches: + - release/v2.x diff --git a/MULTISIG_SERVER_README.md b/BACKEND_SERVER_README.md similarity index 61% rename from MULTISIG_SERVER_README.md rename to BACKEND_SERVER_README.md index dedcec141..94953c17d 100644 --- a/MULTISIG_SERVER_README.md +++ b/BACKEND_SERVER_README.md @@ -1,4 +1,4 @@ - The multisig server is written in the Go programming language using a PostgreSQL database. + The backend server is written in the Go programming language using a PostgreSQL database. # How to run @@ -8,6 +8,8 @@ 2. Install PostgreSQL 14.5 or above. +3. Install Redis. + ### After Postgres installation @@ -38,6 +40,18 @@ cd server sudo -u postgres psql your_dbname < schema/schema.sql ``` +### Add denom and coingecko ID (If you want to fetch price of token) +1. Add this in `update_denom_price.sql` file + ```bash + # Replace coin_minimal_denom with the actual minimal denom and replace coin_gecko_id with actual coin gecko id + ('coin_minimal_denom', 'coin_gecko_id', true, NOW(), '{}'::jsonb) + ``` +2. Run this command + ```bash + # Replace your_database_name with the actual database name + sudo -u postgres psql 'your_database_name' < schema/update_denom_price.sql + ``` + ## Quick Start make sure you have done pre-requisites step diff --git a/LICENSE b/LICENSE index b412e7981..dad3726e9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,24 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Copyright (c) 2024 Vitwit - 1. Definitions. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +All rights reserved. Permission must be obtained from the copyright holder for any use of the this software. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2022 Vitwit - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +All rights reserved @ vitwit. Contact: contact@vitwit.com diff --git a/README.md b/README.md index 110a0db20..feb128ef0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Resolute -Resolute is an advanced spacecraft designed to travel through the multiverse, connecting all Cosmos sovereign chains. +Resolute is your gateway to the Cosmos ecosystem. It enables users to seamlessly interact with any blockchain built using the Cosmos SDK stack, all while using the wallet of their choice. Whether you're a developer, validator, or an everyday user, Resolute offers a streamlined experience to connect and engage with the diverse Cosmos universe. + Supported features: - [x] Overview @@ -8,42 +9,43 @@ Supported features: - [x] Governance - [x] Multisig - [x] IBC Transfer -- [ ] Authz +- [x] IBC Swap +- [x] Authz +- [x] Feegrant +- [x] Cosmwasm contracts +- [x] Multi-Message Transaction Builder - [ ] Airdrops -- [ ] Feegrant - [ ] Groups -- [ ] Cross chain swaps +- [x] Cross chain swaps - [ ] Interchain Accounts -- [ ] Cosmwasm contracts - -## Adding new network - -To add a new network to Resolute, please follow these steps: - -1. Open the frontend/chains directory. -2. Create a new `.json` file. You can refer to the existing examples in the `frontend/chains` folder. ## Prerequisites 1. Install node 18.0.0 or above -## For older version -Use release/v1.x branch - ## Install deps ```bash -# clone the repo with git and checkout to master +# clone the repo with git and checkout to v2.0.0 $ git clone https://github.com/vitwit/resolute.git $ cd resolute -$ git checkout master +$ git checkout v2.0.0 $ cd frontend $ yarn ``` ## Environment variables -Create .env file and set multisig backend URI `NEXT_PUBLIC_APP_API_URI` -You can setup your own mulitisig server in [Set up multisig server](./MULTISIG_SERVER_README.md). +Rename `.env.example` to `.env` and set backend sever URI `NEXT_PUBLIC_APP_API_URI`. + +Backend server setup: [Set up backend server](./BACKEND_SERVER_README.md). + +Set Squid ID `NEXT_PUBLIC_SQUID_ID`, You can get Squid ID from here [Squid ID](https://squidrouter.typeform.com/integrator-id?typeform-source=docs.squidrouter.com) + +To use Cosmwasm contracts set `NEXT_PUBLIC_DUMMY_WALLET_MNEMONIC`. + +Set Squid ID `NEXT_PUBLIC_SQUID_ID`, You can get Squid ID from here [Squid ID](https://squidrouter.typeform.com/integrator-id?typeform-source=docs.squidrouter.com) + +To use Cosmwasm contracts set `NEXT_PUBLIC_DUMMY_WALLET_MNEMONIC`. ## Start in DEV Mode Runs the app in the development mode.
@@ -68,5 +70,15 @@ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +## How to add a new network to available networks + +To add a new network to Resolute, please follow these steps: + +1. Open the `frontend/src/utils/chainInfo.ts` file. +2. Add the new network configuration to the networks list. You can refer to the existing network configurations. +3. Open the `server/networks.json` file and add the new network configuration. +4. Add the token denom and coingecko-id in backend for token price. (Refer: [Set up backend server](./BACKEND_SERVER_README.md)) + ## License -Released under the [Apache 2.0 License](https://github.com/vitwit/resolute/blob/master/LICENSE). +Released under the [License](https://github.com/vitwit/resolute/blob/master/LICENSE). diff --git a/db-docker/Dockerfile b/db-docker/Dockerfile new file mode 100644 index 000000000..0c0e4e512 --- /dev/null +++ b/db-docker/Dockerfile @@ -0,0 +1,10 @@ +FROM postgres:latest + +COPY schema.sql /docker-entrypoint-initdb.d/ + +VOLUME /var/lib/postgresql/data + +EXPOSE 5432 + +# Start the PostgreSQL server +CMD ["postgres"] diff --git a/db-docker/schema.sql b/db-docker/schema.sql new file mode 100644 index 000000000..7d2fe768d --- /dev/null +++ b/db-docker/schema.sql @@ -0,0 +1,220 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 12.12 (Ubuntu 12.12-0ubuntu0.20.04.1) +-- Dumped by pg_dump version 12.12 (Ubuntu 12.12-0ubuntu0.20.04.1) + +-- Started on 2022-09-24 20:18:09 IST + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- TOC entry 626 (class 1247 OID 16424) +-- Name: transaction_status; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.transaction_status AS ENUM ( + 'PENDING', + 'SUCCESS', + 'FAILED' +); + + +-- +-- TOC entry 637 (class 1247 OID 16868) +-- Name: tx_status; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.tx_status AS ENUM ( + 'SUCCESS', + 'PENDING', + 'FAILED' +); + + +SET default_table_access_method = heap; + +-- +-- TOC entry 204 (class 1259 OID 16822) +-- Name: multisig_accounts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.multisig_accounts ( + address character varying(50) NOT NULL, + threshold integer NOT NULL, + chain_id character varying(20) NOT NULL, + pubkey_type character varying(50) NOT NULL, + name character varying(100) NOT NULL, + created_by character varying(50) NOT NULL, + created_at timestamp with time zone DEFAULT '2022-09-23 22:26:53.911454+05:30'::timestamp with time zone NOT NULL +); + +-- +-- TOC entry 204 (class 1259 OID 16822) +-- Name: users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users ( + id SERIAL PRIMARY KEY, + address character varying(100) NOT NULL, + salt INTEGER CHECK (salt > 0), + signature character varying(250) NOT NULL, + pub_key jsonb DEFAULT '[]'::jsonb, + created_at timestamp with time zone DEFAULT '2022-09-23 22:26:53.911454+05:30'::timestamp with time zone NOT NULL +); + + +-- +-- TOC entry 205 (class 1259 OID 16840) +-- Name: pubkeys; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.pubkeys ( + address character varying(50) NOT NULL, + multisig_address character varying(50) NOT NULL, + pubkey jsonb NOT NULL +); + + +-- +-- TOC entry 207 (class 1259 OID 16877) +-- Name: transactions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.transactions ( + id integer NOT NULL, + multisig_address character varying(50) NOT NULL, + fee jsonb, + status public.tx_status DEFAULT 'PENDING'::public.tx_status NOT NULL, + messages jsonb DEFAULT '[]'::jsonb, + hash text DEFAULT ''::text, + err_msg text DEFAULT ''::text, + memo text DEFAULT ''::text, + signatures jsonb DEFAULT '[]'::jsonb, + last_updated timestamp with time zone DEFAULT '2022-09-23 22:26:53.911454+05:30'::timestamp with time zone NOT NULL, + created_at timestamp with time zone DEFAULT '2022-09-23 22:27:24.815343+05:30'::timestamp with time zone NOT NULL +); + +-- +-- TOC entry 206 (class 1259 OID 16444) +-- Name: price_info; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.price_info ( + denom character varying(50) NOT NULL, + coingecko_name character varying(50) NOT NULL, + enabled boolean DEFAULT true NOT NULL, + last_updated timestamp with time zone, + info jsonb DEFAULT '{}'::jsonb +); + + +-- +-- TOC entry 2969 (class 0 OID 16444) +-- Dependencies: 206 +-- Data for Name: price_info; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.price_info (denom, coingecko_name, enabled, last_updated, info) FROM stdin; +uatom cosmos t 2022-10-04 09:10:29.043476+00 {} +uregen regen t 2022-10-04 09:10:29.043476+00 {} +uosmo osmosis t 2022-10-04 09:10:29.043476+00 {} +ujuno juno-network t 2022-10-04 09:10:29.043476+00 {} +ustars stargaze t 2022-10-04 09:10:29.043476+00 {} +uakt akash-network t 2022-10-04 09:10:29.043476+00 {} +\. + + + +-- +-- TOC entry 206 (class 1259 OID 16875) +-- Name: transactions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.transactions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- TOC entry 3005 (class 0 OID 0) +-- Dependencies: 206 +-- Name: transactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.transactions_id_seq OWNED BY public.transactions.id; + + +-- +-- TOC entry 2857 (class 2604 OID 16880) +-- Name: transactions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.transactions ALTER COLUMN id SET DEFAULT nextval('public.transactions_id_seq'::regclass); + + +-- +-- TOC entry 2867 (class 2606 OID 16952) +-- Name: multisig_accounts multisig_accounts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.multisig_accounts + ADD CONSTRAINT multisig_accounts_pkey PRIMARY KEY (address); + + +-- +-- TOC entry 2869 (class 2606 OID 17014) +-- Name: pubkeys pubkeys_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pubkeys + ADD CONSTRAINT pubkeys_pkey PRIMARY KEY (address, multisig_address); + + +-- +-- TOC entry 2871 (class 2606 OID 16890) +-- Name: transactions transactions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.transactions + ADD CONSTRAINT transactions_pkey PRIMARY KEY (id); + + +-- +-- TOC entry 2872 (class 2606 OID 17015) +-- Name: pubkeys pubkeys_multisig_address_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pubkeys + ADD CONSTRAINT pubkeys_multisig_address_fkey FOREIGN KEY (multisig_address) REFERENCES public.multisig_accounts(address); + + +-- +-- TOC entry 2873 (class 2606 OID 17028) +-- Name: transactions transactions_multisig_address_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.transactions + ADD CONSTRAINT transactions_multisig_address_fkey FOREIGN KEY (multisig_address) REFERENCES public.multisig_accounts(address); + + +-- Completed on 2022-09-24 20:18:09 IST + +-- +-- PostgreSQL database dump complete +-- + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..35606ff57 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + frontend: + build: + context: ./frontend + args: + NEXT_PUBLIC_APP_API_URI: http://${HOST_IP}:1323 + ports: + - "3000:3000" + depends_on: + backend: + condition: service_healthy + networks: + - my_network + + backend: + build: + context: ./server + ports: + - "1323:1323" + depends_on: + database: + condition: service_healthy + networks: + - my_network + healthcheck: + test: ["CMD", "curl", "-f", "http://backend:1323"] + interval: 30s + timeout: 10s + retries: 3 + + database: + build: + context: ./db-docker + ports: + - "5432:5432" + environment: + - POSTGRES_DB=multisig + - POSTGRES_USER=alice + - POSTGRES_PASSWORD=password + networks: + - my_network + healthcheck: + test: ["CMD", "pg_isready", "-U", "alice", "-d", "multisig", "-h", "localhost"] + interval: 30s + timeout: 10s + retries: 3 + +networks: + my_network: + driver: bridge diff --git a/docker_compose_README.md b/docker_compose_README.md new file mode 100644 index 000000000..bb27f181a --- /dev/null +++ b/docker_compose_README.md @@ -0,0 +1,56 @@ + +# Docker Compose Deployment Instructions + +This document provides the necessary steps to deploy the application using Docker Compose. + +## Prerequisites + +- Ensure Docker and Docker Compose are installed on your system. + +## Steps to Deploy the Application + +### 1. Export Host IP Address + +First, you need to export your host IP address as an environment variable. This will be used in the Docker Compose file. + +Open your terminal and run the following command: + +```sh +export HOST_IP=$(hostname -I | awk '{print $1}') +``` + +This command fetches the IP address of your host machine and stores it in the `HOST_IP` environment variable. + +### 2. Run Docker Compose + +After exporting the `HOST_IP` variable, you can start the application using Docker Compose. Run the following command: + +```sh +docker compose up -d +``` + +The `-d` flag tells Docker Compose to run the containers in detached mode, meaning they will run in the background. + +### 3. Verify the Deployment + +To verify that the containers are running, use the following command: + +```sh +docker ps +``` + +This will list all running containers. You should see the containers defined in your `docker-compose.yml` file listed here. + +### 4. Access the Application + +Once the containers are running, you can access the application through your web browser or any other client as per the service configuration in your `docker-compose.yml`. + +### 5. Stopping the Application + +To stop and remove the running containers, use the following command: + +```sh +docker compose down +``` + +This will stop the containers and remove them along with any networks created by Docker Compose. diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 000000000..7c3e963a9 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,4 @@ +NEXT_PUBLIC_APP_API_URI= +NEXT_PUBLIC_SQUID_ID= +# To use Cosmwasm contracts provide the dummy wallet mnemonic, create a dummy wallet and add mnemonic here, also have minimum tokens in this wallet. +NEXT_PUBLIC_DUMMY_WALLET_MNEMONIC= \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..9e84fc364 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +FROM node:lts AS build + +WORKDIR /app + +COPY package.json ./ + +RUN yarn install --frozen-lockfile + +COPY . . + +ARG NEXT_PUBLIC_APP_API_URI + +ENV NEXT_PUBLIC_APP_API_URI=${NEXT_PUBLIC_APP_API_URI} + +RUN yarn build + +EXPOSE 3000 + +CMD ["yarn", "start"] diff --git a/frontend/next.config.js b/frontend/next.config.js index 590747b68..18294931e 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -2,6 +2,10 @@ const nextConfig = { images: { remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, { protocol: 'https', hostname: 'raw.githubusercontent.com', @@ -14,8 +18,25 @@ const nextConfig = { port: '', pathname: '/**', }, + { + protocol: 'https', + hostname: 'raw.githubusercontent.com', + port: '', + pathname: '/cosmos/**', + }, + { + protocol: 'https', + hostname: 'resolute.sgp1.cdn.digitaloceanspaces.com', + port: '', + pathname: '/**', + }, ], }, }; -module.exports = nextConfig; +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}); + +module.exports = withBundleAnalyzer(nextConfig); + diff --git a/frontend/package.json b/frontend/package.json index b5f2f1e1c..44b1be3b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,13 +3,16 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "yarn lint && next dev", + "dev": "yarn lint && next dev --turbo", "build": "next build", "start": "yarn lint && next start", - "lint": "next lint --no-cache" + "lint": "next lint --no-cache", + "analyze": "ANALYZE=true next build" }, "dependencies": { + "@0xsquid/sdk": "^1.14.15", "@cosmjs/amino": "^0.31.3", + "@cosmjs/cosmwasm-stargate": "0.32.2", "@cosmjs/proto-signing": "^0.32.1", "@cosmjs/stargate": "^0.32.1", "@emotion/cache": "^11.11.0", @@ -17,39 +20,46 @@ "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", "@keplr-wallet/types": "^0.12.39", + "@leapwallet/cosmos-snap-provider": "^0.1.25", "@mui/icons-material": "^5.14.11", "@mui/material": "^5.14.10", + "@mui/x-date-pickers": "5.0.4", "@reduxjs/toolkit": "^1.9.7", - "@skip-router/core": "^1.1.1", + "@skip-router/core": "^1.3.11", "@types/node": "20.6.5", + "@types/node-gzip": "1.1.0", "@types/react": "18.2.37", "@types/react-dom": "18.2.15", "autoprefixer": "10.4.16", "axios": "^1.5.1", + "chain-registry": "1.28.1", "chart.js": "^4.4.1", "cosmjs-types": "^0.9.0", + "date-fns": "2.30.0", "eslint": "8.50.0", "eslint-config-next": "13.5.2", + "fast-average-color": "^9.4.0", "jsonschema": "^1.4.1", "lodash": "^4.17.21", "mathjs": "^12.0.0", "moment": "^2.29.4", "next": "^14.0.1", + "node-gzip": "^1.1.2", "postcss": "8.4.30", "react": "^18.2.0", - "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", + "react-ga": "^3.3.1", "react-hook-form": "^7.47.0", - "react-particles": "^2.12.2", "react-redux": "^8.1.3", "react-remark": "^2.1.0", "react-router-dom": "^6.18.0", "sharp": "^0.33.1", - "tailwindcss": "3.3.3", - "tsparticles-slim": "^2.12.0", - "typescript": "5.3.2" + "tailwindcss": "3.4.0", + "typescript": "5.3.2", + "xlsx": "^0.18.5" }, "devDependencies": { + "@next/bundle-analyzer": "^14.2.3", "@types/lodash": "^4.14.200", "@typescript-eslint/eslint-plugin": "^6.9.1", "eslint-config-prettier": "^9.0.0", diff --git a/frontend/public/Timer-icon.svg b/frontend/public/Timer-icon.svg new file mode 100644 index 000000000..5cecb3711 --- /dev/null +++ b/frontend/public/Timer-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/ad.png b/frontend/public/ad.png deleted file mode 100644 index 59bc74e79..000000000 Binary files a/frontend/public/ad.png and /dev/null differ diff --git a/frontend/public/add-network-template.json b/frontend/public/add-network-template.json index d3be476a9..aa3c5421a 100644 --- a/frontend/public/add-network-template.json +++ b/frontend/public/add-network-template.json @@ -1,68 +1,80 @@ { - "enable_modules": { - "authz": true, - "feegrant": true, - "group": false - }, - "amino_config": { - "authz": false, - "feegrant": false, - "group": false - }, - "show_airdrop": false, - "logos": { - "menu": "https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/cosmoshub/atom.png", - "toolbar": "https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/cosmoshub/images/cosmoshub-logo.png" - }, - "keplr_experimental": false, - "leap_experimental": false, - "is_testnet": false, - "explorer_tx_hash_endpoint": "https://www.mintscan.io/cosmos/txs/", - "config": { - "chain_id": "cosmoshub-4", - "chain_name": "CosmosHub", - "rest": "https://api.resolute.vitwit.com/cosmos_api", - "rpc": "https://api.resolute.vitwit.com/cosmos_rpc", - "currencies": [ - { - "coin_denom": "ATOM", - "coin_minimal_denom": "uatom", - "coin_decimals": 6 - } - ], - "bech32_config": { - "bech32_prefix_acc_addr": "cosmos", - "bech32_prefix_acc_pub": "cosmospub", - "bech32_prefix_val_addr": "cosmosvaloper", - "bech32_prefix_val_pub": "cosmosvaloperpub", - "bech32_prefix_cons_addr": "cosmosgvalcons", - "bech32_prefix_cons_pub": "cosmosvalconspub" - }, - "fee_currencies": [ - { - "coin_denom": "ATOM", - "coin_minimal_denom": "uatom", - "coin_decimals": 6, - "gas_price_step": { - "low": 0.01, - "average": 0.025, - "high": 0.03 - } - } - ], - "bip44": { - "coin_type": 118 - }, - "stake_currency": { + "enable_modules": { + "authz": true, + "feegrant": true, + "group": false + }, + "amino_config": { + "authz": false, + "feegrant": false, + "group": false + }, + "show_airdrop": false, + "logos": { + "menu": "https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/cosmoshub/atom.png", + "toolbar": "https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/cosmoshub/images/cosmoshub-logo.png" + }, + "supported_wallets": ["keplr", "leap", "cosmostation"], + "keplr_experimental": false, + "leap_experimental": false, + "is_testnet": false, + "gov_v1": false, + "explorer_tx_hash_endpoint": "https://www.mintscan.io/cosmos/txs/", + "config": { + "chain_id": "cosmoshub-4", + "chain_name": "CosmosHub", + "rest": "https://api.resolute.vitwit.com/cosmos_api", + "rpc": "https://api.resolute.vitwit.com/cosmos_rpc", + "restURIs": [ + "https://api-cosmoshub-ia.cosmosia.notional.ventures", + "https://cosmos-lcd.quickapi.com:443", + "https://cosmos-rest.staketab.org", + "https://lcd-cosmoshub.blockapsis.com" + ], + "rpcURIs": [ + "https://cosmos-rpc.polkachu.com", + "https://rpc-cosmoshub.blockapsis.com", + "https://cosmos-rpc.quickapi.com:443" + ], + "currencies": [ + { "coin_denom": "ATOM", "coin_minimal_denom": "uatom", "coin_decimals": 6 - }, - "image": "https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg", - "theme": { - "primary_color": "#fff", - "gradient": "linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)" } + ], + "bech32_config": { + "bech32_prefix_acc_addr": "cosmos", + "bech32_prefix_acc_pub": "cosmospub", + "bech32_prefix_val_addr": "cosmosvaloper", + "bech32_prefix_val_pub": "cosmosvaloperpub", + "bech32_prefix_cons_addr": "cosmosgvalcons", + "bech32_prefix_cons_pub": "cosmosvalconspub" + }, + "fee_currencies": [ + { + "coin_denom": "ATOM", + "coin_minimal_denom": "uatom", + "coin_decimals": 6, + "gas_price_step": { + "low": 0.01, + "average": 0.025, + "high": 0.03 + } + } + ], + "bip44": { + "coin_type": 118 + }, + "stake_currency": { + "coin_denom": "ATOM", + "coin_minimal_denom": "uatom", + "coin_decimals": 6 + }, + "image": "https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg", + "theme": { + "primary_color": "#fff", + "gradient": "linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)" } } - \ No newline at end of file +} diff --git a/frontend/public/address-icon.svg b/frontend/public/address-icon.svg deleted file mode 100644 index 32ebf99b3..000000000 --- a/frontend/public/address-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/after-check.svg b/frontend/public/after-check.svg new file mode 100644 index 000000000..f1e09a13b --- /dev/null +++ b/frontend/public/after-check.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/akash-logo.svg b/frontend/public/akash-logo.svg deleted file mode 100644 index 1a8925c06..000000000 --- a/frontend/public/akash-logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/akash1.png b/frontend/public/akash1.png new file mode 100644 index 000000000..a8c9e0ef8 Binary files /dev/null and b/frontend/public/akash1.png differ diff --git a/frontend/public/all-networks-icon.png b/frontend/public/all-networks-icon.png deleted file mode 100644 index a19e27bf8..000000000 Binary files a/frontend/public/all-networks-icon.png and /dev/null differ diff --git a/frontend/public/allnetworks.png b/frontend/public/allnetworks.png deleted file mode 100644 index 27e3c09a7..000000000 Binary files a/frontend/public/allnetworks.png and /dev/null differ diff --git a/frontend/public/authz-icon-active.svg b/frontend/public/authz-icon-active.svg index 510db0b0a..b6b169165 100644 --- a/frontend/public/authz-icon-active.svg +++ b/frontend/public/authz-icon-active.svg @@ -1,9 +1,9 @@ - - + + - + diff --git a/frontend/public/authz-icon.svg b/frontend/public/authz-icon.svg index 3ea0e7d6b..9b55643bd 100644 --- a/frontend/public/authz-icon.svg +++ b/frontend/public/authz-icon.svg @@ -1,9 +1,9 @@ - - + + - + diff --git a/frontend/public/avail-bal.png b/frontend/public/avail-bal.png new file mode 100644 index 000000000..819680d69 Binary files /dev/null and b/frontend/public/avail-bal.png differ diff --git a/frontend/public/average-fee-icon.svg b/frontend/public/average-fee-icon.svg deleted file mode 100644 index c04b44b5a..000000000 --- a/frontend/public/average-fee-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/back-arrow.svg b/frontend/public/back-arrow.svg deleted file mode 100644 index 0c700c731..000000000 --- a/frontend/public/back-arrow.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/backarrow-icon.svg b/frontend/public/backarrow-icon.svg deleted file mode 100644 index cc9c8bf2c..000000000 --- a/frontend/public/backarrow-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/background-circle.png b/frontend/public/background-circle.png deleted file mode 100644 index 70afc07dc..000000000 Binary files a/frontend/public/background-circle.png and /dev/null differ diff --git a/frontend/public/balanceAmount.svg b/frontend/public/balanceAmount.svg deleted file mode 100644 index 31d8280f1..000000000 --- a/frontend/public/balanceAmount.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/before-check.svg b/frontend/public/before-check.svg new file mode 100644 index 000000000..34c67dd94 --- /dev/null +++ b/frontend/public/before-check.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/blocks-image-2.png b/frontend/public/blocks-image-2.png deleted file mode 100644 index b57e894d3..000000000 Binary files a/frontend/public/blocks-image-2.png and /dev/null differ diff --git a/frontend/public/blocks-image.png b/frontend/public/blocks-image.png deleted file mode 100644 index 5deefed9e..000000000 Binary files a/frontend/public/blocks-image.png and /dev/null differ diff --git a/frontend/public/blocks.png b/frontend/public/blocks.png deleted file mode 100644 index 23d33ad79..000000000 Binary files a/frontend/public/blocks.png and /dev/null differ diff --git a/frontend/public/check-circle-icon.svg b/frontend/public/check-circle-icon.svg deleted file mode 100644 index 4e4f986fb..000000000 --- a/frontend/public/check-circle-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/claim-and-stake-icon.svg b/frontend/public/claim-and-stake-icon.svg deleted file mode 100644 index f66d4ca47..000000000 --- a/frontend/public/claim-and-stake-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/claim-icon.svg b/frontend/public/claim-icon.svg deleted file mode 100644 index f2d6eb554..000000000 --- a/frontend/public/claim-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/claim-stake-icon.svg b/frontend/public/claim-stake-icon.svg deleted file mode 100644 index afd018450..000000000 --- a/frontend/public/claim-stake-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/claim.svg b/frontend/public/claim.svg deleted file mode 100644 index 884c3059f..000000000 --- a/frontend/public/claim.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/close.png b/frontend/public/close.png deleted file mode 100644 index 7002ac75f..000000000 Binary files a/frontend/public/close.png and /dev/null differ diff --git a/frontend/public/close.svg b/frontend/public/close.svg new file mode 100644 index 000000000..eda62410e --- /dev/null +++ b/frontend/public/close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/copy-icon.svg b/frontend/public/copy-icon.svg deleted file mode 100644 index 11e96cc73..000000000 --- a/frontend/public/copy-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/copy.svg b/frontend/public/copy.svg index a876417e2..e1a9f1277 100644 --- a/frontend/public/copy.svg +++ b/frontend/public/copy.svg @@ -1,11 +1,5 @@ - + - + - - - - - - diff --git a/frontend/public/cosmos-background-light.png b/frontend/public/cosmos-background-light.png deleted file mode 100644 index efdd9e629..000000000 Binary files a/frontend/public/cosmos-background-light.png and /dev/null differ diff --git a/frontend/public/cosmos-background.png b/frontend/public/cosmos-background.png deleted file mode 100644 index e47b2c6b7..000000000 Binary files a/frontend/public/cosmos-background.png and /dev/null differ diff --git a/frontend/public/cosmos-icon.svg b/frontend/public/cosmos-icon.svg deleted file mode 100644 index 92ef71fd4..000000000 --- a/frontend/public/cosmos-icon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/cosmos-logo.svg b/frontend/public/cosmos-logo.svg deleted file mode 100644 index 100385d88..000000000 --- a/frontend/public/cosmos-logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/public/cosmos.png b/frontend/public/cosmos.png deleted file mode 100644 index c38a5124a..000000000 Binary files a/frontend/public/cosmos.png and /dev/null differ diff --git a/frontend/public/cosmwasm-icon-active.svg b/frontend/public/cosmwasm-icon-active.svg new file mode 100644 index 000000000..89a77c9b8 --- /dev/null +++ b/frontend/public/cosmwasm-icon-active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/cosmwasm-icon.svg b/frontend/public/cosmwasm-icon.svg new file mode 100644 index 000000000..c1f1b5d7f --- /dev/null +++ b/frontend/public/cosmwasm-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/public/dashboard.png b/frontend/public/dashboard.png new file mode 100644 index 000000000..352bd317a Binary files /dev/null and b/frontend/public/dashboard.png differ diff --git a/frontend/public/delegate-icon.svg b/frontend/public/delegate-icon.svg deleted file mode 100644 index 600908956..000000000 --- a/frontend/public/delegate-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/delegate-popup-image.png b/frontend/public/delegate-popup-image.png deleted file mode 100644 index c573d45e0..000000000 Binary files a/frontend/public/delegate-popup-image.png and /dev/null differ diff --git a/frontend/public/delete-icon-outlined.svg b/frontend/public/delete-icon-outlined.svg deleted file mode 100644 index 934152f1a..000000000 --- a/frontend/public/delete-icon-outlined.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/delete-icon.svg b/frontend/public/delete-icon.svg deleted file mode 100644 index 04f7107a2..000000000 --- a/frontend/public/delete-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/delete-txn-popup-image.png b/frontend/public/delete-txn-popup-image.png deleted file mode 100644 index 0c7abd886..000000000 Binary files a/frontend/public/delete-txn-popup-image.png and /dev/null differ diff --git a/frontend/public/delete.svg b/frontend/public/delete.svg new file mode 100644 index 000000000..841ac22d1 --- /dev/null +++ b/frontend/public/delete.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/deposit.png b/frontend/public/deposit.png deleted file mode 100644 index ea8378b50..000000000 Binary files a/frontend/public/deposit.png and /dev/null differ diff --git a/frontend/public/disable-claim-icon.svg b/frontend/public/disable-claim-icon.svg deleted file mode 100644 index 95e0a5964..000000000 --- a/frontend/public/disable-claim-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/disable-restake.svg b/frontend/public/disable-restake.svg deleted file mode 100644 index 1c5ca6a55..000000000 --- a/frontend/public/disable-restake.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/discord-logo.jpg b/frontend/public/discord-logo.jpg new file mode 100644 index 000000000..348f05dcc Binary files /dev/null and b/frontend/public/discord-logo.jpg differ diff --git a/frontend/public/discord-logo.png b/frontend/public/discord-logo.png new file mode 100644 index 000000000..baececa90 Binary files /dev/null and b/frontend/public/discord-logo.png differ diff --git a/frontend/public/done-icon.svg b/frontend/public/done-icon.svg deleted file mode 100644 index 2b3bb113f..000000000 --- a/frontend/public/done-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/down-arrow-icon.svg b/frontend/public/down-arrow-icon.svg new file mode 100644 index 000000000..02e08d48e --- /dev/null +++ b/frontend/public/down-arrow-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/down-arrow.svg b/frontend/public/down-arrow.svg new file mode 100644 index 000000000..68e3cd4d5 --- /dev/null +++ b/frontend/public/down-arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/down.svg b/frontend/public/down.svg new file mode 100644 index 000000000..b0ec4b08a --- /dev/null +++ b/frontend/public/down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/drop-down-icon.svg b/frontend/public/drop-down-icon.svg index d62636a2c..19af8b0d2 100644 --- a/frontend/public/drop-down-icon.svg +++ b/frontend/public/drop-down-icon.svg @@ -1,12 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/frontend/public/dropdown-icon.svg b/frontend/public/dropdown-icon.svg deleted file mode 100644 index 497838af2..000000000 --- a/frontend/public/dropdown-icon.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/frontend/public/empty-messages-image.png b/frontend/public/empty-messages-image.png deleted file mode 100644 index 5718f0604..000000000 Binary files a/frontend/public/empty-messages-image.png and /dev/null differ diff --git a/frontend/public/expand-close.svg b/frontend/public/expand-close.svg new file mode 100644 index 000000000..9ca687d80 --- /dev/null +++ b/frontend/public/expand-close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/expand-open.svg b/frontend/public/expand-open.svg new file mode 100644 index 000000000..83f6de8ef --- /dev/null +++ b/frontend/public/expand-open.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/failed-icon.svg b/frontend/public/failed-icon.svg new file mode 100644 index 000000000..4d0f49626 --- /dev/null +++ b/frontend/public/failed-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/feegrant-icon.svg b/frontend/public/feegrant-icon.svg deleted file mode 100644 index 176f84c3b..000000000 --- a/frontend/public/feegrant-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/github-logo.png b/frontend/public/github-logo.png new file mode 100644 index 000000000..9fd700cca Binary files /dev/null and b/frontend/public/github-logo.png differ diff --git a/frontend/public/go-back-icon.svg b/frontend/public/go-back-icon.svg deleted file mode 100644 index a1e46d23f..000000000 --- a/frontend/public/go-back-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/gov-illustration.png b/frontend/public/gov-illustration.png deleted file mode 100644 index 38cb83533..000000000 Binary files a/frontend/public/gov-illustration.png and /dev/null differ diff --git a/frontend/public/governance-icon.svg b/frontend/public/governance-icon.svg deleted file mode 100644 index a606e6aac..000000000 --- a/frontend/public/governance-icon.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/frontend/public/graph-view-icon.svg b/frontend/public/graph-view-icon.svg deleted file mode 100644 index 3ca15538f..000000000 --- a/frontend/public/graph-view-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/groups-icon-active.svg b/frontend/public/groups-icon-active.svg deleted file mode 100644 index fc376f35e..000000000 --- a/frontend/public/groups-icon-active.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/groups-icon.svg b/frontend/public/groups-icon.svg deleted file mode 100644 index bef7f6691..000000000 --- a/frontend/public/groups-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/help-icon.svg b/frontend/public/help-icon.svg deleted file mode 100644 index 506fedb09..000000000 --- a/frontend/public/help-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/high-fee-icon.svg b/frontend/public/high-fee-icon.svg deleted file mode 100644 index b965d115e..000000000 --- a/frontend/public/high-fee-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/history-icon.svg b/frontend/public/history-icon.svg new file mode 100644 index 000000000..0b6341805 --- /dev/null +++ b/frontend/public/history-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/public/icons/add-icon-rounded.svg b/frontend/public/icons/add-icon-rounded.svg new file mode 100644 index 000000000..152f55b31 --- /dev/null +++ b/frontend/public/icons/add-icon-rounded.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/public/icons/add-icon.svg b/frontend/public/icons/add-icon.svg new file mode 100644 index 000000000..af2127ff2 --- /dev/null +++ b/frontend/public/icons/add-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/public/icons/alert-icon.svg b/frontend/public/icons/alert-icon.svg new file mode 100644 index 000000000..b6ab0424d --- /dev/null +++ b/frontend/public/icons/alert-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/all-networks-icon.png b/frontend/public/icons/all-networks-icon.png new file mode 100644 index 000000000..0abaef7d2 Binary files /dev/null and b/frontend/public/icons/all-networks-icon.png differ diff --git a/frontend/public/icons/cancel-icon-solid.svg b/frontend/public/icons/cancel-icon-solid.svg new file mode 100644 index 000000000..bb3df7331 --- /dev/null +++ b/frontend/public/icons/cancel-icon-solid.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/check-filled.svg b/frontend/public/icons/check-filled.svg new file mode 100644 index 000000000..ddb9591b9 --- /dev/null +++ b/frontend/public/icons/check-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons/copy-icon.svg b/frontend/public/icons/copy-icon.svg new file mode 100644 index 000000000..dea6ce04b --- /dev/null +++ b/frontend/public/icons/copy-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/cross-icon.svg b/frontend/public/icons/cross-icon.svg new file mode 100644 index 000000000..1dcff7661 --- /dev/null +++ b/frontend/public/icons/cross-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons/drop-down-arrow-filled.svg b/frontend/public/icons/drop-down-arrow-filled.svg new file mode 100644 index 000000000..c37f9eff2 --- /dev/null +++ b/frontend/public/icons/drop-down-arrow-filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/failed-icon.png b/frontend/public/icons/failed-icon.png new file mode 100644 index 000000000..cad79545f Binary files /dev/null and b/frontend/public/icons/failed-icon.png differ diff --git a/frontend/public/icons/flip-icon.svg b/frontend/public/icons/flip-icon.svg new file mode 100644 index 000000000..d47040c56 --- /dev/null +++ b/frontend/public/icons/flip-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/public/icons/globe-icon.svg b/frontend/public/icons/globe-icon.svg new file mode 100644 index 000000000..b99b51c37 --- /dev/null +++ b/frontend/public/icons/globe-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/icons/i-icon.svg b/frontend/public/icons/i-icon.svg new file mode 100644 index 000000000..c512d1381 --- /dev/null +++ b/frontend/public/icons/i-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/logout.png b/frontend/public/icons/logout.png new file mode 100644 index 000000000..7bbe8509f Binary files /dev/null and b/frontend/public/icons/logout.png differ diff --git a/frontend/public/icons/menu-icon.svg b/frontend/public/icons/menu-icon.svg new file mode 100644 index 000000000..5565a713b --- /dev/null +++ b/frontend/public/icons/menu-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/icons/minus-icon-disabled.svg b/frontend/public/icons/minus-icon-disabled.svg new file mode 100644 index 000000000..950e6a07b --- /dev/null +++ b/frontend/public/icons/minus-icon-disabled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/minus-icon.svg b/frontend/public/icons/minus-icon.svg new file mode 100644 index 000000000..36fcf8fac --- /dev/null +++ b/frontend/public/icons/minus-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/notification-icon.svg b/frontend/public/icons/notification-icon.svg new file mode 100644 index 000000000..1b648f01b --- /dev/null +++ b/frontend/public/icons/notification-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/plus-icon-disabled.svg b/frontend/public/icons/plus-icon-disabled.svg new file mode 100644 index 000000000..e32a06458 --- /dev/null +++ b/frontend/public/icons/plus-icon-disabled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/plus-icon.svg b/frontend/public/icons/plus-icon.svg new file mode 100644 index 000000000..0ddf684a7 --- /dev/null +++ b/frontend/public/icons/plus-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/redirect-icon-green.svg b/frontend/public/icons/redirect-icon-green.svg new file mode 100644 index 000000000..6c9ae9175 --- /dev/null +++ b/frontend/public/icons/redirect-icon-green.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/redirect-icon-red.svg b/frontend/public/icons/redirect-icon-red.svg new file mode 100644 index 000000000..e0776dbf9 --- /dev/null +++ b/frontend/public/icons/redirect-icon-red.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/redirect-icon.svg b/frontend/public/icons/redirect-icon.svg new file mode 100644 index 000000000..c1b253d55 --- /dev/null +++ b/frontend/public/icons/redirect-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/remove-icon-outlined.svg b/frontend/public/icons/remove-icon-outlined.svg new file mode 100644 index 000000000..3c4b0a85b --- /dev/null +++ b/frontend/public/icons/remove-icon-outlined.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/remove-icon.svg b/frontend/public/icons/remove-icon.svg new file mode 100644 index 000000000..ddd3ee64c --- /dev/null +++ b/frontend/public/icons/remove-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/repeat-icon.png b/frontend/public/icons/repeat-icon.png new file mode 100644 index 000000000..c21752aa9 Binary files /dev/null and b/frontend/public/icons/repeat-icon.png differ diff --git a/frontend/public/icons/right-arrow-icon.svg b/frontend/public/icons/right-arrow-icon.svg new file mode 100644 index 000000000..cb49d145f --- /dev/null +++ b/frontend/public/icons/right-arrow-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/icons/route-icon.svg b/frontend/public/icons/route-icon.svg new file mode 100644 index 000000000..05c3d61b2 --- /dev/null +++ b/frontend/public/icons/route-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/icons/search-icon.svg b/frontend/public/icons/search-icon.svg new file mode 100644 index 000000000..f7c5e7833 --- /dev/null +++ b/frontend/public/icons/search-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/settings-icon.svg b/frontend/public/icons/settings-icon.svg new file mode 100644 index 000000000..da7a6e9e0 --- /dev/null +++ b/frontend/public/icons/settings-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/icons/share-icon.svg b/frontend/public/icons/share-icon.svg new file mode 100644 index 000000000..793bdfab7 --- /dev/null +++ b/frontend/public/icons/share-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/styled-drop-down-icon-close.svg b/frontend/public/icons/styled-drop-down-icon-close.svg new file mode 100644 index 000000000..739f220f8 --- /dev/null +++ b/frontend/public/icons/styled-drop-down-icon-close.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/icons/styled-drop-down-icon-open.svg b/frontend/public/icons/styled-drop-down-icon-open.svg new file mode 100644 index 000000000..fb4587adc --- /dev/null +++ b/frontend/public/icons/styled-drop-down-icon-open.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/icons/success-icon.png b/frontend/public/icons/success-icon.png new file mode 100644 index 000000000..24e7226de Binary files /dev/null and b/frontend/public/icons/success-icon.png differ diff --git a/frontend/public/icons/swap-icon-filled.svg b/frontend/public/icons/swap-icon-filled.svg new file mode 100644 index 000000000..1779813fd --- /dev/null +++ b/frontend/public/icons/swap-icon-filled.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/icons/swap-route.svg b/frontend/public/icons/swap-route.svg new file mode 100644 index 000000000..6e56a6902 --- /dev/null +++ b/frontend/public/icons/swap-route.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/icons/tick-icon.svg b/frontend/public/icons/tick-icon.svg new file mode 100644 index 000000000..1da6fb213 --- /dev/null +++ b/frontend/public/icons/tick-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/timer-icon.svg b/frontend/public/icons/timer-icon.svg new file mode 100644 index 000000000..67a175f51 --- /dev/null +++ b/frontend/public/icons/timer-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/toggle-off.svg b/frontend/public/icons/toggle-off.svg new file mode 100644 index 000000000..6669d1e36 --- /dev/null +++ b/frontend/public/icons/toggle-off.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/toggle-on.svg b/frontend/public/icons/toggle-on.svg new file mode 100644 index 000000000..0c04aed91 --- /dev/null +++ b/frontend/public/icons/toggle-on.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/public/icons/twitter-icon.png b/frontend/public/icons/twitter-icon.png new file mode 100644 index 000000000..b795454ab Binary files /dev/null and b/frontend/public/icons/twitter-icon.png differ diff --git a/frontend/public/icons/upload-icon.svg b/frontend/public/icons/upload-icon.svg new file mode 100644 index 000000000..a91bdbb03 --- /dev/null +++ b/frontend/public/icons/upload-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/illustrate.png b/frontend/public/illustrate.png new file mode 100644 index 000000000..f526d43b3 Binary files /dev/null and b/frontend/public/illustrate.png differ diff --git a/frontend/public/illustrations/delete.png b/frontend/public/illustrations/delete.png new file mode 100644 index 000000000..184bd4158 Binary files /dev/null and b/frontend/public/illustrations/delete.png differ diff --git a/frontend/public/illustrations/empty-illustration.png b/frontend/public/illustrations/empty-illustration.png new file mode 100644 index 000000000..352bd317a Binary files /dev/null and b/frontend/public/illustrations/empty-illustration.png differ diff --git a/frontend/public/illustrations/no-data-illustration.png b/frontend/public/illustrations/no-data-illustration.png new file mode 100644 index 000000000..f526d43b3 Binary files /dev/null and b/frontend/public/illustrations/no-data-illustration.png differ diff --git a/frontend/public/illustrations/no-messages-illustration.png b/frontend/public/illustrations/no-messages-illustration.png new file mode 100644 index 000000000..63b08c581 Binary files /dev/null and b/frontend/public/illustrations/no-messages-illustration.png differ diff --git a/frontend/public/illustrations/verify-illustration.png b/frontend/public/illustrations/verify-illustration.png new file mode 100644 index 000000000..7bcdcedea Binary files /dev/null and b/frontend/public/illustrations/verify-illustration.png differ diff --git a/frontend/public/info-yellow.svg b/frontend/public/info-yellow.svg new file mode 100644 index 000000000..fb95f0323 --- /dev/null +++ b/frontend/public/info-yellow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/infoblack.svg b/frontend/public/infoblack.svg new file mode 100644 index 000000000..2380d7575 --- /dev/null +++ b/frontend/public/infoblack.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/keplr-wallet-logo.png b/frontend/public/keplr-wallet-logo.png index 0d85b5be5..8e075e153 100644 Binary files a/frontend/public/keplr-wallet-logo.png and b/frontend/public/keplr-wallet-logo.png differ diff --git a/frontend/public/key.svg b/frontend/public/key.svg deleted file mode 100644 index c3e023ecd..000000000 --- a/frontend/public/key.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/landing-laptop.svg b/frontend/public/landing-laptop.svg deleted file mode 100644 index b4cde2e94..000000000 --- a/frontend/public/landing-laptop.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/frontend/public/list-icon.svg b/frontend/public/list-icon.svg deleted file mode 100644 index 7977ee6ef..000000000 --- a/frontend/public/list-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/logout-icon.svg b/frontend/public/logout-icon.svg deleted file mode 100644 index adea4bc9d..000000000 --- a/frontend/public/logout-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/low-fee-icon.svg b/frontend/public/low-fee-icon.svg deleted file mode 100644 index d1c6e653c..000000000 --- a/frontend/public/low-fee-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/menu-icon.svg b/frontend/public/menu-icon.svg deleted file mode 100644 index 8fe115a88..000000000 --- a/frontend/public/menu-icon.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/frontend/public/metamask.png b/frontend/public/metamask.png new file mode 100644 index 000000000..26370bb6a Binary files /dev/null and b/frontend/public/metamask.png differ diff --git a/frontend/public/metamask.svg b/frontend/public/metamask.svg new file mode 100644 index 000000000..d497670f2 --- /dev/null +++ b/frontend/public/metamask.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/more.svg b/frontend/public/more.svg new file mode 100644 index 000000000..ab5c44e9d --- /dev/null +++ b/frontend/public/more.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/multiops-icon-active.svg b/frontend/public/multiops-icon-active.svg new file mode 100644 index 000000000..12eb87269 --- /dev/null +++ b/frontend/public/multiops-icon-active.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/public/multiops-icon.svg b/frontend/public/multiops-icon.svg new file mode 100644 index 000000000..d88ac4bb2 --- /dev/null +++ b/frontend/public/multiops-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/multisig-popup-image-2.png b/frontend/public/multisig-popup-image-2.png deleted file mode 100644 index baf2349b1..000000000 Binary files a/frontend/public/multisig-popup-image-2.png and /dev/null differ diff --git a/frontend/public/multisig-popup-image.png b/frontend/public/multisig-popup-image.png deleted file mode 100644 index 4724a3023..000000000 Binary files a/frontend/public/multisig-popup-image.png and /dev/null differ diff --git a/frontend/public/network.png b/frontend/public/network.png deleted file mode 100644 index 06f60066c..000000000 Binary files a/frontend/public/network.png and /dev/null differ diff --git a/frontend/public/no-assets-illustration.png b/frontend/public/no-assets-illustration.png deleted file mode 100644 index 2e1c1830f..000000000 Binary files a/frontend/public/no-assets-illustration.png and /dev/null differ diff --git a/frontend/public/no-messages-illustration.png b/frontend/public/no-messages-illustration.png deleted file mode 100644 index b6bcd16ac..000000000 Binary files a/frontend/public/no-messages-illustration.png and /dev/null differ diff --git a/frontend/public/no-multisigs.png b/frontend/public/no-multisigs.png deleted file mode 100644 index 5f1e1395e..000000000 Binary files a/frontend/public/no-multisigs.png and /dev/null differ diff --git a/frontend/public/no-transactions-illustration.png b/frontend/public/no-transactions-illustration.png deleted file mode 100644 index 1fe87483e..000000000 Binary files a/frontend/public/no-transactions-illustration.png and /dev/null differ diff --git a/frontend/public/no-transactions.png b/frontend/public/no-transactions.png deleted file mode 100644 index 1fe87483e..000000000 Binary files a/frontend/public/no-transactions.png and /dev/null differ diff --git a/frontend/public/o.png b/frontend/public/o.png deleted file mode 100644 index 09ec92416..000000000 Binary files a/frontend/public/o.png and /dev/null differ diff --git a/frontend/public/oasis-network-logo.png b/frontend/public/oasis-network-logo.png new file mode 100644 index 000000000..01856306b Binary files /dev/null and b/frontend/public/oasis-network-logo.png differ diff --git a/frontend/public/others.svg b/frontend/public/others.svg new file mode 100644 index 000000000..aeb8484e7 --- /dev/null +++ b/frontend/public/others.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/public/page-ad-sample.png b/frontend/public/page-ad-sample.png deleted file mode 100644 index 6fef453f9..000000000 Binary files a/frontend/public/page-ad-sample.png and /dev/null differ diff --git a/frontend/public/plainclose-icon.svg b/frontend/public/plainclose-icon.svg deleted file mode 100644 index 9acf29eae..000000000 --- a/frontend/public/plainclose-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/public/polygon-logo.svg b/frontend/public/polygon-logo.svg new file mode 100644 index 000000000..522d7d9be --- /dev/null +++ b/frontend/public/polygon-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/printed-color.png b/frontend/public/printed-color.png deleted file mode 100644 index cc6f66bc8..000000000 Binary files a/frontend/public/printed-color.png and /dev/null differ diff --git a/frontend/public/profile.svg b/frontend/public/profile.svg deleted file mode 100644 index c72c60261..000000000 --- a/frontend/public/profile.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/radio-clr.svg b/frontend/public/radio-clr.svg new file mode 100644 index 000000000..19af2ac3d --- /dev/null +++ b/frontend/public/radio-clr.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/public/radio-plain.svg b/frontend/public/radio-plain.svg new file mode 100644 index 000000000..43088a06e --- /dev/null +++ b/frontend/public/radio-plain.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/raw-icon.svg b/frontend/public/raw-icon.svg deleted file mode 100644 index 2dad52fce..000000000 --- a/frontend/public/raw-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/public/report-icon.svg b/frontend/public/report-icon.svg deleted file mode 100644 index d112c270a..000000000 --- a/frontend/public/report-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/resolute-logo-vitwit.svg b/frontend/public/resolute-logo-vitwit.svg new file mode 100644 index 000000000..b7b0044ae --- /dev/null +++ b/frontend/public/resolute-logo-vitwit.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/public/resolute-logo.png b/frontend/public/resolute-logo.png new file mode 100644 index 000000000..0793648dd Binary files /dev/null and b/frontend/public/resolute-logo.png differ diff --git a/frontend/public/resolute-logo.svg b/frontend/public/resolute-logo.svg new file mode 100644 index 000000000..a837d8e30 --- /dev/null +++ b/frontend/public/resolute-logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/restake.svg b/frontend/public/restake.svg deleted file mode 100644 index 3978e9ad4..000000000 --- a/frontend/public/restake.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/rewards.png b/frontend/public/rewards.png new file mode 100644 index 000000000..a89294507 Binary files /dev/null and b/frontend/public/rewards.png differ diff --git a/frontend/public/rewardsAmount.svg b/frontend/public/rewardsAmount.svg deleted file mode 100644 index c4ef97dfc..000000000 --- a/frontend/public/rewardsAmount.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/round-checked.svg b/frontend/public/round-checked.svg deleted file mode 100644 index 8a5f1a23b..000000000 --- a/frontend/public/round-checked.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/search-icon.svg b/frontend/public/search-icon.svg deleted file mode 100644 index 0f2d0bebd..000000000 --- a/frontend/public/search-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/frontend/public/search.svg b/frontend/public/search.svg new file mode 100644 index 000000000..c083d77ce --- /dev/null +++ b/frontend/public/search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/semi-circle-left.svg b/frontend/public/semi-circle-left.svg deleted file mode 100644 index c0f7b8584..000000000 --- a/frontend/public/semi-circle-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/public/semi-circle-right.svg b/frontend/public/semi-circle-right.svg deleted file mode 100644 index 3a157a22e..000000000 --- a/frontend/public/semi-circle-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/public/sidebar-menu-icons/dashboard-icon.svg b/frontend/public/sidebar-menu-icons/dashboard-icon.svg new file mode 100644 index 000000000..19f560579 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/dashboard-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/gov-icon.svg b/frontend/public/sidebar-menu-icons/gov-icon.svg new file mode 100644 index 000000000..c87d56013 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/gov-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/logout-icon.svg b/frontend/public/sidebar-menu-icons/logout-icon.svg new file mode 100644 index 000000000..797b42107 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/logout-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/multisig-icon.svg b/frontend/public/sidebar-menu-icons/multisig-icon.svg new file mode 100644 index 000000000..9712e47b3 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/multisig-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/settings-icon.svg b/frontend/public/sidebar-menu-icons/settings-icon.svg new file mode 100644 index 000000000..b382c6436 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/settings-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/smart-contracts-icon.svg b/frontend/public/sidebar-menu-icons/smart-contracts-icon.svg new file mode 100644 index 000000000..4e64e2ac8 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/smart-contracts-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/staking-icon.svg b/frontend/public/sidebar-menu-icons/staking-icon.svg new file mode 100644 index 000000000..1a496e932 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/staking-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/timer-icon.svg b/frontend/public/sidebar-menu-icons/timer-icon.svg new file mode 100644 index 000000000..5cecb3711 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/timer-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/transfers-icon.svg b/frontend/public/sidebar-menu-icons/transfers-icon.svg new file mode 100644 index 000000000..0fd1bb421 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/transfers-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/public/sidebar-menu-icons/txn-builder-icon.svg b/frontend/public/sidebar-menu-icons/txn-builder-icon.svg new file mode 100644 index 000000000..fd1f97f69 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/txn-builder-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sidebar-menu-icons/txn-history-icon.svg b/frontend/public/sidebar-menu-icons/txn-history-icon.svg new file mode 100644 index 000000000..3884b0266 --- /dev/null +++ b/frontend/public/sidebar-menu-icons/txn-history-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/sign-icon.svg b/frontend/public/sign-icon.svg deleted file mode 100644 index e820689f5..000000000 --- a/frontend/public/sign-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/space-ship.png b/frontend/public/space-ship.png deleted file mode 100644 index 707e1d9db..000000000 Binary files a/frontend/public/space-ship.png and /dev/null differ diff --git a/frontend/public/stake-fish-icon.png b/frontend/public/stake-fish-icon.png deleted file mode 100644 index ef29f84f7..000000000 Binary files a/frontend/public/stake-fish-icon.png and /dev/null differ diff --git a/frontend/public/stake-icon.svg b/frontend/public/stake-icon.svg deleted file mode 100644 index 9995e6881..000000000 --- a/frontend/public/stake-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/staked-bal.png b/frontend/public/staked-bal.png new file mode 100644 index 000000000..a19c658f7 Binary files /dev/null and b/frontend/public/staked-bal.png differ diff --git a/frontend/public/stakesAmount.svg b/frontend/public/stakesAmount.svg deleted file mode 100644 index e3fe71a46..000000000 --- a/frontend/public/stakesAmount.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/staking-ad-1.png b/frontend/public/staking-ad-1.png deleted file mode 100644 index b35d0845f..000000000 Binary files a/frontend/public/staking-ad-1.png and /dev/null differ diff --git a/frontend/public/staking-ad-2.png b/frontend/public/staking-ad-2.png deleted file mode 100644 index b6427c804..000000000 Binary files a/frontend/public/staking-ad-2.png and /dev/null differ diff --git a/frontend/public/success-icon.svg b/frontend/public/success-icon.svg new file mode 100644 index 000000000..cacc525d0 --- /dev/null +++ b/frontend/public/success-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/swap.svg b/frontend/public/swap.svg deleted file mode 100644 index b80e17e53..000000000 --- a/frontend/public/swap.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/public/switch-icon.svg b/frontend/public/switch-icon.svg index fa7fdb098..fe8e67d59 100644 --- a/frontend/public/switch-icon.svg +++ b/frontend/public/switch-icon.svg @@ -1,12 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/frontend/public/telegram-icon.png b/frontend/public/telegram-icon.png new file mode 100644 index 000000000..d078cb118 Binary files /dev/null and b/frontend/public/telegram-icon.png differ diff --git a/frontend/public/threshold-icon.svg b/frontend/public/threshold-icon.svg deleted file mode 100644 index e69be07bf..000000000 --- a/frontend/public/threshold-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/timer-icon.svg b/frontend/public/timer-icon.svg deleted file mode 100644 index 25714ed51..000000000 --- a/frontend/public/timer-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/timer.svg b/frontend/public/timer.svg new file mode 100644 index 000000000..34a66cd4d --- /dev/null +++ b/frontend/public/timer.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/tokens-icon.svg b/frontend/public/tokens-icon.svg deleted file mode 100644 index 8c43d2b59..000000000 --- a/frontend/public/tokens-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/total-bal.png b/frontend/public/total-bal.png new file mode 100644 index 000000000..8a3af2bb4 Binary files /dev/null and b/frontend/public/total-bal.png differ diff --git a/frontend/public/twitter-icon.png b/frontend/public/twitter-icon.png index 22b2fddbb..b795454ab 100644 Binary files a/frontend/public/twitter-icon.png and b/frontend/public/twitter-icon.png differ diff --git a/frontend/public/unbonding.png b/frontend/public/unbonding.png new file mode 100644 index 000000000..3aee92e87 Binary files /dev/null and b/frontend/public/unbonding.png differ diff --git a/frontend/public/up.svg b/frontend/public/up.svg new file mode 100644 index 000000000..83093f6f6 --- /dev/null +++ b/frontend/public/up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/upload-icon.svg b/frontend/public/upload-icon.svg new file mode 100644 index 000000000..a91bdbb03 --- /dev/null +++ b/frontend/public/upload-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/verify-illustration.png b/frontend/public/verify-illustration.png index 0dcda6921..7bcdcedea 100644 Binary files a/frontend/public/verify-illustration.png and b/frontend/public/verify-illustration.png differ diff --git a/frontend/public/vertical-divider.svg b/frontend/public/vertical-divider.svg deleted file mode 100644 index aa0a2a5c5..000000000 --- a/frontend/public/vertical-divider.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/frontend/public/view-more-icon.svg b/frontend/public/view-more-icon.svg deleted file mode 100644 index 30f356d2b..000000000 --- a/frontend/public/view-more-icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/vitwit-logo.svg b/frontend/public/vitwit-logo.svg new file mode 100644 index 000000000..d66f81333 --- /dev/null +++ b/frontend/public/vitwit-logo.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/vote-icon.svg b/frontend/public/vote-icon.svg deleted file mode 100644 index 686af0321..000000000 --- a/frontend/public/vote-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontend/public/vote-image.png b/frontend/public/vote-image.png deleted file mode 100644 index 45c967fec..000000000 Binary files a/frontend/public/vote-image.png and /dev/null differ diff --git a/frontend/public/warning.svg b/frontend/public/warning.svg deleted file mode 100644 index aabc321ee..000000000 --- a/frontend/public/warning.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/public/witval-logo.png b/frontend/public/witval-logo.png deleted file mode 100644 index 4b783ec3f..000000000 Binary files a/frontend/public/witval-logo.png and /dev/null differ diff --git a/frontend/public/youtube-logo.png b/frontend/public/youtube-logo.png new file mode 100644 index 000000000..ba376c2fe Binary files /dev/null and b/frontend/public/youtube-logo.png differ diff --git a/frontend/src/app/(routes)/(overview)/error.tsx b/frontend/src/app/(routes)/(overview)/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/(overview)/loader/Loading.tsx b/frontend/src/app/(routes)/(overview)/loader/Loading.tsx new file mode 100644 index 000000000..374addbd0 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/loader/Loading.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import DashboardLoading from '../overview-components/DashboardLoading'; +import TokenAllocationSkeleton from '../overview-components/TokenAllocationSkeleton'; +import GovSkeleton from '../overview-components/GovSkeleton'; +import PortfolioSkeleton from '../overview-components/PortfolioSkeleton'; + +const Loading = () => { + return ( +
+
+
+
+ +
+
+
Asset Information
+
+
+ Your total assets information +
+
+
+
+ +
+
+
+
+
+
+
Token Allocation
+
+
Token Allocation
+
+
+
+ +
+
+
+
Governance
+
+
Acitve proposals
+
+
+
+ +
+
+
+
+ ); +}; + +export default Loading; diff --git a/frontend/src/app/(routes)/(overview)/loading.tsx b/frontend/src/app/(routes)/(overview)/loading.tsx new file mode 100644 index 000000000..b51f4e928 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import React from 'react'; +import Loading from './loader/Loading'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/AccountSummary.tsx b/frontend/src/app/(routes)/(overview)/overview-components/AccountSummary.tsx index 8391e4805..276131b49 100644 --- a/frontend/src/app/(routes)/(overview)/overview-components/AccountSummary.tsx +++ b/frontend/src/app/(routes)/(overview)/overview-components/AccountSummary.tsx @@ -29,7 +29,7 @@ const AccountSummery = ({ chainID }: { chainID: string }) => { value: ( ), }, @@ -68,7 +68,13 @@ const AccountSummaryCard = (props: AssetSummary) => {
- {alt} + {alt}
diff --git a/frontend/src/app/(routes)/(overview)/overview-components/Asset.tsx b/frontend/src/app/(routes)/(overview)/overview-components/Asset.tsx index daaf7d01a..3551f0226 100644 --- a/frontend/src/app/(routes)/(overview)/overview-components/Asset.tsx +++ b/frontend/src/app/(routes)/(overview)/overview-components/Asset.tsx @@ -1,6 +1,6 @@ -import { formatAmount, formatCoin, formatDollarAmount } from '@/utils/util'; +import { formatAmount } from '@/utils/util'; import Link from 'next/link'; -import React from 'react'; +import React, { RefObject, useEffect, useRef, useState } from 'react'; import Image from 'next/image'; import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; import { txWithdrawAllRewards } from '@/store/features/distribution/distributionSlice'; @@ -11,13 +11,17 @@ import { RootState } from '@/store/store'; import { CircularProgress, Tooltip } from '@mui/material'; import { setError } from '@/store/features/common/commonSlice'; import { capitalize } from 'lodash'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { DelegationsPairs } from '@/types/distribution'; +import useAuthzStakingExecHelper from '@/custom-hooks/useAuthzStakingExecHelper'; +import NumberFormat from '@/components/common/NumberFormat'; const Asset = ({ asset, - showChainName, + // showChainName, }: { asset: ParsedAsset; - showChainName: boolean; + // showChainName: boolean; }) => { const txClaimStatus = useAppSelector( (state: RootState) => @@ -27,11 +31,38 @@ const Asset = ({ (state: RootState) => state.staking.chains[asset.chainID]?.reStakeTxStatus || TxStatus.IDLE ); + const authzRewards = useAppSelector( + (state) => state.distribution.authzChains + ); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); + const { getChainInfo } = useGetChainInfo(); const dispatch = useAppDispatch(); - const { txWithdrawAllRewardsInputs, txRestakeInputs } = useGetTxInputs(); + const { txWithdrawAllRewardsInputs, txRestakeInputs, txAuthzRestakeMsgs } = + useGetTxInputs(); + const { txAuthzClaim, txAuthzRestake } = useAuthzStakingExecHelper(); const claim = (chainID: string) => { + if (isAuthzMode) { + const { address } = getChainInfo(chainID); + const pairs: DelegationsPairs[] = ( + authzRewards[chainID]?.delegatorRewards?.list || [] + ).map((reward) => { + const pair = { + delegator: authzAddress, + validator: reward.validator_address, + }; + return pair; + }); + txAuthzClaim({ + grantee: address, + granter: authzAddress, + pairs: pairs, + chainID: chainID, + }); + return; + } if (txClaimStatus === TxStatus.PENDING) { dispatch( setError({ @@ -58,6 +89,17 @@ const Asset = ({ }; const claimAndStake = (chainID: string) => { + if (isAuthzMode) { + const { address } = getChainInfo(chainID); + const msgs = txAuthzRestakeMsgs(chainID); + txAuthzRestake({ + grantee: address, + granter: authzAddress, + msgs: msgs, + chainID: chainID, + }); + return; + } if (txRestakeStatus === TxStatus.PENDING) { dispatch( setError({ @@ -82,131 +124,238 @@ const Asset = ({ ); }; + // actions for claim and claim and stake + + const [showPopup, setShowPopup] = useState(false); + const popupRef = useRef(null); + const buttonRef: RefObject = useRef(null); + + const togglePopup = () => { + setShowPopup(!showPopup); + }; + + const handleClickOutside = (event: MouseEvent) => { + if ( + popupRef.current && + !popupRef.current.contains(event.target as Node) && + buttonRef.current && + !buttonRef.current.contains(event.target as Node) + ) { + setShowPopup(false); + } + }; + + useEffect(() => { + document.addEventListener('click', handleClickOutside); + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, []); + return ( - - -
-
- {formatCoin(asset.balance, asset.displayDenom)} + + +
+
+
- {showChainName ? ( -
+
+ chain-Logo +

on{' '} - + {asset.chainName} -

- ) : null} -
- - -
- {asset.type === 'native' - ? formatCoin(asset.staked, asset.displayDenom) - : '-'} +

+
- - -
- {asset.type === 'native' - ? formatCoin(asset.rewards, asset.displayDenom) - : '-'} + + +
+
+ {asset.type === 'native' ? ( + + + + ) : ( + '-' + )} +
+ +
- - -
-
- {formatDollarAmount(asset.usdPrice)} + + +
+
+ {asset.type === 'native' ? ( + + + + ) : ( + '-' + )}
-
- = 0 ? 'up' : 'down' - }-arrow-filled-icon.svg`} - height={16} - width={16} - alt="inflation change" +
+
+ + +
+
+ +
+
= 0 ? 'text-[#238636]' : 'text-[#E57575]') + 'text-sm font-normal leading-[21px] ' + + (asset.inflation >= 0 ? 'text-[#238636]' : 'text-[#F1575780]') } > - {formatAmount(Math.abs(asset.inflation))}% +

+ {formatAmount(Math.abs(asset.inflation))}% +

+ = 0 ? 'up' : 'down' + }-arrow-filled-icon.svg`} + width={18} + height={5} + alt="down-arrow-filled-icon" + />
- - -
- {formatDollarAmount(asset.usdValue)} + + {/* +
+
+ {formatDollarAmount(asset.usdValue).split('.')[0]}. + + {formatDollarAmount(asset.usdValue).split('.')[1]} + +
+
- - -
- + */} + + + + {/* +
-
{ - if (asset.type === 'native') claimAndStake(asset.chainID); - }} - > - {txRestakeStatus === TxStatus.PENDING && asset.type !== 'ibc' ? ( - - ) : ( - Claim and Stake - )} + title={asset.type === 'ibc' ? 'IBC Deposit feature is coming soon..' : 'Claim'} + @@ -243,7 +370,7 @@ const Asset = ({
- + */} ); }; - export default Asset; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/AssetsTable.tsx b/frontend/src/app/(routes)/(overview)/overview-components/AssetsTable.tsx index b352756b4..a97297170 100644 --- a/frontend/src/app/(routes)/(overview)/overview-components/AssetsTable.tsx +++ b/frontend/src/app/(routes)/(overview)/overview-components/AssetsTable.tsx @@ -2,85 +2,82 @@ import { useAppSelector } from '@/custom-hooks/StateHooks'; import useSortedAssets from '@/custom-hooks/useSortedAssets'; import React from 'react'; import Asset from './Asset'; -import { CircularProgress } from '@mui/material'; import NoAssets from '@/components/illustrations/NoAssets'; +import DashboardLoading from './DashboardLoading'; const AssetsTable = ({ chainIDs }: { chainIDs: string[] }) => { - const [sortedAssets] = useSortedAssets(chainIDs, { + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const [sortedAssets, authzSortedAssets] = useSortedAssets(chainIDs, { showAvailable: true, showRewards: true, showStaked: true, }); + + const assets = isAuthzMode ? authzSortedAssets : sortedAssets; + const balancesLoading = useAppSelector( (state) => state.bank.balancesLoading > 0 ); const delegationsLoading = useAppSelector( (state) => state.staking.delegationsLoading > 0 ); + const authzBalanceLoading = useAppSelector( + (state) => state.bank.authz.balancesLoading > 0 + ); + const authzDelegationsLoading = useAppSelector( + (state) => state.staking.authz.delegationsLoading > 0 + ); + + const loading = !isAuthzMode && (balancesLoading || delegationsLoading); + const authzLoading = + isAuthzMode && (authzBalanceLoading || authzDelegationsLoading); return ( -
-
-
- {sortedAssets.length ? ( -
- - - - - - - - - + + + {assets.map((asset) => ( + 1} + /> + ))} + +
-
- Available -
-
-
- Staked -
-
-
- Rewards -
-
-
- Price -
-
-
- Value -
-
-
- Actions +
+
+
Asset Information
+
+
+ Information of your asset holdings +
+
+
+
+ + {/* table */} + + {assets.length ? ( +
+ + + + {['Available', 'Staked', 'Rewards', 'Value', ''].map( + (header, hIndex) => ( + - - - - {sortedAssets.map((asset) => ( - 1} - /> - ))} - -
+
+ {header}
-
- ) : ( -
- {balancesLoading || delegationsLoading ? ( - - ) : ( - - )} -
- )} + ) + )} +
-
+ ) : ( +
+ {loading || authzLoading ? : } +
+ )}
); }; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/BalanceSummary.tsx b/frontend/src/app/(routes)/(overview)/overview-components/BalanceSummary.tsx new file mode 100644 index 000000000..c59a522e1 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview-components/BalanceSummary.tsx @@ -0,0 +1,76 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetAssetsAmount from '@/custom-hooks/useGetAssetsAmount'; +import React from 'react'; +import useGetAuthzAssetsAmount from '../../../../custom-hooks/useGetAuthzAssetsAmount'; +import NumberFormat from '@/components/common/NumberFormat'; +type AssetSummary = { icon: string; alt: string; type: string; amount: string }; + +export default function BalanceSummary({ chainIDs }: { chainIDs: string[] }) { + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const [myStaked, myAvailable, myRewards] = useGetAssetsAmount(chainIDs); + + const [authzStaked, authzAvailable, authzRewards] = + useGetAuthzAssetsAmount(chainIDs); + + const stakedAmount = isAuthzMode ? authzStaked : myStaked; + const availableAmount = isAuthzMode ? authzAvailable : myAvailable; + const rewardsAmount = isAuthzMode ? authzRewards : myRewards; + + const available = availableAmount?.toString(); + const staked = stakedAmount?.toString(); + const rewards = rewardsAmount?.toString(); + + const total = Number(available) + Number(staked) + Number(rewards); + const totalAmt = total?.toString(); + + const assetsSummaryData: AssetSummary[] = [ + { + icon: '/total-bal.png', + alt: 'total-balance', + type: 'Total Balance', + amount: totalAmt, + }, + { + icon: '/staked-bal.png', + alt: 'stake', + type: 'Staked Amt', + amount: staked, + }, + { + icon: '/rewards.png', + alt: 'rewards', + type: 'Rewards', + amount: rewards, + }, + { + icon: '/avail-bal.png', + alt: 'available', + type: 'Balance', + amount: available, + }, + ]; + + return ( +
+ {/*
+
Portfolio
+
+
Summary of your digital assets
+
+
+
*/} +
+ {assetsSummaryData.map((data, index) => ( +
+
+
{data.type}
+
+ +
+
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/app/(routes)/(overview)/overview-components/DashboardLoading.tsx b/frontend/src/app/(routes)/(overview)/overview-components/DashboardLoading.tsx new file mode 100644 index 000000000..e6f723376 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview-components/DashboardLoading.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +const DashboardLoading = () => { + return ( +
+
+
+ {/*
+
+
+
Hello
+
+
+
+ Summary of your assets across all chains +
+
+
+ +
+
+ {[1, 2, 3, 4].map((_, index) => ( +
+ ))} +
+
+
*/} + + {/* Skeleton*/} +
+ + + + {['Available', 'Staked', 'Rewards', 'Total', 'Actions'].map( + (header, hIndex) => ( + + ) + )} + + + + {Array(3) + .fill(null) + .map((_, colIndex) => ( + + {Array(6) + .fill(null) + .map((_, colIndex) => ( + + ))} + + ))} + +
+
+ {header} +
+
+
+
+
+
+
+
+ ); +}; + +export default DashboardLoading; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/GovSkeleton.tsx b/frontend/src/app/(routes)/(overview)/overview-components/GovSkeleton.tsx new file mode 100644 index 000000000..ae5c67d80 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview-components/GovSkeleton.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +function GovSkeleton() { + return ( +
+ {/*
+
Governance
+
+ Connect your wallet now to access all the modules on{' '} +
+
+
*/} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + +export default GovSkeleton; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/GovernanceView.tsx b/frontend/src/app/(routes)/(overview)/overview-components/GovernanceView.tsx new file mode 100644 index 000000000..c4b6ca109 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview-components/GovernanceView.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import Image from 'next/image'; +import { REDIRECT_ICON } from '@/constants/image-names'; +import useInitGovernance from '@/custom-hooks/governance/useInitGovernance'; +import useGetProposals from '@/custom-hooks/governance/useGetProposals'; +import { get } from 'lodash'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import GovSkeleton from './GovSkeleton'; +import Link from 'next/link'; +// import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const ProposalCard: React.FC<{ proposal: any }> = ({ proposal }) => { + // const { getChainInfo } = useGetChainInfo(); + // const chainID = get(proposal, 'chainID').toLowerCase(); + // const { chainLogo } = getChainInfo(chainID); + + return ( + +
+
+
+ + {get(proposal, 'proposalInfo.proposalId', 0)} + +
+
+
+
+ {get(proposal, 'proposalInfo.proposalTitle', '-')} +
+ +
+
+
+ +

+ {get(proposal, 'chainName', '-')} +

+
+
+ timer-icon +

+ Voting ends in {get(proposal, 'proposalInfo.endTime', 0)} +

+
+
+
+
+
+ + ); +}; + +const GovernanceView = ({ chainIDs }: { chainIDs: string[] }) => { + useInitGovernance({ chainIDs }); + const { getProposals } = useGetProposals(); + const proposalsData = getProposals({ chainIDs, showAll: false }); + + const proposalsLoading = + useAppSelector((state) => state.gov?.activeProposalsLoading) > + chainIDs?.length; + + return ( +
+
+
Governance
+
+
+ Active proposals on cosmos ecosystem +
+
+
+
+ + {proposalsData.map((proposal) => ( + + ))} + + {proposalsLoading ? : null} + + {!proposalsLoading && !proposalsData.length + ? 'No Active Proposals found.' + : null} +
+ ); +}; + +export default GovernanceView; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/History.tsx b/frontend/src/app/(routes)/(overview)/overview-components/History.tsx deleted file mode 100644 index d35a2e6a1..000000000 --- a/frontend/src/app/(routes)/(overview)/overview-components/History.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react'; -import SideAd from './SideAd'; -import useGetAssetsAmount from '@/custom-hooks/useGetAssetsAmount'; -import { formatDollarAmount } from '@/utils/util'; -import TopNav from '@/components/TopNav'; -import TransactionItem from './TransactionItem'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { useRouter } from 'next/navigation'; -import NoTransactions from '@/components/illustrations/NoTransactions'; - -const History = ({ chainIDs }: { chainIDs: string[] }) => { - return ( -
- - - - - -
-

- Recent Transactions -

-
- -
- ); -}; - -export default History; - -const Balance = ({ chainIDs }: { chainIDs: string[] }) => { - const router = useRouter(); - const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); - const getPath = (chainIDs: string[], module: string) => { - if (chainIDs.length !== 1) { - return '/' + module; - } - let curChainName: string = ''; - Object.keys(nameToChainIDs).forEach((chainName) => { - if (nameToChainIDs[chainName] === chainIDs[0]) curChainName = chainName; - }); - return '/' + module + '/' + curChainName; - }; - const [staked, available, rewards] = useGetAssetsAmount(chainIDs); - return ( -
-
-
- Total Balance -
- - {formatDollarAmount(staked + available + rewards)} - -
-
- - -
-
- ); -}; - -export const RecentTransactions = ({ - chainIDs, - msgFilters, -}: { - chainIDs: string[]; - msgFilters: string[]; -}) => { - /** - * Note: Currently, this implementation of recent transactions addresses scenarios involving either a single chain or all chains. - * If the system evolves to support multiple selected chains in the future, - * modifications to this logic will be necessary. - */ - const transactions = useAppSelector( - (state: RootState) => - (chainIDs.length == 1 - ? state.transactionHistory.chains[chainIDs[0]] - : state.transactionHistory.allTransactions) || [] - ); - return ( -
- {transactions.length ? ( -
- {transactions.map((tx) => ( - - ))} -
- ) : ( -
- -
- )} -
- ); -}; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/OverviewDashboard.tsx b/frontend/src/app/(routes)/(overview)/overview-components/OverviewDashboard.tsx new file mode 100644 index 000000000..0ad75b133 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview-components/OverviewDashboard.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import AssetsTable from './AssetsTable'; +import TokenAllocation from './TokenAllocation'; +import BalanceSummary from './BalanceSummary'; +import GovernanceView from './GovernanceView'; +import PageHeader from '@/components/common/PageHeader'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; + +const OverviewDashboard = ({ chainIDs }: { chainIDs: string[] }) => { + const showAuthzAlert = useGetShowAuthzAlert(); + return ( +
+
+
+
+
+ +
+ + +
+
+
+ + +
+
+
+ ); +}; + +export default OverviewDashboard; + +const OverviewHeader = () => { + return ( + + ); +}; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/OverviewPage.tsx b/frontend/src/app/(routes)/(overview)/overview-components/OverviewPage.tsx deleted file mode 100644 index 98ba5e9ac..000000000 --- a/frontend/src/app/(routes)/(overview)/overview-components/OverviewPage.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; - -import React, { useEffect } from 'react'; -import { RootState } from '../../../../store/store'; -import { getBalances } from '@/store/features/bank/bankSlice'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { - getDelegations, - getAllValidators, -} from '@/store/features/staking/stakeSlice'; -import WalletSummery from './WalletSummery'; -import TopNav from './TopNav'; -import History from './History'; -import PageAd from './PageAd'; -import AssetsTable from './AssetsTable'; -import AccountSummery from './AccountSummary'; -import { getAccountInfo } from '@/store/features/auth/authSlice'; -import { getDelegatorTotalRewards } from '@/store/features/distribution/distributionSlice'; - -const OverviewPage = ({ chainIDs }: { chainIDs: string[] }) => { - const dispatch = useAppDispatch(); - const networks = useAppSelector((state: RootState) => state.wallet.networks); - - useEffect(() => { - chainIDs.forEach((chainID) => { - const allChainInfo = networks[chainID]; - const chainInfo = allChainInfo.network; - const address = allChainInfo?.walletInfo?.bech32Address; - const minimalDenom = - allChainInfo.network.config.stakeCurrency.coinMinimalDenom; - const basicChainInputs = { - baseURL: chainInfo.config.rest, - address, - chainID, - }; - - dispatch(getBalances(basicChainInputs)); - dispatch(getDelegations(basicChainInputs)); - dispatch( - getAllValidators({ - baseURL: chainInfo.config.rest, - chainID: chainID, - }) - ); - dispatch(getAccountInfo(basicChainInputs)); - dispatch( - getDelegatorTotalRewards({ - baseURL: chainInfo.config.rest, - address: address, - chainID: chainID, - denom: minimalDenom, - }) - ); - }); - }, []); - - return ( -
-
- - - {chainIDs.length === 1 && } - -
-

- Asset Information -

-
- -
- -
- ); -}; - -export default OverviewPage; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/PageAd.tsx b/frontend/src/app/(routes)/(overview)/overview-components/PageAd.tsx deleted file mode 100644 index 380128927..000000000 --- a/frontend/src/app/(routes)/(overview)/overview-components/PageAd.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import Image from 'next/image'; -import React, { useState } from 'react'; - -const PageAd = () => { - const [adOpen, setAdOpen] = useState(false); - - return adOpen ? ( -
-
- setAdOpen(false)} - src="/close.svg" - width={24} - height={24} - alt="Close ad" - /> -
- Ad -
- ) : ( -
- ); -}; - -export default PageAd; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/PortfolioSkeleton.tsx b/frontend/src/app/(routes)/(overview)/overview-components/PortfolioSkeleton.tsx new file mode 100644 index 000000000..320e16ca3 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview-components/PortfolioSkeleton.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const PortfolioSkeleton = () => { + return ( +
+
+
Portfolio
+
+
Summary of assets information
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +export default PortfolioSkeleton; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/Profile.tsx b/frontend/src/app/(routes)/(overview)/overview-components/Profile.tsx deleted file mode 100644 index 526b66a3d..000000000 --- a/frontend/src/app/(routes)/(overview)/overview-components/Profile.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import Image from 'next/image'; -import React from 'react'; -import { Tooltip } from '@mui/material'; -import { LOGOUT_ICON } from '@/utils/constants'; -import { resetWallet } from '@/store/features/wallet/walletSlice'; -import { logout } from '@/utils/localStorage'; -import { - resetError, - resetTxAndHash, -} from '@/store/features/common/commonSlice'; -const Profile = () => { - const profileName = useAppSelector((state) => state.wallet.name); - const dispatch = useAppDispatch(); - - return ( -
- -
- profile -

- {profileName} -

-
-
- - { - dispatch(resetWallet()); - dispatch(resetError()); - dispatch(resetTxAndHash()); - logout(); - }} - className="cursor-pointer" - src={LOGOUT_ICON} - width={36} - height={36} - alt="Logout" - /> - -
- ); -}; - -export default Profile; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/SideAd.tsx b/frontend/src/app/(routes)/(overview)/overview-components/SideAd.tsx deleted file mode 100644 index b859122bd..000000000 --- a/frontend/src/app/(routes)/(overview)/overview-components/SideAd.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Image from 'next/image'; -import React, { useState } from 'react'; - -const SideAd = () => { - const [adOpen, setAdOpen] = useState(false); - - return adOpen ? ( -
-
-
- setAdOpen(false)} - src="/close.svg" - width={24} - height={24} - alt="Close ad" - /> -
- Ad -
-
- ) : ( -
- ); -}; -export default SideAd; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/TokenAllocation.tsx b/frontend/src/app/(routes)/(overview)/overview-components/TokenAllocation.tsx new file mode 100644 index 000000000..b3eb6369c --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview-components/TokenAllocation.tsx @@ -0,0 +1,190 @@ +import React from 'react'; +import Image from 'next/image'; +import useGetAssetsAmount from '@/custom-hooks/useGetAssetsAmount'; +import { get } from 'lodash'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import TokenAllocationSkeleton from './TokenAllocationSkeleton'; +import { RootState } from '@/store/store'; +// import { useParams } from 'next/navigation'; +import { Tooltip } from '@mui/material'; + +const truncateChainName = (name: string, maxLength: number) => { + return name.length > maxLength ? `${name.substring(0, maxLength)}...` : name; +}; + +const TokenAllocation = () => { + // const params = useParams(); + // const currentChainName = params?.chainNames?.[0]; + + const nameToChainIDs = useAppSelector( + (state: RootState) => state.wallet.nameToChainIDs + ); + + const chainIDs = Object.keys(nameToChainIDs).map( + (chainName) => nameToChainIDs[chainName] + ); + + const [, , , , totalAmountByChain] = useGetAssetsAmount(chainIDs); + + const balancesLoading = useAppSelector( + (state) => state.bank.balancesLoading > 0 + ); + const delegationsLoading = useAppSelector( + (state) => state.staking.delegationsLoading > 0 + ); + + const loading = balancesLoading || delegationsLoading; + + const totalAmtObj = totalAmountByChain(); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const sumOfTotals: any = Object.values(totalAmtObj).reduce( + (acc, curr: any) => acc + curr.total, + 0 + ); + + for (const key in totalAmtObj) { + if (totalAmtObj.hasOwnProperty(key)) { + // Calculate the percentage + totalAmtObj[key].percentage = + (totalAmtObj[key].total * 100) / sumOfTotals; + } + } + + const entries = Object.entries(totalAmtObj); + + // Sort the array based on the percentage in descending order + /* eslint-disable @typescript-eslint/no-explicit-any */ + entries.sort((a: any, b: any) => b[1].percentage - a[1].percentage); + + const firstEntries = entries.slice(0, 5); + + // Calculate the "Others" total and percentage + const others = entries.slice(5); + const othersPercentage = others.reduce((acc, [, value]) => { + if ( + value && + typeof value === 'object' && + 'percentage' in value && + typeof value.percentage === 'number' + ) { + return acc + value.percentage; + } + return acc; + }, 0); + + // Convert the sorted array back into an object + const sortedObj = Object.fromEntries(firstEntries); + + return ( +
+
+
Token Distribution
+
+
+ Distribution of tokens across various networks +
+
+
+
+ + {loading ? ( + + ) : ( +
+ {Object.entries(sortedObj).map(([key, value], index) => ( +
+
+ {Number(get(value, 'percentage', 0).toFixed(2)) > 0 ? ( +
+ {Math.round(get(value, 'percentage', 0))}% +
+ ) : ( +
+ 0% +
+ )} + + +
+
+ + {`Radio + +
+ +
+ {truncateChainName(get(value, 'chainName', key), 5)} +
+
+
+ ))} + +
+
+ {Number(othersPercentage.toFixed(2)) > 0 ? ( +
+ {Math.round(othersPercentage)}% +
+ ) : ( +
+ 0% +
+ )} + +
+
+ + {`Radio + +
+ +
+ Others +
+
+
+
+ )} +
+ ); +}; + +export default TokenAllocation; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/TokenAllocationSkeleton.tsx b/frontend/src/app/(routes)/(overview)/overview-components/TokenAllocationSkeleton.tsx new file mode 100644 index 000000000..fa658767b --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview-components/TokenAllocationSkeleton.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +function TokenAllocationSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+ ); +} + +export default TokenAllocationSkeleton; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/TopNav.tsx b/frontend/src/app/(routes)/(overview)/overview-components/TopNav.tsx deleted file mode 100644 index 24b4c1328..000000000 --- a/frontend/src/app/(routes)/(overview)/overview-components/TopNav.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -const TopNav = () => { - return ( -
-

Overview

-
- ); -}; - -export default TopNav; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/TransactionItem.tsx b/frontend/src/app/(routes)/(overview)/overview-components/TransactionItem.tsx deleted file mode 100644 index 8045d2966..000000000 --- a/frontend/src/app/(routes)/(overview)/overview-components/TransactionItem.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { formatTransaction } from '@/utils/transaction'; -import Image from 'next/image'; - -const TransactionItem = ({ - transaction, - msgFilters, -}: { - transaction: Transaction; - msgFilters: string[]; -}) => { - const uiTx = formatTransaction(transaction, msgFilters); - - return uiTx.showTx ? ( -
-
-
-
- {uiTx.time} -
-
- transaction -
-
- transaction -
-
-
-
- {uiTx.firstMessage} -
-
-
- -
-
- {uiTx.showMsgs[0] && } - {uiTx.showMsgs[1] && } - {uiTx.showMsgs[2] && } -
-
-
- ) : null; -}; - -export const Chip = ({ msg }: { msg: string }) => { - return ( -
- {msg} -
- ); -}; - -export const FilledChip = ({ count }: { count: number }) => { - return
+ {count} more
; -}; - -export const TransactionStatus = ({ uiTx }: { uiTx: UiTx }) => { - const txStatus = uiTx.isTxSuccess - ? 'Transaction Successful' - : 'Transaction Failed'; - return uiTx.isIBCPending ? ( -
- -
-
- ) : ( - - ); -}; - -export const StatusContent = ({ content }: { content: string }) => { - return ( -
- {content} -
- ); -}; - -export default TransactionItem; diff --git a/frontend/src/app/(routes)/(overview)/overview-components/WalletSummery.tsx b/frontend/src/app/(routes)/(overview)/overview-components/WalletSummery.tsx deleted file mode 100644 index 0bdb939b9..000000000 --- a/frontend/src/app/(routes)/(overview)/overview-components/WalletSummery.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import useGetAssetsAmount from '@/custom-hooks/useGetAssetsAmount'; -import { formatDollarAmount } from '@/utils/util'; -import Image from 'next/image'; -import React from 'react'; -type AssetSummary = { icon: string; alt: string; type: string; amount: string }; - -const WalletSummery = ({ chainIDs }: { chainIDs: string[] }) => { - const [stakedAmount, availableAmount, rewardsAmount] = - useGetAssetsAmount(chainIDs); - const available = formatDollarAmount(availableAmount); - const staked = formatDollarAmount(stakedAmount); - const rewards = formatDollarAmount(rewardsAmount); - const assetsSummaryData: AssetSummary[] = [ - { - icon: '/stakesAmount.svg', - alt: 'stake', - type: 'Staked Amount', - amount: staked, - }, - { - icon: '/rewardsAmount.svg', - alt: 'rewards', - type: 'Rewards', - amount: rewards, - }, - { - icon: '/balanceAmount.svg', - alt: 'available', - type: 'Available', - amount: available, - }, - ]; - - return ( -
- {assetsSummaryData.map((assetTypeData) => ( - - ))} -
- ); -}; - -const WalletSummaryCard = (props: AssetSummary) => { - const { type, icon, amount, alt } = props; - return ( -
-
-
- {alt} -
-
-
- {type} -
-
-
-
- {amount} -
-
- ); -}; - -export default WalletSummery; diff --git a/frontend/src/app/(routes)/(overview)/overview.css b/frontend/src/app/(routes)/(overview)/overview.css index ffb209c09..709cab25c 100644 --- a/frontend/src/app/(routes)/(overview)/overview.css +++ b/frontend/src/app/(routes)/(overview)/overview.css @@ -1,56 +1,95 @@ -.summary-card { - @apply px-6 py-4 w-full space-y-2 rounded-2xl backdrop-blur-[2px] bg-[#0e0b26]; - backdrop-filter: blur(2px); +/* .summary-card { + @apply px-6 py-4 w-full space-y-2 rounded-2xl backdrop-blur-[2px] bg-[#0e0b26]; + backdrop-filter: blur(2px); } .summary-cards-container { - @apply flex gap-6 w-full + @apply flex gap-6 w-full; } .assets-table { - @apply rounded-3xl text-white bg-[#0e0b26] flex flex-col flex-1; - backdrop-filter: blur(2px); + @apply rounded-3xl text-white bg-[#0e0b26] flex flex-col flex-1; + backdrop-filter: blur(2px); } .assets-table th { - @apply pb-4; + @apply pb-4; } .assets-table td { - @apply font-light leading-normal pt-4 pb-4 px-2; + @apply font-light leading-normal pt-4 pb-4 px-2; } .assets-table tbody tr { - @apply hover:bg-[#1a1731]; + @apply hover:bg-[#1a1731]; } .border-1 { - @apply absolute bottom-0 h-[0.5px]; - background: linear-gradient(90deg, - rgba(74, 162, 156, 0.5) 7.46%, - rgba(139, 61, 167, 0.5) 94.28%); + @apply absolute bottom-0 h-[0.5px]; + background: linear-gradient( + 90deg, + rgba(74, 162, 156, 0.5) 7.46%, + rgba(139, 61, 167, 0.5) 94.28% + ); } .asset-action { - @apply w-8 h-8 rounded-lg flex justify-center items-center; + @apply w-8 h-8 rounded-lg flex justify-center items-center; } .active { - @apply cursor-pointer; - background: linear-gradient(180deg, - rgba(74, 162, 156, 0.8) 0%, - rgba(139, 61, 167, 0.8) 100%), - lightgray 50% / cover no-repeat; + @apply cursor-pointer; + background: + linear-gradient( + 180deg, + rgba(74, 162, 156, 0.8) 0%, + rgba(139, 61, 167, 0.8) 100% + ), + lightgray 50% / cover no-repeat; } .disabled { - background: #ffffff1a; - opacity: 1; - cursor: not-allowed; + background: #ffffff1a; + opacity: 1; + cursor: not-allowed; } .h-line { - height: 1px; - background: linear-gradient(90deg, rgba(74, 162, 156, 0.50) 7.46%, rgba(139, 61, 167, 0.50) 94.28%); - opacity: 50%; -} \ No newline at end of file + height: 1px; + background: linear-gradient( + 90deg, + rgba(74, 162, 156, 0.5) 7.46%, + rgba(139, 61, 167, 0.5) 94.28% + ); + opacity: 50%; +} */ + +/* New ui css */ + +.table-border-line { + @apply h-[1px] self-stretch border-b border-[#1C1C20]; +} +.portfolio-bg { + @apply flex flex-col w-full p-6 rounded-2xl; + background: rgba(255, 255, 255, 0.02); +} +.portfolio-card { + @apply flex flex-col gap-2 px-6 py-4 rounded-2xl h-[92px]; + background: linear-gradient( + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% + ); +} +.proposal-id-dashboard { + @apply relative flex items-center justify-center font-bold rounded-lg w-12 text-[14px] tracking-[1.96px] h-[35px]; + background: linear-gradient( + 180deg, + rgba(122, 126, 156, 0.2) 0.5%, + rgba(9, 9, 10, 0.2) 100% + ); +} +.proposal-network-logo { + @apply absolute bottom-[-8px] right-[-4px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] rounded-[100px] border-2 border-solid border-[#111115]; + background: lightgray 50% / cover no-repeat; +} diff --git a/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/error.tsx b/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/loading.tsx b/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/loading.tsx new file mode 100644 index 000000000..e0d5206c2 --- /dev/null +++ b/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import React from 'react'; +import Loading from '../../loader/Loading'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/page.tsx b/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/page.tsx index 1c959b279..595ea63b5 100644 --- a/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/page.tsx +++ b/frontend/src/app/(routes)/(overview)/overview/[...chainNames]/page.tsx @@ -1,11 +1,14 @@ 'use client'; import React from 'react'; -import OverviewPage from '../../overview-components/OverviewPage'; +// import OverviewPage from '../../overview-components/OverviewPage'; import '../../overview.css'; import { useAppSelector } from '@/custom-hooks/StateHooks'; import { RootState } from '@/store/store'; import { useParams } from 'next/navigation'; +// import OverviewTable from '../../overview-components/OverviewTable'; +import WithoutConnectionIllustration from '@/components/illustrations/WithoutConnectionIllustration'; +import OverviewDashboard from '../../overview-components/OverviewDashboard'; const Overview = () => { const params = useParams(); @@ -15,21 +18,41 @@ const Overview = () => { const nameToChainIDs = useAppSelector( (state: RootState) => state.wallet.nameToChainIDs ); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); const chainIDs: string[] = []; Object.keys(nameToChainIDs).forEach((chain) => { chainNames.forEach((paramChain) => { if (chain === paramChain) chainIDs.push(nameToChainIDs[chain]); }); }); - + return ( <> - {chainIDs.length ? ( - + {isWalletConnected ? ( + <> + {chainIDs.length ? ( + + ) : ( + // +
+ - Chain Not found - +
+ )} + ) : ( -
- - Chain Not found - -
+ <> +
+
Dashboard
+
+
+

Summary of your digital assets

+
+
+
+
+ + + )} ); diff --git a/frontend/src/app/(routes)/(overview)/page.tsx b/frontend/src/app/(routes)/(overview)/page.tsx index 6a455b988..f326a7c40 100644 --- a/frontend/src/app/(routes)/(overview)/page.tsx +++ b/frontend/src/app/(routes)/(overview)/page.tsx @@ -1,19 +1,41 @@ 'use client'; import React from 'react'; -import OverviewPage from './overview-components/OverviewPage'; import './overview.css'; import { useAppSelector } from '@/custom-hooks/StateHooks'; import { RootState } from '@/store/store'; +import WithoutConnectionIllustration from '@/components/illustrations/WithoutConnectionIllustration'; +import OverviewDashboard from './overview-components/OverviewDashboard'; const Overview = () => { - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs + const { nameToChainIDs, connected: isWalletConnected, isLoading: isWalletLoading } = useAppSelector( + (state: RootState) => state.wallet ); - const chainIDs = Object.keys(nameToChainIDs).map( - (chainName) => nameToChainIDs[chainName] + + const chainIDs = Object.keys(nameToChainIDs || {}).map( + (chainName) => nameToChainIDs?.[chainName] + ); + + if (isWalletLoading) return null; + + return ( +
+ {isWalletConnected ? ( + + ) : ( +
+
Dashboard
+
+
+

Summary of your digital assets

+
+
+
+ +
+ )} +
); - return ; }; export default Overview; diff --git a/frontend/src/app/(routes)/authz/page.tsx b/frontend/src/app/(routes)/authz/page.tsx deleted file mode 100644 index 2c21bdb03..000000000 --- a/frontend/src/app/(routes)/authz/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const page = () => { - return
authz
; -}; - -export default page; diff --git a/frontend/src/app/(routes)/cosmwasm/Cosmwasm.tsx b/frontend/src/app/(routes)/cosmwasm/Cosmwasm.tsx new file mode 100644 index 000000000..6caf300d8 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/Cosmwasm.tsx @@ -0,0 +1,34 @@ +'use client'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import PageHeader from '@/components/common/PageHeader'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { setChangeNetworkDialogOpen } from '@/store/features/common/commonSlice'; +import { COSMWASM_DESCRIPTION } from '@/utils/constants'; +import React from 'react'; + +const Cosmwasm = () => { + const dispatch = useAppDispatch(); + + const openChangeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: true })); + }; + + return ( +
+ +
+
+ +
+
+
+ ); +}; + +export default Cosmwasm; diff --git a/frontend/src/app/(routes)/cosmwasm/[network]/ChainContracts.tsx b/frontend/src/app/(routes)/cosmwasm/[network]/ChainContracts.tsx new file mode 100644 index 000000000..92d723b99 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/[network]/ChainContracts.tsx @@ -0,0 +1,51 @@ +'use client'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React from 'react'; +import PageContracts from './PageContracts'; +import { useSearchParams } from 'next/navigation'; +import AllContracts from '../components/all-contracts/AllContracts'; +import DeployContract from '../components/deploy-contract/DeployContract'; +import DialogTxExecuteStatus from '../components/tx-status/DialogTxExecuteStatus'; +import DialogTxInstantiateStatus from '../components/tx-status/DialogTxInstantiateStatus'; +import DialogTxUploadCodeStatus from '../components/tx-status/DialogTxUploadCodeStatus'; + +interface ChainContractsProps { + network: string; +} + +const ChainContracts: React.FC = ({ network }) => { + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const chainName = network.toLowerCase(); + const chainID = nameToChainIDs?.[chainName]; + const paramTabName = useSearchParams().get('tab'); + + if (!chainID) { + return ( +
+ - The {chainName} is not supported - +
+ ); + } + + const renderContent = () => { + switch (paramTabName) { + case 'codes': + return ; + case 'deploy': + return ; + default: + return ; + } + }; + + return ( +
+ {renderContent()} + + + +
+ ); +}; + +export default ChainContracts; diff --git a/frontend/src/app/(routes)/cosmwasm/[network]/PageContracts.tsx b/frontend/src/app/(routes)/cosmwasm/[network]/PageContracts.tsx new file mode 100644 index 000000000..0c6a6a591 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/[network]/PageContracts.tsx @@ -0,0 +1,20 @@ +'use client'; +import React from 'react'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import Contract from '../components/single-contract/Contract'; +import PageHeader from '@/components/common/PageHeader'; +import { COSMWASM_DESCRIPTION } from '@/utils/constants'; +const PageContracts = ({ chainName }: { chainName: string }) => { + const nameToChainIDs: Record = useAppSelector( + (state) => state.common.nameToChainIDs + ); + const chainID = nameToChainIDs[chainName]; + return ( +
+ + +
+ ); +}; + +export default PageContracts; diff --git a/frontend/src/app/(routes)/cosmwasm/[network]/page.tsx b/frontend/src/app/(routes)/cosmwasm/[network]/page.tsx new file mode 100644 index 000000000..7dc44f417 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/[network]/page.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import '../cosmwasm.css'; +import ChainContracts from './ChainContracts'; + +const page = ({ params: { network } }: { params: { network: string } }) => { + return ; +}; + +export default page; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/AllContracts.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/AllContracts.tsx new file mode 100644 index 000000000..04c5376b0 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/AllContracts.tsx @@ -0,0 +1,36 @@ +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { useSearchParams } from 'next/navigation'; +import React, { useEffect, useState } from 'react'; +import Contracts from './Contracts'; +import Codes from './Codes'; + +const AllContracts = (props: { chainID: string }) => { + const { chainID } = props; + const { getChainInfo } = useGetChainInfo(); + const { restURLs, chainName } = getChainInfo(chainID); + + const paramCodeId = useSearchParams().get('code_id'); + + const [selectedCodeId, setSelectedCodeId] = useState(paramCodeId); + + useEffect(() => { + setSelectedCodeId(paramCodeId); + }, [paramCodeId]); + + return ( +
+ {selectedCodeId ? ( + + ) : ( + + )} +
+ ); +}; + +export default AllContracts; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/CodeItem.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/CodeItem.tsx new file mode 100644 index 000000000..2d50859dd --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/CodeItem.tsx @@ -0,0 +1,52 @@ +import { shortenAddress, shortenMsg } from '@/utils/util'; +import Link from 'next/link'; +import React from 'react'; +import PermissionsData from './PermissionsData'; +import Copy from '@/components/common/Copy'; +import Image from 'next/image'; + +const CodeItem = ({ + code, + chainLogo, +}: { + code: CodeInfo; + chainLogo: string; +}) => { + return ( +
+
+

Code ID

+

{code.code_id}

+
+
+

Code Hash

+
+

+ + {shortenMsg(code.data_hash, 25)} + +

+ +
+
+
+

Creator

+
+ network-logo +

{shortenAddress(code.creator, 20)}

+ +
+
+ + +
+ ); +}; + +export default CodeItem; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/Codes.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/Codes.tsx new file mode 100644 index 000000000..8d35c7d7d --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/Codes.tsx @@ -0,0 +1,53 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getAllCodes } from '@/store/features/cosmwasm/cosmwasmSlice'; +import { TxStatus } from '@/types/enums'; +import React, { useEffect } from 'react'; +import CodesList from './CodesList'; +import PageHeader from '@/components/common/PageHeader'; +import CodesLoading from '../loaders/CodesLoading'; + +const Codes = ({ + chainID, + baseURLs, +}: { + chainID: string; + baseURLs: string[]; +}) => { + const dispatch = useAppDispatch(); + const codesLoading = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.codes.status + ); + + const codes = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.codes.data.codes + ); + + useEffect(() => { + dispatch(getAllCodes({ baseURLs, chainID })); + }, []); + + return ( +
+ +
+ {codesLoading === TxStatus.PENDING ? ( + + ) : codes?.length ? ( + + ) : ( +
+
+ {codesLoading === TxStatus.REJECTED ? ( +
- Failed to fetch codes -
+ ) : ( + '- No Codes Found -' + )} +
+
+ )} +
+
+ ); +}; + +export default Codes; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/CodesList.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/CodesList.tsx new file mode 100644 index 000000000..c06cbefa5 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/CodesList.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import CodeItem from './CodeItem'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; + +const CodesList = (props: { codes: CodeInfo[]; chainID: string }) => { + const { codes, chainID } = props; + const { getChainInfo } = useGetChainInfo(); + const { chainLogo } = getChainInfo(chainID); + return ( +
+ {codes.map((code) => ( + + ))} +
+ ); +}; + +export default CodesList; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/ContractItem.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/ContractItem.tsx new file mode 100644 index 000000000..9554ff7cf --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/ContractItem.tsx @@ -0,0 +1,40 @@ +import Copy from '@/components/common/Copy'; +import Image from 'next/image'; +import Link from 'next/link'; +import React from 'react'; + +const ContractItem = ({ + contract, + chainLogo, +}: { + contract: string; + chainLogo: string; +}) => { + return ( +
+
+ logo +
+

{contract}

+ +
+
+
+ + + + + + +
+
+ ); +}; + +export default ContractItem; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/Contracts.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/Contracts.tsx new file mode 100644 index 000000000..449b14e13 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/Contracts.tsx @@ -0,0 +1,80 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getAllContractsByCode } from '@/store/features/cosmwasm/cosmwasmSlice'; +import { TxStatus } from '@/types/enums'; +import { useRouter } from 'next/navigation'; +import React, { useEffect } from 'react'; +import ContractsList from './ContractsList'; +import PageHeader from '@/components/common/PageHeader'; +import ContractsLoading from '../loaders/ContractsLoading'; + +const Contracts = ({ + codeId, + baseURLs, + chainID, + chainName, +}: { + codeId: string; + chainID: string; + baseURLs: string[]; + chainName: string; +}) => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch( + getAllContractsByCode({ + baseURLs, + chainID, + codeId, + }) + ); + }, [codeId]); + + const contractsLoading = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.contracts.status + ); + + const contracts = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.contracts.data.contracts + ); + + return ( +
+
+ + +
+
+ {contractsLoading === TxStatus.PENDING ? ( + + ) : contracts?.length ? ( + + ) : ( +
+
+ {contractsLoading === TxStatus.REJECTED ? ( +
+ - Failed to fetch contracts - +
+ ) : ( + '- No Contracts Found -' + )} +
+
+ )} +
+
+ ); +}; +export default Contracts; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/ContractsList.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/ContractsList.tsx new file mode 100644 index 000000000..3b05782f9 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/ContractsList.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import ContractItem from './ContractItem'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; + +const ContractsList = ({ + contracts, + chainID, +}: { + contracts: string[]; + chainID: string; +}) => { + const { getChainInfo } = useGetChainInfo(); + const { chainLogo } = getChainInfo(chainID); + return ( +
+ {contracts.map((contract) => ( +
+
+ +
+
+
+ ))} +
+ ); +}; + +export default ContractsList; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/DialogAddressesList.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/DialogAddressesList.tsx new file mode 100644 index 000000000..ab47a866e --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/DialogAddressesList.tsx @@ -0,0 +1,69 @@ +import Copy from '@/components/common/Copy'; +import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import { CLOSE_ICON_PATH } from '@/utils/constants'; +import { shortenAddress } from '@/utils/util'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const DialogAddressesList = ({ + addresses, + onClose, + open, +}: { + open: boolean; + onClose: () => void; + addresses: string[]; +}) => { + const handleDialogClose = () => { + onClose(); + }; + return ( + + +
+
+
+ close +
+
+
+
+
+ Allowed Addresses +
+
+ List of addresses that are allowed to instantiate contract{' '} +
+
+
+
+ {addresses.map((address) => ( +
+
{shortenAddress(address, 24)}
+ +
+ ))} +
+
+
+
+
+ ); +}; + +export default DialogAddressesList; diff --git a/frontend/src/app/(routes)/cosmwasm/components/all-contracts/PermissionsData.tsx b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/PermissionsData.tsx new file mode 100644 index 000000000..59a2076b1 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/all-contracts/PermissionsData.tsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react'; +import DialogAddressesList from './DialogAddressesList'; + +const PermissionsData = ({ + permission, +}: { + permission: InstantiatePermission; +}) => { + const permissionType = permission.permission; + const [showAddresses, setShowAddresses] = useState(false); + const handleDialogOpen = ( + e: React.MouseEvent, + value: boolean + ) => { + setShowAddresses(value); + e.stopPropagation(); + }; + return ( +
+
+

Permission

+ +
+ setShowAddresses(false)} + open={showAddresses} + /> +
+ ); +}; + +export default PermissionsData; diff --git a/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/AddAddresses.tsx b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/AddAddresses.tsx new file mode 100644 index 000000000..b1af8f37c --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/AddAddresses.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import AddressInputField from './AddressInputField'; +import Image from 'next/image'; +import { ADD_ICON_ROUNDED } from '@/constants/image-names'; + +interface AddAddressesI { + addresses: string[]; + setAddresses: React.Dispatch>; +} + +const AddAddresses = (props: AddAddressesI) => { + const { addresses, setAddresses } = props; + + const onAddAddress = (address: string) => { + setAddresses((prev) => [...prev, address]); + }; + + const onDelete = (index: number) => { + const newAddresses = addresses.filter((_, i) => i !== index); + setAddresses(newAddresses); + }; + + const handleAddressChange = ( + e: React.ChangeEvent, + index: number + ) => { + const input = e.target.value; + const newAddresses = addresses.map((value, key) => { + if (index === key) { + return input.trim(); + } + return value; + }); + setAddresses(newAddresses); + }; + + return ( +
+ {addresses.map((value, index) => ( +
+ +
+ ))} +
+ +
+
+ ); +}; + +export default AddAddresses; diff --git a/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/AddressInputField.tsx b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/AddressInputField.tsx new file mode 100644 index 000000000..dfb57e4c3 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/AddressInputField.tsx @@ -0,0 +1,64 @@ +import { InputAdornment, TextField } from '@mui/material'; +import React from 'react'; +import Image from 'next/image'; +import { customTextFieldStyles } from '@/utils/commonStyles'; +import { MINUS_ICON_DISABLED } from '@/constants/image-names'; + +interface AddressInputFieldI { + address: string; + handleChange: ( + e: React.ChangeEvent, + index: number + ) => void; + onDelete: (index: number) => void; + index: number; + disableDelete: boolean; +} + +const AddressInputField = (props: AddressInputFieldI) => { + const { address, handleChange, onDelete, index, disableDelete } = props; + return ( + + {disableDelete ? null : ( +
{ + onDelete(index); + }} + > + +
+ )} + + ), + }} + onChange={(e) => handleChange(e, index)} + /> + ); +}; + +export default AddressInputField; diff --git a/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/CustomTextField.tsx b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/CustomTextField.tsx new file mode 100644 index 000000000..8b56be33d --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/CustomTextField.tsx @@ -0,0 +1,54 @@ +import { TextField } from '@mui/material'; +import React from 'react'; +import { Control, Controller } from 'react-hook-form'; +import { customTextFieldStyles } from '@/utils/commonStyles'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const CustomTextField = ({ + control, + name, + placeHolder, + rules, + required, +}: { + control: Control; + rules?: any; + name: string; + placeHolder: string; + required: boolean; +}) => { + return ( + ( + + )} + /> + ); +}; + +export default CustomTextField; diff --git a/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/DeployContract.tsx b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/DeployContract.tsx new file mode 100644 index 000000000..2d6cf4b07 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/DeployContract.tsx @@ -0,0 +1,33 @@ +import PageHeader from '@/components/common/PageHeader'; +import { DEPLOY_CONTRACT_DESCRIPTION } from '@/utils/constants'; +import React, { useState } from 'react'; +import DeployContractTabs from './DeployContractTabs'; +import UploadWasmFile from './UploadWasmFile'; +import InstantiateContract from './InstantiateContract'; + +const DeployContract = ({ chainID }: { chainID: string }) => { + const [selectedTab, setSelectedTab] = useState(0); + const handleTabChange = (tab: number) => { + setSelectedTab(tab); + }; + return ( +
+ +
+ +
+ {selectedTab === 0 ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default DeployContract; diff --git a/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/DeployContractTabs.tsx b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/DeployContractTabs.tsx new file mode 100644 index 000000000..dd421aac3 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/DeployContractTabs.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +const DeployContractTabs = ({ + selectedTab, + handleTabChange, +}: { + selectedTab: number; + handleTabChange: (tab: number) => void; +}) => { + const tabs = ['Use Existing code ID', 'Upload WASM file']; + return ( +
+ {tabs.map((tab, index) => ( +
+
handleTabChange(index)} + > + {tab} +
+
+
+ ))} +
+ ); +}; + +export default DeployContractTabs; diff --git a/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/InstantiateContract.tsx b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/InstantiateContract.tsx new file mode 100644 index 000000000..b792b5de9 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/InstantiateContract.tsx @@ -0,0 +1,287 @@ +import { SelectChangeEvent, TextField } from '@mui/material'; +import React, { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import useContracts from '@/custom-hooks/useContracts'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getFormattedFundsList } from '@/utils/util'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { txInstantiateContract } from '@/store/features/cosmwasm/cosmwasmSlice'; +import { TxStatus } from '@/types/enums'; +import { setError } from '@/store/features/common/commonSlice'; +import CustomTextField from './CustomTextField'; +import AttachFunds from '../single-contract/AttachFunds'; +import { multiSendInputFieldStyles } from '@/app/(routes)/transfers/styles'; +import CustomButton from '@/components/common/CustomButton'; +import Image from 'next/image'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; + +interface InstatiateContractInputs { + codeId: string; + label: string; + adminAddress: string; + message: string; +} + +const InstantiateContract = ({ chainID }: { chainID: string }) => { + // ------------------------------------------// + // ---------------DEPENDENCIES---------------// + // ------------------------------------------// + const dispatch = useAppDispatch(); + const { instantiateContract } = useContracts(); + const { getDenomInfo, getChainInfo } = useGetChainInfo(); + const { decimals, minimalDenom } = getDenomInfo(chainID); + const { restURLs, chainName, address: walletAddres } = getChainInfo(chainID); + + // ------------------------------------------// + // ------------------STATES------------------// + // ------------------------------------------// + const [attachFundType, setAttachFundType] = useState('no-funds'); + const [funds, setFunds] = useState([ + { + amount: '', + denom: minimalDenom, + decimals: decimals, + }, + ]); + const [fundsInput, setFundsInput] = useState(''); + + const txInstantiateStatus = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txInstantiate?.status + ); + const showAuthzAlert = useGetShowAuthzAlert(); + + + // ------------------------------------------------// + // -----------------FORM HOOKS---------------------// + // ------------------------------------------------// + const { handleSubmit, control, setValue, getValues, watch } = + useForm({ + defaultValues: { + codeId: '', + label: '', + adminAddress: '', + message: '', + }, + }); + + // ------------------------------------------------// + // -----------------CHANGE HANDLER-----------------// + // ------------------------------------------------// + const handleAttachFundTypeChange = (event: SelectChangeEvent) => { + setAttachFundType(event.target.value); + }; + + // ----------------------------------------------------// + // -----------------CUSTOM VALIDATIONS-----------------// + // ----------------------------------------------------// + const validateJSONInput = ( + input: string, + setInput: (value: string) => void, + errorMessagePrefix: string + ): boolean => { + try { + if (!input?.length) { + dispatch( + setError({ + type: 'error', + message: `Please enter ${errorMessagePrefix}`, + }) + ); + return false; + } + const parsed = JSON.parse(input); + const formattedJSON = JSON.stringify(parsed, undefined, 4); + setInput(formattedJSON); + return true; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + dispatch( + setError({ + type: 'error', + message: `Invalid JSON input: (${errorMessagePrefix}) ${error?.message || ''}`, + }) + ); + return false; + } + }; + + const formatInstantiationMessage = () => { + return validateJSONInput( + getValues('message'), + (value: string) => { + setValue('message', value); + }, + 'Instatiation Message' + ); + }; + + const validateFunds = () => { + return validateJSONInput( + fundsInput, + (value: string) => { + setFundsInput(value); + }, + 'Attach Funds List' + ); + }; + + // ------------------------------------------// + // ---------------TRANSACTION----------------// + // ------------------------------------------// + const onSubmit = (data: InstatiateContractInputs) => { + const parsedCodeId = Number(data.codeId); + if (Number.isNaN(parsedCodeId)) { + dispatch( + setError({ + type: 'error', + message: 'Invalid Code ID', + }) + ); + return; + } + + if (!formatInstantiationMessage()) return; + if (attachFundType === 'json' && !validateFunds()) return; + + const attachedFunds = getFormattedFundsList( + funds, + fundsInput, + attachFundType + ); + + dispatch( + txInstantiateContract({ + chainID, + codeId: Number(data.codeId), + instantiateContract, + label: data.label, + msg: data.message, + baseURLs: restURLs, + admin: data.adminAddress ? data.adminAddress : undefined, + funds: attachedFunds, + }) + ); + }; + + return ( +
+
+
+
+
+

Code ID

+ +
+
+
+

Admin Address

+
{ + if (watch('adminAddress') === walletAddres) { + setValue('adminAddress', ''); + } else { + setValue('adminAddress', walletAddres); + } + }} + > + {watch('adminAddress') === walletAddres ? ( + after-check-icon + ) : ( + before-check-icon + )} +

Assign me

+
+
+ +
+
+

Label

+ +
+
+
Attach Funds
+ +
+
+
+

Instantiate message

+
+ ( + + )} + /> + +
+
+
+
+ +
+ ); +}; + +export default InstantiateContract; diff --git a/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/SelectPermissionType.tsx b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/SelectPermissionType.tsx new file mode 100644 index 000000000..7adb5b44d --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/SelectPermissionType.tsx @@ -0,0 +1,68 @@ +import { + FormControl, + MenuItem, + Select, + SelectChangeEvent, +} from '@mui/material'; +import React from 'react'; +import { AccessType } from 'cosmjs-types/cosmwasm/wasm/v1/types'; +import { customSelectStyles } from '@/utils/commonStyles'; + +interface SelectPermissionTypeI { + handleAccessTypeChange: (event: SelectChangeEvent) => void; + accessType: AccessType; +} + +const SelectPermissionType = (props: SelectPermissionTypeI) => { + const { handleAccessTypeChange, accessType } = props; + return ( +
+ + + +
+ ); +}; + +export default SelectPermissionType; diff --git a/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/UploadWasmFile.tsx b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/UploadWasmFile.tsx new file mode 100644 index 000000000..f5c81bd24 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/deploy-contract/UploadWasmFile.tsx @@ -0,0 +1,226 @@ +import React, { useState } from 'react'; +import { IconButton, SelectChangeEvent, Tooltip } from '@mui/material'; +import ClearIcon from '@mui/icons-material/Clear'; +import Image from 'next/image'; +import { useForm } from 'react-hook-form'; +import { AccessType } from 'cosmjs-types/cosmwasm/wasm/v1/types'; +import useContracts from '@/custom-hooks/useContracts'; +import { gzip } from 'node-gzip'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { uploadCode } from '@/store/features/cosmwasm/cosmwasmSlice'; +import { TxStatus } from '@/types/enums'; +import { setError } from '@/store/features/common/commonSlice'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import SelectPermissionType from './SelectPermissionType'; +import AddAddresses from './AddAddresses'; +import CustomButton from '@/components/common/CustomButton'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; + +interface UploadContractInput { + wasmFile?: File; + permission: AccessType; + allowedAddresses: Record<'address', string>[]; +} + +const UploadWasmFile = ({ chainID }: { chainID: string }) => { + // ------------------------------------------// + // ---------------DEPENDENCIES---------------// + // ------------------------------------------// + const dispatch = useAppDispatch(); + const { uploadContract } = useContracts(); + const { getChainInfo } = useGetChainInfo(); + const { address: walletAddress, restURLs } = getChainInfo(chainID); + + // ------------------------------------------// + // -----------------STATES-------------------// + // ------------------------------------------// + const [uploadedFileName, setUploadedFileName] = useState(''); + const [accessType, setAccessType] = useState(3); + const [addresses, setAddresses] = useState(['']); + + const uploadContractStatus = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txUpload.status + ); + const showAuthzAlert = useGetShowAuthzAlert(); + + const uploadContractLoading = uploadContractStatus === TxStatus.PENDING; + + // ------------------------------------------------// + // -----------------FORM HOOKS---------------------// + // ------------------------------------------------// + const { setValue, handleSubmit, getValues } = useForm({ + defaultValues: { + wasmFile: undefined, + permission: AccessType.ACCESS_TYPE_EVERYBODY, + allowedAddresses: [{ address: '' }], + }, + }); + + const validateAddresses = () => { + for (const addr of addresses) { + if (addr.length === 0) { + return false; + } + } + return true; + }; + + // ------------------------------------------------// + // -----------------CHANGE HANDLERS----------------// + // ------------------------------------------------// + const handleAccessTypeChange = (event: SelectChangeEvent) => { + setAccessType(event.target.value as AccessType); + setValue('permission', event.target.value as AccessType); + }; + + const resetUploadedFile = () => { + const fileInputElement = document.getElementById( + 'wasm-file-upload' + ) as HTMLInputElement; + fileInputElement.value = ''; + setValue('wasmFile', undefined); + }; + + // ------------------------------------------// + // ---------------TRANSACTION----------------// + // ------------------------------------------// + const onUpload = async (data: UploadContractInput) => { + const wasmcode = getValues('wasmFile')?.arrayBuffer(); + if (!wasmcode) { + dispatch( + setError({ type: 'error', message: 'Please upload the wasm file' }) + ); + return; + } + if ( + data.permission === AccessType.ACCESS_TYPE_ANY_OF_ADDRESSES && + !validateAddresses() + ) { + dispatch(setError({ type: 'error', message: 'Address cannot be empty' })); + return; + } + const msg: Msg = { + typeUrl: '/cosmwasm.wasm.v1.MsgStoreCode', + value: { + sender: walletAddress, + wasmByteCode: await gzip(new Uint8Array(await wasmcode)), + instantiatePermission: { + permission: data.permission, + addresses, + address: '', + }, + }, + }; + dispatch( + uploadCode({ + chainID, + address: walletAddress, + messages: [msg], + baseURLs: restURLs, + uploadContract, + }) + ); + }; + return ( +
+
+
+
{ + document.getElementById('wasm-file-upload')!.click(); + }} + > +
+ {uploadedFileName ? ( + <> +
+ {uploadedFileName}{' '} + + { + if (!uploadContractLoading) { + setUploadedFileName(''); + resetUploadedFile(); + e.preventDefault(); + e.stopPropagation(); + } + }} + > + + + +
+ + ) : ( + <> + Upload Icon +
Upload (.wasm) file here
+ + )} +
+
+ { + const { files } = e.target; + const selectedFiles = files as FileList; + const selectedFile = selectedFiles?.[0]; + setValue('wasmFile', selectedFile); + setUploadedFileName(selectedFile?.name || ''); + }} + style={{ display: 'none' }} + /> +
+
+
+ Select Instantiate Permission +
+
+ +
+
+ {accessType === AccessType.ACCESS_TYPE_ANY_OF_ADDRESSES ? ( +
+ +
+ ) : null} +
+
+
+ +
+ ); +}; + +export default UploadWasmFile; diff --git a/frontend/src/app/(routes)/cosmwasm/components/loaders/CodesLoading.tsx b/frontend/src/app/(routes)/cosmwasm/components/loaders/CodesLoading.tsx new file mode 100644 index 000000000..754fcc404 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/loaders/CodesLoading.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +const CodesLoading = () => { + return ( +
+ {[1, 2, 3, 4].map((_, index) => ( +
+
+

Code ID

+

+
+
+

Code Hash

+
+
+
+

Creator

+
+
+
+
+
+
+

Permission

+
+
+
+ ))} +
+ ); +}; + +export default CodesLoading; diff --git a/frontend/src/app/(routes)/cosmwasm/components/loaders/ContractsLoading.tsx b/frontend/src/app/(routes)/cosmwasm/components/loaders/ContractsLoading.tsx new file mode 100644 index 000000000..d003c210b --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/loaders/ContractsLoading.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const ContractsLoading = () => { + return ( +
+ {[1, 2, 3, 4].map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+ ); +}; + +export default ContractsLoading; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/AmountInputField.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/AmountInputField.tsx new file mode 100644 index 000000000..1bcfe936b --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/AmountInputField.tsx @@ -0,0 +1,58 @@ +import { InputAdornment, TextField } from '@mui/material'; +import React from 'react'; +import Image from 'next/image'; +import { customTextFieldStyles } from '@/utils/commonStyles'; +import { MINUS_ICON_DISABLED } from '@/constants/image-names'; + +interface AmountInputFieldI { + amount: string; + handleChange: ( + e: React.ChangeEvent, + index: number + ) => void; + onDelete: () => void; + index: number; + disableDelete: boolean; +} + +const AmountInputField = (props: AmountInputFieldI) => { + const { amount, handleChange, onDelete, index, disableDelete } = props; + return ( + + {disableDelete ? null : ( +
+ +
+ )} + + ), + }} + onChange={(e) => handleChange(e, index)} + /> + ); +}; + +export default AmountInputField; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/AttachFunds.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/AttachFunds.tsx new file mode 100644 index 000000000..e00374e7f --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/AttachFunds.tsx @@ -0,0 +1,106 @@ +import { + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, +} from '@mui/material'; +import React from 'react'; +import useContracts from '@/custom-hooks/useContracts'; +import SelectFunds from './SelectFunds'; +import ProvideFundsJson from './ProvideFundsJson'; +import { customSelectStyles } from '@/utils/commonStyles'; + +interface AttachFundsI { + handleAttachFundTypeChange: (event: SelectChangeEvent) => void; + attachFundType: string; + chainName: string; + setFunds: (value: React.SetStateAction) => void; + funds: FundInfo[]; + fundsInputJson: string; + setFundsInputJson: (value: string) => void; +} + +const AttachFunds = (props: AttachFundsI) => { + const { + handleAttachFundTypeChange, + attachFundType, + chainName, + funds, + setFunds, + fundsInputJson, + setFundsInputJson, + } = props; + const onAddFund = (fund: FundInfo) => { + setFunds((prev) => [...prev, fund]); + }; + const { getChainAssets } = useContracts(); + const { assetsList } = getChainAssets(chainName); + const onDelete = (index: number) => { + if (funds.length === 1) return; + const newFunds = funds.filter((_, i) => i !== index); + setFunds(newFunds); + }; + return ( +
+ + + Select Transaction + + + + {attachFundType === 'select' ? ( + + ) : null} + {attachFundType === 'json' ? ( + + ) : null} +
+ ); +}; + +export default AttachFunds; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/Contract.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/Contract.tsx new file mode 100644 index 000000000..faa9951af --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/Contract.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import useContracts from '@/custom-hooks/useContracts'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { setContract } from '@/store/features/cosmwasm/cosmwasmSlice'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { CircularProgress } from '@mui/material'; +import ContractInfo from './ContractInfo'; +import SearchContract from './SearchContract'; +import Link from 'next/link'; + +const Contract = ({ chainID }: { chainID: string }) => { + const dispatch = useAppDispatch(); + const [selectedContract, setSelectedContract] = useState({ + address: '', + name: '', + }); + const handleSelectContract = (address: string, name: string) => { + setSelectedContract({ address, name }); + }; + + const { getChainInfo } = useGetChainInfo(); + const { restURLs, chainName } = getChainInfo(chainID); + + const { getContractInfo, contractError, contractLoading } = useContracts(); + + const paramsContractAddress = useSearchParams().get('contract'); + + const fetchContractInfo = async () => { + try { + const { data } = await getContractInfo({ + address: paramsContractAddress || '', + baseURLs: restURLs, + chainID, + }); + if (data) { + dispatch( + setContract({ + chainID, + contractAddress: data?.address, + contractInfo: data?.contract_info, + }) + ); + setSelectedContract({ + address: data?.address, + name: data?.contract_info?.label, + }); + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + console.log(error); + } + }; + + useEffect(() => { + if (paramsContractAddress?.length) { + fetchContractInfo(); + } + }, [paramsContractAddress]); + + return ( +
+
+
+ Don't have a contract? then deploy it + + here + +
+
+ +
+
+ {contractLoading ? ( +
+ +
+ Fetching contract info +
+
+ ) : contractError ? ( +
{contractError}
+ ) : null} + {selectedContract.address ? : null} +
+ ); +}; + +export default Contract; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractInfo.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractInfo.tsx new file mode 100644 index 000000000..d6fb17a51 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractInfo.tsx @@ -0,0 +1,128 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React, { useState } from 'react'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import Image from 'next/image'; +import QueryContract from './QueryContract'; +import ExecuteContract from './ExecuteContract'; + +const ContractInfo = ({ chainID }: { chainID: string }) => { + const selectedContractAddress = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.contractAddress + ); + const selectedContractInfo = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.contractInfo + ); + const tabs = ['Execute Contract', 'Query Contract']; + const [selectedTab, setSelectedTab] = useState(tabs[0]); + const [infoOpen, setInfoOpen] = useState(false); + + const { getChainInfo } = useGetChainInfo(); + const { + restURLs, + rpcURLs, + address: walletAddress, + chainName, + } = getChainInfo(chainID); + + return ( +
+
+
+
+
Contract Details
+ setInfoOpen((prev) => !prev)} + className="cursor-pointer" + src={infoOpen ? '/expand-close.svg' : '/expand-open.svg'} + height={24} + width={24} + alt="Expand" + /> +
+ {infoOpen ? ( +
+ + + + + +
+ ) : null} +
+
+
+
+ {tabs.map((tab) => ( + + ))} +
+
+ {selectedTab === 'Query Contract' ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default ContractInfo; + +const ContractInfoAttribute = ({ + name, + value, +}: { + name: string; + value: string; +}) => { + return ( + <> + {value ? ( +
+
{name}
+
{value}
+
+ ) : null} + + ); +}; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractNotSelected.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractNotSelected.tsx new file mode 100644 index 000000000..917d4dc4f --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractNotSelected.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const ContractNotSelected = () => { + return ( +
+
Select or search contract
+
+ ); +}; + +export default ContractNotSelected; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractSelected.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractSelected.tsx new file mode 100644 index 000000000..d44ae9987 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ContractSelected.tsx @@ -0,0 +1,30 @@ +import Copy from '@/components/common/Copy'; +import React from 'react'; + +const ContractSelected = ({ + contract, + openSearchDialog, +}: { + contract: { address: string; name: string }; + openSearchDialog: () => void; +}) => { + const { address, name } = contract; + return ( +
+
+
{name}
+ {/* */} +
+
{address}
+ +
+ + +
+
+ ); +}; + +export default ContractSelected; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/DialogSearchContract.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/DialogSearchContract.tsx new file mode 100644 index 000000000..a7efe8511 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/DialogSearchContract.tsx @@ -0,0 +1,140 @@ +import { CircularProgress } from '@mui/material'; +import React, { useState } from 'react'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { setContract } from '@/store/features/cosmwasm/cosmwasmSlice'; +import useContracts from '@/custom-hooks/useContracts'; +import { useRouter } from 'next/navigation'; +import { shortenName } from '@/utils/util'; +import SearchInputField from './SearchInputField'; +import CustomDialog from '@/components/common/CustomDialog'; +import Copy from '@/components/common/Copy'; + +interface DialogSearchContractI { + open: boolean; + onClose: () => void; + chainID: string; + restURLs: string[]; + handleSelectContract: (address: string, name: string) => void; +} + +const DialogSearchContract = (props: DialogSearchContractI) => { + const { onClose, open, chainID, restURLs, handleSelectContract } = props; + const dispatch = useAppDispatch(); + const router = useRouter(); + const handleClose = () => { + onClose(); + setSearchTerm(''); + setSearchResult(null); + }; + const [searchResult, setSearchResult] = useState( + null + ); + + const [searchTerm, setSearchTerm] = useState(''); + const { getContractInfo, contractLoading, contractError } = useContracts(); + + const onSearchContract = async (e: React.FormEvent) => { + e.preventDefault(); + const { data } = await getContractInfo({ + address: searchTerm, + baseURLs: restURLs, + chainID, + }); + setSearchResult(data); + }; + + const onSelectContract = () => { + if (searchResult) { + dispatch( + setContract({ + chainID, + contractAddress: searchResult?.address, + contractInfo: searchResult?.contract_info, + }) + ); + router.push(`?contract=${searchResult?.address}`); + handleSelectContract( + searchResult?.address, + searchResult?.contract_info?.label + ); + onClose(); + setSearchTerm(''); + setSearchResult(null); + } + }; + + return ( + +
+
+
onSearchContract(e)}> +
+ setSearchTerm(value)} + /> + +
+
+
+ {contractLoading ? ( +
+ +
+ Searching for contract +
+
+ ) : ( + <> + {searchResult ? ( +
+
Search Result
+
+
onSelectContract()} + className="contract-item" + > +
+
+ {shortenName(searchResult?.contract_info?.label, 20)} +
+ {/* */} +
+ + {searchResult?.address}{' '} + + +
+
Select
+
+
+
+ ) : ( + <> + {contractError ? ( +
Error: {contractError}
+ ) : null} + + )} + + )} +
+
+
+
+ ); +}; + +export default DialogSearchContract; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/ExecuteContract.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ExecuteContract.tsx new file mode 100644 index 000000000..a7f4b7fd5 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ExecuteContract.tsx @@ -0,0 +1,241 @@ +import useContracts from '@/custom-hooks/useContracts'; +import { SelectChangeEvent } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { executeContract } from '@/store/features/cosmwasm/cosmwasmSlice'; +import { getFormattedFundsList } from '@/utils/util'; +import { setError } from '@/store/features/common/commonSlice'; +import AttachFunds from './AttachFunds'; +import ExecuteContractInputs from './ExecuteContractInputs'; + +interface ExecuteContractI { + address: string; + baseURLs: string[]; + chainID: string; + rpcURLs: string[]; + walletAddress: string; + chainName: string; +} + +const ExecuteContract = (props: ExecuteContractI) => { + const { address, baseURLs, chainID, rpcURLs, walletAddress, chainName } = + props; + + // ------------------------------------------// + // ---------------DEPENDENCIES---------------// + // ------------------------------------------// + const dispatch = useAppDispatch(); + const { + getExecutionOutput, + getExecuteMessages, + executeMessagesError, + executeMessagesLoading, + getExecuteMessagesInputs, + executeInputsError, + executeInputsLoading, + } = useContracts(); + const { getDenomInfo } = useGetChainInfo(); + const { decimals, minimalDenom } = getDenomInfo(chainID); + + // ------------------------------------------// + // ------------------STATES------------------// + // ------------------------------------------// + const [executeInput, setExecuteInput] = useState(''); + const [attachFundType, setAttachFundType] = useState('no-funds'); + const [funds, setFunds] = useState([ + { + amount: '', + denom: minimalDenom, + decimals: decimals, + }, + ]); + const [fundsInput, setFundsInput] = useState(''); + const [executeMessages, setExecuteMessages] = useState([]); + const [selectedMessage, setSelectedMessage] = useState(''); + const [executeMessageInputs, setExecuteMessageInputs] = useState( + [] + ); + + const txExecuteLoading = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID].txExecute.status + ); + + // ------------------------------------------------// + // -----------------CHANGE HANDLERS----------------// + // ------------------------------------------------// + const handleExecuteInputChange = ( + e: React.ChangeEvent + ) => { + setExecuteInput(e.target.value); + }; + + const handleAttachFundTypeChange = (event: SelectChangeEvent) => { + setAttachFundType(event.target.value); + }; + + const handleSelectMessage = async (msg: string) => { + setExecuteInput(`{\n\t"${msg}": {}\n}`); + setSelectedMessage(msg); + const { messages } = await getExecuteMessagesInputs({ + chainID, + contractAddress: address, + rpcURLs, + msg: { [msg]: {} }, + msgName: msg, + extractedMessages: [], + }); + setExecuteMessageInputs(messages); + }; + + const handleSelectedMessageInputChange = (value: string) => { + setExecuteInput( + JSON.stringify( + { + [selectedMessage]: { + [value]: '', + }, + }, + undefined, + 2 + ) + ); + }; + + // ----------------------------------------------------// + // -----------------CUSTOM VALIDATIONS-----------------// + // ----------------------------------------------------// + const validateJSONInput = ( + input: string, + setInput: React.Dispatch>, + errorMessagePrefix: string + ): boolean => { + try { + if (!input?.length) { + dispatch( + setError({ + type: 'error', + message: `Please enter ${errorMessagePrefix}`, + }) + ); + return false; + } + const parsed = JSON.parse(input); + const formattedJSON = JSON.stringify(parsed, undefined, 4); + setInput(formattedJSON); + return true; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + dispatch( + setError({ + type: 'error', + message: `Invalid JSON input: (${errorMessagePrefix}) ${error?.message || ''}`, + }) + ); + return false; + } + }; + + const formatExecutionMessage = () => { + return validateJSONInput( + executeInput, + setExecuteInput, + 'Execution Message' + ); + }; + + const validateFunds = () => { + return validateJSONInput(fundsInput, setFundsInput, 'Attach Funds List'); + }; + + // ------------------------------------------// + // ---------------TRANSACTION----------------// + // ------------------------------------------// + const onExecute = async (input: string) => { + if (!input?.length) { + dispatch( + setError({ + type: 'error', + message: 'Please enter execution message', + }) + ); + return; + } + if (!formatExecutionMessage()) return; + if (attachFundType === 'json' && !validateFunds()) return; + + const attachedFunds = getFormattedFundsList( + funds, + fundsInput, + attachFundType + ); + + dispatch( + executeContract({ + chainID, + contractAddress: address, + msgs: input, + rpcURLs, + walletAddress, + funds: attachedFunds, + baseURLs, + getExecutionOutput, + }) + ); + }; + + // ------------------------------------------// + // ---------------SIDE EFFECT----------------// + // ------------------------------------------// + useEffect(() => { + setSelectedMessage(''); + setExecuteMessageInputs([]); + const fetchMessages = async () => { + const { messages } = await getExecuteMessages({ + chainID, + contractAddress: address, + rpcURLs, + }); + setExecuteMessages(messages); + }; + fetchMessages(); + }, [address]); + + return ( +
+ +
+
Attach Funds
+
+ +
+
+
+ ); +}; + +export default ExecuteContract; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/ExecuteContractInputs.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ExecuteContractInputs.tsx new file mode 100644 index 000000000..c05812c2e --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ExecuteContractInputs.tsx @@ -0,0 +1,241 @@ +import { CircularProgress, TextField } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { TxStatus } from '@/types/enums'; +import MessageInputFields from './MessageInputFields'; +import { queryInputStyles } from '../../styles'; +import ToggleSwitch from '@/components/common/ToggleSwitch'; + +const ExecuteContractInputs = (props: ExecuteContractInputsI) => { + const { + messagesLoading, + executeMessages, + handleSelectMessage, + executeMessageInputs, + selectedMessage, + handleSelectedMessageInputChange, + executeInput, + handleExecuteInputChange, + onExecute, + executionLoading, + formatJSON, + executeInputsError, + executeInputsLoading, + messagesError, + contractAddress, + } = props; + + // ------------------------------------------// + // ------------------STATES------------------// + // ------------------------------------------// + const [isJSONInput, setIsJSONInput] = useState(false); + const [messageInputFields, setMessageInputFields] = useState< + MessageInputField[] + >([]); + + // ------------------------------------------------// + // -----------------CHANGE HANDLERS----------------// + // ------------------------------------------------// + const handleInputMessageChange = ( + e: React.ChangeEvent, + index: number + ) => { + const input = e.target.value; + const updatedFields = messageInputFields.map((value, key) => { + if (index === key) { + value.value = input; + } + return value; + }); + setMessageInputFields(updatedFields); + }; + + const executeContract = () => { + let messageInputs = {}; + messageInputFields.forEach((field) => { + messageInputs = { ...messageInputs, [field.name]: field.value }; + }); + const executionInput = JSON.stringify( + { + [selectedMessage]: messageInputs, + }, + undefined, + 2 + ); + onExecute(executionInput); + }; + + // ------------------------------------------// + // ---------------SIDE EFFECTS----------------// + // ------------------------------------------// + useEffect(() => { + const inputFields: MessageInputField[] = []; + executeMessageInputs.forEach((messageInput) => { + inputFields.push({ name: messageInput, open: false, value: '' }); + }); + setMessageInputFields(inputFields); + }, [executeMessageInputs]); + + useEffect(() => { + setMessageInputFields([]); + }, [contractAddress]); + + return ( +
+
+
+ Execution Messages: + {messagesLoading ? ( + + {' '} + Fetching messages{' '} + + ) : executeMessages?.length ? null : ( + No messages found + )} +
+
+ {executeMessages?.map((msg) => ( + + ))} +
+
+ {isJSONInput && executeMessageInputs?.length ? ( +
+
+ Suggested Inputs for{' '} + {selectedMessage}: +
+
+ {executeMessageInputs?.map((msg) => ( + + ))} +
+
+ ) : null} +
+
+ {isJSONInput + ? 'Enter execution message in JSON format:' + : messageInputFields.length + ? 'Enter fields to execute:' + : 'Execution Input:'} +
+
+
+ JSON Input +
+ setIsJSONInput((prev) => !prev)} + /> +
+
+ {isJSONInput ? ( +
+ + + +
+ ) : executeInputsLoading ? ( +
+ + Fetching message inputs + +
+ ) : executeInputsError ? ( +
+ Couldn't fetch message inputs, Please switch to JSON format +
+ ) : !messageInputFields.length ? ( +
+ {selectedMessage?.length ? ( +
+
+
{selectedMessage}
+
+ +
+ ) : ( +
+ {messagesError ? ( +
+ Couldn't fetch messages, Please switch to JSON format +
+ ) : ( +
+ - Select a message to execute - +
+ )} +
+ )} +
+ ) : ( + + )} +
+ ); +}; + +export default ExecuteContractInputs; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/Fund.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/Fund.tsx new file mode 100644 index 000000000..dd598c2a3 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/Fund.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import AmountInputField from './AmountInputField'; +import TokensList from './TokensList'; + +const Fund = ({ + assetsList, + fund, + onDelete, + index, + funds, + setFunds, + disableDelete, +}: { + assetsList: AssetInfo[]; + fund: FundInfo; + onDelete: () => void; + index: number; + funds: FundInfo[]; + setFunds: (value: React.SetStateAction) => void; + disableDelete: boolean; +}) => { + const handleAmountChange = ( + e: React.ChangeEvent, + index: number + ) => { + const input = e.target.value; + const newFunds = funds.map((value, key) => { + if (index === key) { + if (/^-?\d*\.?\d*$/.test(input)) { + if ((input.match(/\./g) || []).length <= 1) { + value.amount = input; + } + } + } + return value; + }); + setFunds(newFunds); + }; + + return ( +
+ + +
+ ); +}; + +export default Fund; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/MessageInputFields.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/MessageInputFields.tsx new file mode 100644 index 000000000..001e53764 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/MessageInputFields.tsx @@ -0,0 +1,68 @@ +import { TxStatus } from '@/types/enums'; +import { customTextFieldStyles } from '@/utils/commonStyles'; +import { CircularProgress, TextField } from '@mui/material'; +import React from 'react'; + +const MessageInputFields = ({ + fields, + handleChange, + onQuery, + queryLoading, + isQuery, +}: { + fields: MessageInputField[]; + handleChange: ( + e: React.ChangeEvent, + index: number + ) => void; + onQuery: () => void; + queryLoading: TxStatus; + isQuery: boolean; +}) => { + return ( +
+
+ {fields.map((field, index) => ( +
+
+
+ {field.name} +
+
+
+
+ handleChange(e, index)} + autoFocus={true} + sx={customTextFieldStyles} + fullWidth + /> +
+
+
+ ))} +
+ +
+
+
+ ); +}; + +export default MessageInputFields; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/ProvideFundsJson.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ProvideFundsJson.tsx new file mode 100644 index 000000000..5c7237d9e --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/ProvideFundsJson.tsx @@ -0,0 +1,69 @@ +import { TextField } from '@mui/material'; +import React from 'react'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import { queryInputStyles } from '../../styles'; + +interface ProvideFundsJsonI { + fundsInput: string; + setFundsInput: (value: string) => void; +} + +const ProvideFundsJson = (props: ProvideFundsJsonI) => { + const { fundsInput, setFundsInput } = props; + const dispatch = useAppDispatch(); + const handleFundsChange = ( + e: React.ChangeEvent + ) => { + setFundsInput(e.target.value); + }; + const formatJSON = () => { + try { + const parsed = JSON.parse(fundsInput); + const formattedJSON = JSON.stringify(parsed, undefined, 4); + setFundsInput(formattedJSON); + return; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + dispatch( + setError({ + type: 'error', + message: + 'Invalid JSON input: (Attach Funds) ' + (error?.message || ''), + }) + ); + } + }; + + return ( +
+ + +
+ ); +}; + +export default ProvideFundsJson; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/QueryContract.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/QueryContract.tsx new file mode 100644 index 000000000..2a47bfcf8 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/QueryContract.tsx @@ -0,0 +1,177 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useContracts from '@/custom-hooks/useContracts'; +import { queryContractInfo } from '@/store/features/cosmwasm/cosmwasmSlice'; +import React, { useEffect, useState } from 'react'; +import { setError } from '@/store/features/common/commonSlice'; +import QueryContractInputs from './QueryContractInputs'; + +interface QueryContractI { + address: string; + baseURLs: string[]; + chainID: string; +} + +const QueryContract = (props: QueryContractI) => { + const { address, baseURLs, chainID } = props; + + // ------------------------------------------// + // ---------------DEPENDENCIES---------------// + // ------------------------------------------// + const dispatch = useAppDispatch(); + const { + getContractMessages, + getQueryContract, + getContractMessageInputs, + messagesLoading, + messageInputsLoading, + messageInputsError, + messagesError, + } = useContracts(); + + // ------------------------------------------// + // ------------------STATES------------------// + // ------------------------------------------// + const [queryText, setQueryText] = useState(''); + const [contractMessages, setContractMessages] = useState([]); + const [contractMessageInputs, setContractMessageInputs] = useState( + [] + ); + const [selectedMessage, setSelectedMessage] = useState(''); + + const queryOutput = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.query.queryOutput + ); + const queryLoading = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.query.status + ); + + // ------------------------------------------------// + // -----------------CHANGE HANDLERS----------------// + // ------------------------------------------------// + const handleQueryChange = ( + e: React.ChangeEvent + ) => { + setQueryText(e.target.value); + }; + + const handleSelectMessage = async (msg: string) => { + setQueryText(`{\n\t"${msg}": {}\n}`); + setSelectedMessage(msg); + const { messages } = await getContractMessageInputs({ + address, + baseURLs, + queryMsg: { [msg]: {} }, + msgName: msg, + extractedMessages: [], + chainID, + }); + setContractMessageInputs(messages); + }; + + const handleSelectedMessageInputChange = (value: string) => { + setQueryText( + JSON.stringify( + { + [selectedMessage]: { + [value]: '', + }, + }, + undefined, + 2 + ) + ); + }; + + const formatJSON = () => { + try { + const parsed = JSON.parse(queryText); + const formattedJSON = JSON.stringify(parsed, undefined, 4); + setQueryText(formattedJSON); + return true; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + dispatch( + setError({ + type: 'error', + message: 'Invalid JSON input: ' + (error?.message || ''), + }) + ); + } + return false; + }; + + // --------------------------------------// + // -----------------QUERY----------------// + // --------------------------------------// + const onQuery = (queryInput: string) => { + if (!queryInput?.length) { + dispatch( + setError({ + type: 'error', + message: 'Please enter query message', + }) + ); + return; + } + if (!formatJSON()) { + return; + } + + dispatch( + queryContractInfo({ + address, + baseURLs, + queryData: queryInput, + chainID, + getQueryContract, + }) + ); + }; + + // ------------------------------------------// + // ---------------SIDE EFFECT----------------// + // ------------------------------------------// + useEffect(() => { + setSelectedMessage(''); + setContractMessageInputs([]); + const fetchMessages = async () => { + const { messages } = await getContractMessages({ + address, + baseURLs, + chainID, + }); + setContractMessages(messages); + }; + fetchMessages(); + }, [address]); + + return ( +
+ +
+
Query Output:
+
+
{JSON.stringify(queryOutput, undefined, 2)}
+
+
+
+ ); +}; + +export default QueryContract; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/QueryContractInputs.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/QueryContractInputs.tsx new file mode 100644 index 000000000..74c4a5f1c --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/QueryContractInputs.tsx @@ -0,0 +1,244 @@ +import { CircularProgress, TextField } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { TxStatus } from '@/types/enums'; +import { queryInputStyles } from '../../styles'; +import MessageInputFields from './MessageInputFields'; +import ToggleSwitch from '@/components/common/ToggleSwitch'; +import CustomButton from '@/components/common/CustomButton'; + +const QueryContractInputs = (props: QueryContractInputsI) => { + const { + contractMessageInputs, + contractMessages, + handleQueryChange, + handleSelectMessage, + handleSelectedMessageInputChange, + messagesLoading, + queryText, + selectedMessage, + onQuery, + formatJSON, + queryLoading, + messageInputsLoading, + messageInputsError, + messagesError, + contractAddress, + } = props; + + // ------------------------------------------// + // ------------------STATES------------------// + // ------------------------------------------// + const [isJSONInput, setIsJSONInput] = useState(false); + const [messageInputFields, setMessageInputFields] = useState< + MessageInputField[] + >([]); + + // ------------------------------------------------// + // -----------------CHANGE HANDLERS----------------// + // ------------------------------------------------// + const handleInputMessageChange = ( + e: React.ChangeEvent, + index: number + ) => { + const input = e.target.value; + const updatedFields = messageInputFields.map((value, key) => { + if (index === key) { + value.value = input; + } + return value; + }); + setMessageInputFields(updatedFields); + }; + + const queryContract = () => { + const messageInputs = messageInputFields.reduce( + (acc, field) => ({ + ...acc, + [field.name]: field.value, + }), + {} + ); + const queryInput = JSON.stringify( + { + [selectedMessage]: messageInputs, + }, + undefined, + 2 + ); + onQuery(queryInput); + }; + + // ------------------------------------------// + // ---------------SIDE EFFECT----------------// + // ------------------------------------------// + useEffect(() => { + const inputFields: MessageInputField[] = []; + contractMessageInputs.forEach((messageInput) => { + inputFields.push({ name: messageInput, open: false, value: '' }); + }); + setMessageInputFields(inputFields); + }, [contractMessageInputs]); + + useEffect(() => { + setMessageInputFields([]); + }, [contractAddress]); + + return ( +
+
+
+ Query Messages: + {messagesLoading ? ( + + Fetching messages{' '} + + ) : contractMessages?.length ? null : ( + No messages found + )} +
+
+ {contractMessages?.map((msg) => ( +
handleSelectMessage(msg)} + key={msg} + className={`selected-msgs ${selectedMessage === msg ? 'bg-[#ffffff14] border-transparent' : 'border-[#ffffff26]'}`} + > + {msg} +
+ ))} +
+
+ {isJSONInput && messageInputsLoading ? ( +
+ + Fetching message inputs + +
+ ) : isJSONInput && contractMessageInputs?.length ? ( +
+
+ Suggested Inputs for{' '} + {selectedMessage}: +
+
+ {contractMessageInputs?.map((msg) => ( +
handleSelectedMessageInputChange(msg)} + key={msg} + className="selected-msgs border-[#ffffff26]" + > + {msg} +
+ ))} +
+
+ ) : null} +
+
+ {isJSONInput + ? 'Enter query in JSON format:' + : messageInputFields.length + ? 'Enter field value to query:' + : ''} +
+
+
+ JSON Input +
+ setIsJSONInput((prev) => !prev)} + /> +
+
+ {isJSONInput ? ( +
+ + + +
+ ) : messageInputsLoading ? ( +
+ + Fetching message inputs + +
+ ) : messageInputsError ? ( +
+ Couldn't fetch message inputs, Please switch to JSON format +
+ ) : !messageInputFields.length ? ( +
+ {selectedMessage?.length ? ( +
+
+
{selectedMessage}
+
+ onQuery(queryText)} + btnType="button" + /> +
+ ) : ( +
+ {messagesError ? ( +
+ Couldn't fetch messages, Please switch to JSON format +
+ ) : ( +
- Select a message to query -
+ )} +
+ )} +
+ ) : ( + + )} +
+ ); +}; + +export default QueryContractInputs; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/SearchContract.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/SearchContract.tsx new file mode 100644 index 000000000..7e443384a --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/SearchContract.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import ContractSelected from './ContractSelected'; +import DialogSearchContract from './DialogSearchContract'; +import ContractNotSelected from './ContractNotSelected'; + +interface SearchContractI { + chainID: string; + selectedContract: { address: string; name: string }; + handleSelectContract: (address: string, name: string) => void; +} + +const SearchContract = (props: SearchContractI) => { + const { chainID, handleSelectContract, selectedContract } = props; + + // DEPENDENCIES + const { getChainInfo } = useGetChainInfo(); + const { restURLs } = getChainInfo(chainID); + + // STATE + const [searchDialogOpen, setSearchDialogOpen] = useState(false); + + // CHANGE HANDLER + const handleClose = () => { + setSearchDialogOpen(false); + }; + + return ( + <> +
{ + if (!selectedContract.address) setSearchDialogOpen(true); + }} + > + {selectedContract.address ? ( + setSearchDialogOpen(true)} + /> + ) : ( + + )} +
+ + + ); +}; + +export default SearchContract; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/SearchInputField.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/SearchInputField.tsx new file mode 100644 index 000000000..d32b03ce4 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/SearchInputField.tsx @@ -0,0 +1,37 @@ +import { SEARCH_ICON } from '@/constants/image-names'; +import Image from 'next/image'; +import React from 'react'; + +const SearchInputField = ({ + searchTerm, + setSearchTerm, +}: { + searchTerm: string; + setSearchTerm: (value: string) => void; +}) => { + return ( +
+
+ Search +
+
+ setSearchTerm(e.target.value)} + autoFocus={true} + /> +
+
+ ); +}; + +export default SearchInputField; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/SelectFunds.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/SelectFunds.tsx new file mode 100644 index 000000000..c8dd911e0 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/SelectFunds.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import Fund from './Fund'; +import { ADD_ICON_ROUNDED } from '@/constants/image-names'; +import Image from 'next/image'; + +interface SelectFundsI { + onAddFund: (fund: FundInfo) => void; + funds: FundInfo[]; + assetsList: AssetInfo[]; + onDelete: (index: number) => void; + setFunds: (value: React.SetStateAction) => void; +} + +const SelectFunds = (props: SelectFundsI) => { + const { onAddFund, funds, assetsList, onDelete, setFunds } = props; + const handleAddFund = () => { + onAddFund({ + amount: '', + denom: '', + decimals: 1, + }); + }; + return ( +
+ {assetsList?.length ? ( +
+ {funds.map((fund, index) => ( + onDelete(index)} + index={index} + funds={funds} + setFunds={setFunds} + disableDelete={funds.length === 1} + /> + ))} +
+ +
+
+ ) : ( +
+ - Assets not found, Please select:{' '} + + Provide Assets List + {' '} + option to enter manually - +
+ )} +
+ ); +}; + +export default SelectFunds; diff --git a/frontend/src/app/(routes)/cosmwasm/components/single-contract/TokensList.tsx b/frontend/src/app/(routes)/cosmwasm/components/single-contract/TokensList.tsx new file mode 100644 index 000000000..cc3256a5d --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/single-contract/TokensList.tsx @@ -0,0 +1,61 @@ +import { MenuItem, Select, SelectChangeEvent } from '@mui/material'; +import React from 'react'; +import { customSelectStyles } from '@/utils/commonStyles'; + +interface TokensListI { + assetsList: AssetInfo[]; + denom: string; + index: number; + funds: FundInfo[]; + setFunds: (value: React.SetStateAction) => void; +} + +const TokensList = (props: TokensListI) => { + const { assetsList, denom, index, funds, setFunds } = props; + const handleSelectAsset = (e: SelectChangeEvent) => { + const selectedValue = e.target.value; + const selected = assetsList.find( + (asset) => asset.coinMinimalDenom === selectedValue + ); + + const newFunds = funds.map((value, key) => { + if (index === key) { + value.denom = selectedValue; + value.decimals = selected?.decimals || 1; + } + return value; + }); + setFunds(newFunds); + }; + return ( +
+ +
+ ); +}; +export default TokensList; diff --git a/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxExecuteStatus.tsx b/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxExecuteStatus.tsx new file mode 100644 index 000000000..8119f2c1d --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxExecuteStatus.tsx @@ -0,0 +1,97 @@ +import Copy from '@/components/common/Copy'; +import DialogTxnStatus from '@/components/txn-status-popups/DialogTxnStatus'; +import TxnInfoCard from '@/components/txn-status-popups/TxnInfoCard'; +import TxnStatus from '@/components/txn-status-popups/TxnStatus'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { TxStatus } from '@/types/enums'; +import { TXN_FAILED_ICON, TXN_SUCCESS_ICON } from '@/utils/constants'; +import { parseBalance } from '@/utils/denom'; +import { shortenMsg } from '@/utils/util'; +import Image from 'next/image'; +import React, { useEffect, useMemo, useState } from 'react'; + +const DialogTxExecuteStatus = ({ chainID }: { chainID: string }) => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { explorerTxHashEndpoint, chainName } = getChainInfo(chainID); + const { + decimals = 0, + displayDenom = '', + minimalDenom = '', + } = getDenomInfo(chainID); + const currency = useMemo( + () => ({ + coinMinimalDenom: minimalDenom, + coinDecimals: decimals, + coinDenom: displayDenom, + }), + [minimalDenom, decimals, displayDenom] + ); + + const [open, setOpen] = useState(false); + const txExecuteStatus = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txExecute?.status + ); + const txExecuteHash = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txExecute?.txHash + ); + const txResponse = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txExecute?.txResponse + ); + + const handleClose = () => { + setOpen(false); + }; + + useEffect(() => { + if (txExecuteHash && txExecuteStatus === TxStatus.IDLE) setOpen(true); + }, [txExecuteHash, txExecuteStatus]); + + return open ? ( + +
+ { +
+
+ +
+
+ +
+ {shortenMsg(txResponse?.transactionHash || '', 20) || '-'} +
+ {txResponse?.transactionHash ? ( + + ) : null} +
+ + {txResponse?.fee?.[0] + ? parseBalance( + txResponse?.fee, + currency.coinDecimals, + currency.coinMinimalDenom + ) + : '-'}{' '} + {currency.coinDenom} + +
+
+
+ ) : null; +}; + +export default DialogTxExecuteStatus; diff --git a/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxInstantiateStatus.tsx b/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxInstantiateStatus.tsx new file mode 100644 index 000000000..c811116a0 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxInstantiateStatus.tsx @@ -0,0 +1,122 @@ +import Copy from '@/components/common/Copy'; +import DialogTxnStatus from '@/components/txn-status-popups/DialogTxnStatus'; +import TxnInfoCard from '@/components/txn-status-popups/TxnInfoCard'; +import TxnStatus from '@/components/txn-status-popups/TxnStatus'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { TxStatus } from '@/types/enums'; +import { TXN_FAILED_ICON, TXN_SUCCESS_ICON } from '@/utils/constants'; +import { parseBalance } from '@/utils/denom'; +import { shortenAddress, shortenMsg } from '@/utils/util'; +import Image from 'next/image'; +import React, { useEffect, useMemo, useState } from 'react'; + +const DialogTxInstantiateStatus = ({ chainID }: { chainID: string }) => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { explorerTxHashEndpoint, chainName } = getChainInfo(chainID); + const { + decimals = 0, + displayDenom = '', + minimalDenom = '', + } = getDenomInfo(chainID); + const currency = useMemo( + () => ({ + coinMinimalDenom: minimalDenom, + coinDecimals: decimals, + coinDenom: displayDenom, + }), + [minimalDenom, decimals, displayDenom] + ); + + const [open, setOpen] = useState(false); + const txInstantiateStatus = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txInstantiate?.status + ); + const txHash = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txInstantiate?.txHash + ); + const txResponse = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txInstantiate?.txResponse + ); + + const handleClose = () => { + setOpen(false); + }; + + useEffect(() => { + if (txHash && txInstantiateStatus === TxStatus.IDLE) setOpen(true); + }, [txHash, txInstantiateStatus]); + + return ( + +
+ { +
+
+ +
+
+
+ +
{txResponse?.codeId || '-'}
+ {txResponse?.codeId ? ( + + ) : null} +
+ +
+ {shortenAddress(txResponse?.contractAddress, 12) || '-'} +
+ {txResponse?.contractAddress ? ( + + ) : null} +
+
+ +
+ {shortenMsg(txResponse?.transactionHash || '', 20) || '-'} +
+ {txResponse?.transactionHash ? ( + + ) : null} +
+ +
+ {txResponse?.fee?.[0] + ? parseBalance( + txResponse?.fee, + currency.coinDecimals, + currency.coinMinimalDenom + ) + : '-'}{' '} + {currency.coinDenom} +
+
+ {txResponse?.code === 0 ? null : ( + +
+ {txResponse?.rawLog || '-'} +
+
+ )} +
+
+
+ ); +}; + +export default DialogTxInstantiateStatus; diff --git a/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxUploadCodeStatus.tsx b/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxUploadCodeStatus.tsx new file mode 100644 index 000000000..14f8cce45 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/components/tx-status/DialogTxUploadCodeStatus.tsx @@ -0,0 +1,112 @@ +import Copy from '@/components/common/Copy'; +import DialogTxnStatus from '@/components/txn-status-popups/DialogTxnStatus'; +import TxnInfoCard from '@/components/txn-status-popups/TxnInfoCard'; +import TxnStatus from '@/components/txn-status-popups/TxnStatus'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { TxStatus } from '@/types/enums'; +import { TXN_FAILED_ICON, TXN_SUCCESS_ICON } from '@/utils/constants'; +import { parseBalance } from '@/utils/denom'; +import { shortenMsg } from '@/utils/util'; +import Image from 'next/image'; +import React, { useEffect, useMemo, useState } from 'react'; + +const DialogTxUploadCodeStatus = ({ chainID }: { chainID: string }) => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { explorerTxHashEndpoint, chainName } = getChainInfo(chainID); + const { + decimals = 0, + displayDenom = '', + minimalDenom = '', + } = getDenomInfo(chainID); + const currency = useMemo( + () => ({ + coinMinimalDenom: minimalDenom, + coinDecimals: decimals, + coinDenom: displayDenom, + }), + [minimalDenom, decimals, displayDenom] + ); + + const [open, setOpen] = useState(false); + const txUploadStatus = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txUpload?.status + ); + const txUploadHash = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txUpload?.txHash + ); + const txResponse = useAppSelector( + (state) => state.cosmwasm.chains?.[chainID]?.txUpload?.txResponse + ); + + const handleClose = () => { + setOpen(false); + }; + + useEffect(() => { + if (txUploadHash && txUploadStatus === TxStatus.IDLE) setOpen(true); + }, [txUploadHash]); + + return ( + +
+ { +
+
+ +
+
+ +
{txResponse?.codeId || '-'}
+ {txResponse?.codeId ? ( + + ) : null} +
+ +
+ {shortenMsg(txResponse?.transactionHash || '', 20) || '-'} +
+ {txResponse?.transactionHash ? ( + + ) : null} +
+ +
+ {txResponse?.fee?.[0] + ? parseBalance( + txResponse?.fee, + currency.coinDecimals, + currency.coinMinimalDenom + ) + : '-'}{' '} + {currency.coinDenom} +
+
+ {txResponse?.code === 0 ? null : ( + +
+ {txResponse?.rawLog || '-'} +
+
+ )} +
+
+
+ ); +}; + +export default DialogTxUploadCodeStatus; diff --git a/frontend/src/app/(routes)/cosmwasm/cosmwasm.css b/frontend/src/app/(routes)/cosmwasm/cosmwasm.css new file mode 100644 index 000000000..9d1b1b3f3 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/cosmwasm.css @@ -0,0 +1,50 @@ +.contract-card { + @apply flex justify-start gap-10 p-6 rounded-2xl; + background: linear-gradient( + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% + ); +} + +.search-contract-input { + @apply w-full border-none cursor-text focus:outline-none bg-transparent placeholder:text-[#FFFFFF30] placeholder:font-normal text-[#ffffff] flex-1 text-[14px]; +} + +.search-contract-field { + @apply py-4 px-6 bg-[#FFFFFF05] rounded-full flex w-full; +} +.upload-box-cosmwasm { + @apply rounded-3xl px-6 py-[10.5px] flex items-center cursor-pointer h-[50vh]; + border: 2px dashed #ffffff20; +} + +.query-input, +.execute-input { + @apply relative flex-1 border-[1px] rounded-2xl border-[#ffffff1e] hover:border-[#ffffff50]; +} + +.query-btn, +.execute-btn { + @apply absolute bottom-6 right-6 min-w-[85px]; +} + +.format-json-btn { + @apply h-8 border-[1px] border-[#FFFFFF33] rounded-full p-2 text-[12px] font-extralight top-4 right-4 absolute hover:border-[#ffffff50]; +} + +.query-output-box { + @apply text-[14px] border-[1px] border-[#FFFFFF26] p-6 rounded-2xl min-h-[380px] flex flex-col gap-4; +} + +.provide-funds-input { + @apply border-[1px] border-[#ffffff1e] hover:border-[#ffffff50] rounded-2xl relative; +} +.contract-info-card { + @apply space-y-2 bg-[#FFFFFF0D] rounded-lg p-4 justify-center items-center text-center; + background: linear-gradient( + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% + ); +} diff --git a/frontend/src/app/(routes)/cosmwasm/page.tsx b/frontend/src/app/(routes)/cosmwasm/page.tsx new file mode 100644 index 000000000..1a47558a9 --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/page.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import './cosmwasm.css'; +import Cosmwasm from './Cosmwasm'; + +const page = () => { + return ; +}; + +export default page; diff --git a/frontend/src/app/(routes)/cosmwasm/styles.ts b/frontend/src/app/(routes)/cosmwasm/styles.ts new file mode 100644 index 000000000..fe0fc822d --- /dev/null +++ b/frontend/src/app/(routes)/cosmwasm/styles.ts @@ -0,0 +1,91 @@ +export const customTextFieldStyles = { + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '1px solid transparent', + borderRadius: '12px', + color: 'white', + }, + '& .Mui-focused': { + border: '1px solid #ffffff4a', + borderRadius: '12px', + }, + '& .MuiInputAdornment-root': { + '& button': { + color: 'white', + }, + }, + '& .Mui-disabled': { + WebkitTextFillColor: '#ffffff !important', + }, +}; + +export const assetsDropDownStyle = { + '& .MuiOutlinedInput-input': { + color: 'white', + }, + '& .MuiOutlinedInput-root': { + padding: '0px !important', + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: 'white', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none !important', + }, + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + borderRadius: '8px', +}; + +export const selectTxnStyles = { + '& .MuiOutlinedInput-input': { + color: 'white', + }, + '& .MuiOutlinedInput-root': { + padding: '0px !important', + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: 'white', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none !important', + }, + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + borderRadius: '8px', +}; + +export const queryInputStyles = { + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: 'none', + borderRadius: '16px', + color: 'white', + }, + '& .Mui-focused': { + border: 'none', + borderRadius: '16px', + }, +}; diff --git a/frontend/src/app/(routes)/feegrant/page.tsx b/frontend/src/app/(routes)/feegrant/page.tsx deleted file mode 100644 index 9bc9f7f5d..000000000 --- a/frontend/src/app/(routes)/feegrant/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const page = () => { - return
Feegrant
; -}; - -export default page; diff --git a/frontend/src/app/(routes)/governance/AllProposals.tsx b/frontend/src/app/(routes)/governance/AllProposals.tsx deleted file mode 100644 index c055f82a4..000000000 --- a/frontend/src/app/(routes)/governance/AllProposals.tsx +++ /dev/null @@ -1,259 +0,0 @@ -'use client'; -import React, { useEffect, useState } from 'react'; -import Image from 'next/image'; -import { RootState } from '@/store/store'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { - Chains, - getProposalsInDeposit, - getProposalsInVoting, -} from '@/store/features/gov/govSlice'; -import './style.css'; -import { get } from 'lodash'; -import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; -import messages from '@/utils/messages.json'; -import { CircularProgress } from '@mui/material'; - -type handleOpenOverview = () => void; -type handleSetCurrentOverviewId = (id: number, chainID: string) => void; - -const AllProposals = ({ - isRightBarOpen, - chainIDs, - status, - handleOpenOverview, - handleSetCurrentOverviewId, - currentOverviewId, - handleProposalSelected, - isSelected, -}: { - isRightBarOpen: boolean; - chainIDs: string[]; - status: string; - handleOpenOverview: handleOpenOverview; - handleSetCurrentOverviewId: handleSetCurrentOverviewId; - currentOverviewId: number; - handleProposalSelected: (value: boolean) => void; - isSelected: boolean; -}) => { - const dispatch = useAppDispatch(); - const [selectedChainsProposals, setSelectedChainsProposals] = - useState({}); - - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const activeProposalsLoading = useAppSelector( - (state: RootState) => state.gov.activeProposalsLoading - ); - const depositProposalsLoading = useAppSelector( - (state: RootState) => state.gov.depositProposalsLoading - ); - const getChainName = (chainID: string) => { - let chain: string = ''; - Object.keys(nameToChainIDs).forEach((chainName) => { - if (nameToChainIDs[chainName] === chainID) chain = chainName; - }); - return chain; - }; - - const chainsProposals = useAppSelector( - (state: RootState) => state.gov.chains - ); - - let allProposalsLength = 0; - - if (selectedChainsProposals) { - /* eslint-disable @typescript-eslint/no-unused-vars */ - Object.entries(selectedChainsProposals).map( - ([_chainName, chainProposal]) => { - allProposalsLength += get( - chainProposal, - `${status === 'deposit' ? 'deposit' : 'active'}.proposals.length` - ); - } - ); - } - - useEffect(() => { - chainIDs.forEach((chainID) => { - const allChainInfo = networks[chainID]; - const chainInfo = allChainInfo.network; - const address = allChainInfo?.walletInfo?.bech32Address; - const basicChainInputs = { - baseURL: chainInfo.config.rest, - voter: address, - chainID, - }; - - dispatch(getProposalsInVoting(basicChainInputs)); - dispatch( - getProposalsInDeposit({ - baseURL: chainInfo.config.rest, - chainID, - }) - ); - }); - }, []); - - useEffect(() => { - chainIDs.forEach((chainID) => - setSelectedChainsProposals((selectedChainsProposals) => { - selectedChainsProposals[chainID] = chainsProposals[chainID]; - return selectedChainsProposals; - }) - ); - }, [chainsProposals]); - - return ( -
- {( - status === 'active' - ? activeProposalsLoading === 0 && allProposalsLength === 0 - : depositProposalsLoading === 0 && allProposalsLength === 0 - ) ? ( -
-
-
-
- no action proposals -

- {messages.noProposals} -

-
-
-
-
- ) : null} - - {Object.entries(selectedChainsProposals).map( - ([chainID, chainProposal]) => { - const chainLogo = networks[chainID]?.network?.logos?.menu || ''; - return ( - <> - {get( - chainProposal, - `${ - status === 'deposit' ? 'deposit' : 'active' - }.proposals.length` - ) ? ( -
-
-
- Networks-Logo -

- {getChainName(chainID)} -

-
-
-
- -
- {get( - chainProposal, - `${status === 'deposit' ? 'deposit' : 'active'}.proposals` - ).map((proposal, index) => ( -
{ - handleOpenOverview(); - handleSetCurrentOverviewId( - parseInt(get(proposal, 'proposal_id')), - chainID - ); - handleProposalSelected(true); - }} - className={ - isSelected && - currentOverviewId.toString() === - get(proposal, 'proposal_id') - ? 'proposal proposal-selected' - : 'proposal' - } - key={index} - > -
-
-
-

- {get(proposal, 'proposal_id')} -

-
- -

- {get(proposal, 'content.title') || - get(proposal, 'content.@type')} -

-
-
- {!isRightBarOpen && ( -
-
- Timer-Icon - {status === 'deposit' ? ( -

- Deposit ends in{' '} - {getTimeDifferenceToFutureDate( - get(proposal, 'deposit_end_time') - )} -

- ) : ( -

- Expires in{' '} - {getTimeDifferenceToFutureDate( - get(proposal, 'voting_end_time') - )} -

- )} -
-
- )} -
-
- ))} -
-
- ) : null} - - ); - } - )} - {( - status === 'active' - ? activeProposalsLoading < chainIDs?.length - : depositProposalsLoading < chainIDs?.length - ) ? null : ( -
- -
- )} -
- ); -}; - -export default AllProposals; diff --git a/frontend/src/app/(routes)/governance/Banner.tsx b/frontend/src/app/(routes)/governance/Banner.tsx new file mode 100644 index 000000000..ee3c0bf48 --- /dev/null +++ b/frontend/src/app/(routes)/governance/Banner.tsx @@ -0,0 +1,19 @@ +import './style.css'; +import Image from 'next/image'; + +const Banner = () => { + return ( +
+ info-icon +

Important

+

Voting ends in 03 Days

+
+ ); +}; +export default Banner; diff --git a/frontend/src/app/(routes)/governance/CustomPiechart.tsx b/frontend/src/app/(routes)/governance/CustomPiechart.tsx deleted file mode 100644 index 832b4baaa..000000000 --- a/frontend/src/app/(routes)/governance/CustomPiechart.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; -import { Chart as ChartJS, ArcElement } from 'chart.js'; -import { Pie } from 'react-chartjs-2'; -ChartJS.register(ArcElement); -const CustomPieChart = ({ - value, - color, - label, -}: { - value: number | string; - color: string; - label: string; -}) => { - const data = { - datasets: [ - { - label: label, - data: [100 - Number(value), Number(value)], - backgroundColor: ['#FFFFFF1A', color], - borderWidth: 0, - }, - ], - }; - return ( -
- -
- ); -}; -export default CustomPieChart; diff --git a/frontend/src/app/(routes)/governance/CustomRadioButton.tsx b/frontend/src/app/(routes)/governance/CustomRadioButton.tsx deleted file mode 100644 index 0ed13ee56..000000000 --- a/frontend/src/app/(routes)/governance/CustomRadioButton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; - -const RadioButton = ({ - name, - value, - voteOption, - handleVoteChange, - displayOption, -}: { - name: string; - value: string; - voteOption: string; - handleVoteChange: (value: string) => void; - displayOption: string; -}) => { - return ( - - ); -}; - -export default RadioButton; diff --git a/frontend/src/app/(routes)/governance/DepositPopup.tsx b/frontend/src/app/(routes)/governance/DepositPopup.tsx deleted file mode 100644 index 2adfbe247..000000000 --- a/frontend/src/app/(routes)/governance/DepositPopup.tsx +++ /dev/null @@ -1,211 +0,0 @@ -'use client'; -import React from 'react'; -import { - CircularProgress, - Dialog, - DialogContent, - InputAdornment, - TextField, -} from '@mui/material'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import Image from 'next/image'; -import { RootState } from '@/store/store'; -import './style.css'; - -import useGetTxInputs from '@/custom-hooks/useGetTxInputs'; -import { txDeposit } from '@/store/features/gov/govSlice'; -import { Controller, useForm } from 'react-hook-form'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; -import { TxStatus } from '@/types/enums'; - -const DepositPopup = ({ - chainID, - votingEndsInDays, - denom, - proposalId, - proposalname, - open, - onClose, - networkLogo, -}: { - chainID: string; - votingEndsInDays: string; - denom?: string; - proposalId: number; - proposalname: string; - open: boolean; - onClose: () => void; - networkLogo: string; -}) => { - console.log(denom); - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const allChainInfo = networks[chainID]; - - const { getVoteTxInputs } = useGetTxInputs(); - const dispatch = useAppDispatch(); - - const currency = allChainInfo.network.config.currencies[0]; - const loading = useAppSelector( - (state: RootState) => state.gov.chains?.[chainID]?.tx?.status - ); - - const { - handleSubmit, - control, - formState: { errors }, - } = useForm({ - defaultValues: { - amount: 0, - }, - }); - - const handleClose = () => { - onClose(); - }; - - const handleDeposit = (data: { amount: number }) => { - const { aminoConfig, prefix, rest, feeAmount, address, rpc, minimalDenom } = - getVoteTxInputs(chainID); - console.log(data); - - dispatch( - txDeposit({ - depositer: address, - proposalId: proposalId, - amount: Number(data.amount) * 10 ** currency.coinDecimals, - denom: minimalDenom, - chainID: chainID, - rpc: rpc, - rest: rest, - aminoConfig: aminoConfig, - prefix: prefix, - feeAmount: feeAmount, - feegranter: '', - }) - ); - }; - - return ( - - -
-
- Close -
-
-
- Deposit-Image -
-
-
-
Deposit
-
-
-
-
- logo -

{proposalId}

-
-
{`Deposit period ends in ${votingEndsInDays} `}
-
-
- {proposalname} -
-
-
- -
-
- ( - - {currency?.coinDenom} - - ), - sx: { - input: { - color: 'white', - fontSize: '14px', - padding: 2, - }, - }, - }} - error={!!errors.amount} - helperText={ - errors.amount?.type === 'validate' - ? 'Insufficient balance' - : errors.amount?.message - } - /> - )} - /> -
-
- -
-
-
-
-
-
-
-
-
- ); -}; - -export default DepositPopup; diff --git a/frontend/src/app/(routes)/governance/DepositProposalDetails.tsx b/frontend/src/app/(routes)/governance/DepositProposalDetails.tsx deleted file mode 100644 index 897d19e3b..000000000 --- a/frontend/src/app/(routes)/governance/DepositProposalDetails.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { getLocalTime } from '@/utils/dataTime'; -import { Tooltip } from '@mui/material'; -import { get } from 'lodash'; -import React from 'react'; - -const DepositProposalDetails = ({ - submittedAt, - endsAt, - depositrequired, - proposalNetwork, -}: { - submittedAt: string; - depositrequired: string; - endsAt: string; - proposalNetwork: string; -}) => { - const proposalInfo = useAppSelector( - (state: RootState) => state.gov.proposalDetails - ); - return ( -
-
-
-

Submitted Time

- -

{submittedAt}

-
-
-
-

Deposit Period Ends

- -

{endsAt}

-
-
-
-

Deposit Required

-

{depositrequired}

-
-
-

Proposal Network

-

{proposalNetwork}

-
-
-
- ); -}; - -export default DepositProposalDetails; diff --git a/frontend/src/app/(routes)/governance/DepositProposalInfo.tsx b/frontend/src/app/(routes)/governance/DepositProposalInfo.tsx deleted file mode 100644 index 2d5a6f6c0..000000000 --- a/frontend/src/app/(routes)/governance/DepositProposalInfo.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { parseBalance } from '@/utils/denom'; -import Image from 'next/image'; -import React, { useEffect, useState } from 'react'; - -const DepositProposalInfo = ({ chainID }: { chainID: string }) => { - const currency = useAppSelector( - (state: RootState) => - state.wallet.networks[chainID]?.network.config.currencies[0] - ); - const depositParams = useAppSelector( - (state: RootState) => state.gov.chains[chainID]?.depositParams.params - ); - const proposalInfo = useAppSelector( - (state: RootState) => state.gov.proposalDetails - ); - const [minDeposit, setMinDeposit] = useState(0); - const [totalDeposit, setTotalDeposit] = useState(0); - const [depositPercent, setDepositPercent] = useState(0); - useEffect(() => { - if ( - depositParams?.min_deposit?.length && - proposalInfo?.total_deposit?.length - ) { - const min_deposit = parseBalance( - depositParams.min_deposit, - currency.coinDecimals, - currency.coinMinimalDenom - ); - const total_deposit = parseBalance( - proposalInfo.total_deposit, - currency.coinDecimals, - currency.coinMinimalDenom - ); - const deposit_percent = Math.floor((total_deposit / min_deposit) * 100); - setMinDeposit(min_deposit); - setTotalDeposit(total_deposit); - setDepositPercent(deposit_percent); - } - }, [depositParams, proposalInfo]); - - return ( -
-
-
-
-
-
- Vote-Icon -
-

Deposit Collected

-
-
-

- {totalDeposit}/{minDeposit} {currency.coinDenom} -

-
-
-
-
-
-
-
-
{depositPercent}%
-
-
-
-
-
-
-
-
-
- ); -}; - -export default DepositProposalInfo; diff --git a/frontend/src/app/(routes)/governance/GovAds.tsx b/frontend/src/app/(routes)/governance/GovAds.tsx deleted file mode 100644 index dc594260b..000000000 --- a/frontend/src/app/(routes)/governance/GovAds.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useState } from 'react'; -import Image from 'next/image'; - -const GovAds = () => { - const [adOpen, setAdOpen] = useState(true); - - return ( -
- {adOpen ? ( -
-
- setAdOpen(false)} - src="/close.svg" - width={30} - height={30} - alt="Close ad" - /> -
- - Advertisement-Image -
- ) : null} -
- ); -}; - -export default GovAds; diff --git a/frontend/src/app/(routes)/governance/GovPage.tsx b/frontend/src/app/(routes)/governance/GovPage.tsx deleted file mode 100644 index a6fa824d3..000000000 --- a/frontend/src/app/(routes)/governance/GovPage.tsx +++ /dev/null @@ -1,88 +0,0 @@ -'use client'; -import React, { useEffect, useState } from 'react'; -import Proposals from './Proposals'; -import AllProposals from './AllProposals'; -import RightOverview from './RightOverview'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { getBalances } from '@/store/features/bank/bankSlice'; - -const GovPage = ({ chainIDs }: { chainIDs: string[] }) => { - const [proposalState, setProposalState] = useState('active'); - const [isOverviewOpen, setIsOverviewOpen] = useState(false); - const [currentOverviewId, setCurrentOverviewId] = useState(0); - const [chainID, setChainID] = useState(''); - const [isSelected, setIsSelected] = React.useState(false); - const networks = useAppSelector((state) => state.wallet.networks); - const dispatch = useAppDispatch(); - - useEffect(() => { - chainIDs.forEach((chainID) => { - const allChainInfo = networks[chainID]; - const chainInfo = allChainInfo.network; - const address = allChainInfo?.walletInfo?.bech32Address; - const basicChainInputs = { - baseURL: chainInfo.config.rest, - address, - chainID, - }; - - dispatch(getBalances(basicChainInputs)); - }); - }, []); - - const handleProposalSelected = (value: boolean) => { - setIsSelected(value); - }; - - const handleChangeProposalState = (status: string) => { - setProposalState(status); - setIsOverviewOpen(false); - setCurrentOverviewId(0); - }; - - const handleOpenOverview = () => { - setIsOverviewOpen(true); - }; - - const handleCloseOverview = () => { - setIsOverviewOpen(false); - }; - - const handleSetCurrentOverviewId = (id: number, chainID: string) => { - setCurrentOverviewId(id); - setChainID(chainID); - }; - - return ( -
-
- - -
- {(isOverviewOpen && ( - - )) || - null} -
- ); -}; - -export default GovPage; diff --git a/frontend/src/app/(routes)/governance/ProposalDetailsCard.tsx b/frontend/src/app/(routes)/governance/ProposalDetailsCard.tsx deleted file mode 100644 index 909ae199c..000000000 --- a/frontend/src/app/(routes)/governance/ProposalDetailsCard.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -const ProposalDetailsCard = ({ - submittedAt, - endsAt, - depositrequired, - proposalNetwork, -}: { - submittedAt: string; - depositrequired: string; - endsAt: string; - proposalNetwork: string; -}) => { - return ( -
-
-
-

Submitted Time

-

{submittedAt}

-
-
-

Deposit Period Ends

-

{endsAt}

-
-
-

Deposit Required

-

{depositrequired}

-
-
-

Proposal Network

-

{proposalNetwork}

-
-
-
- ); -}; - -export default ProposalDetailsCard; diff --git a/frontend/src/app/(routes)/governance/ProposalDetailsVoteCard.tsx b/frontend/src/app/(routes)/governance/ProposalDetailsVoteCard.tsx deleted file mode 100644 index 99ec1e781..000000000 --- a/frontend/src/app/(routes)/governance/ProposalDetailsVoteCard.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client'; -import { getLocalTime } from '@/utils/dataTime'; -import { Tooltip } from '@mui/material'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; - -import React from 'react'; -import { get } from 'lodash'; - -const ProposalDetailsVoteCard = ({ - createdAt, - startedAt, - endsAt, - proposalNetwork, - - depositamount, -}: { - createdAt: string; - startedAt: string; - endsAt: string; - proposalNetwork: string; - createdby: string; - depositamount: string; -}) => { - const proposalInfo = useAppSelector( - (state: RootState) => state.gov.proposalDetails - ); - return ( -
-
-
-

Proposal created

- -

{createdAt}

-
-
- -
-

Voting Started

- -

{startedAt}

-
-
-
-

Voting ends in

- -

{endsAt}

-
-
-
-

Proposal Network

-

{proposalNetwork}

-
- {/*
-

Proposal Created by

-

{createdby}

-
*/} -
-

Deposit Amount

-

{depositamount}

-
-
-
- ); -}; - -export default ProposalDetailsVoteCard; diff --git a/frontend/src/app/(routes)/governance/ProposalOverviewVote.tsx b/frontend/src/app/(routes)/governance/ProposalOverviewVote.tsx deleted file mode 100644 index f52ec54c8..000000000 --- a/frontend/src/app/(routes)/governance/ProposalOverviewVote.tsx +++ /dev/null @@ -1,513 +0,0 @@ -'use client'; -import React, { useState, useEffect } from 'react'; -import Image from 'next/image'; -import Link from 'next/link'; -import ProposalViewRaw from './ProposalViewRaw'; -import { RootState } from '@/store/store'; -import CustomPieChart from './CustomPiechart'; -import './style.css'; -import ProposalDetailsVoteCard from './ProposalDetailsVoteCard'; -import VotePopup from './VotePopup'; - -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { - getProposal, - getGovTallyParams, - getProposalTally, - getDepositParams, -} from '@/store/features/gov/govSlice'; -import { get, parseInt } from 'lodash'; -import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; -import { parseBalance } from '@/utils/denom'; - -import { formatCoin } from '@/utils/util'; -import { getPoolInfo } from '@/store/features/staking/stakeSlice'; -import { Tooltip } from '@mui/material'; -import DepositProposalDetails from './DepositProposalDetails'; -import DepositProposalInfo from './DepositProposalInfo'; -import DepositPopup from './DepositPopup'; -import { setSelectedNetwork } from '@/store/features/common/commonSlice'; -import ProposalProjection from './ProposalProjection'; -import TopNav from '@/components/TopNav'; -import { useRemark } from 'react-remark'; - -const emptyTallyResult = { - yes: '', - abstain: '', - no: '', - no_with_veto: '', - proposal_id: '', -}; - -const ProposalOverviewVote = ({ - chainName, - proposalId, -}: { - chainName: string; - proposalId: number; -}) => { - const [depositRequired, setDepositRequired] = useState(0); - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - - const chainID = nameToChainIDs[chainName]; - const [proposalMarkdown, setProposalMarkdown] = useRemark(); - - const dispatch = useAppDispatch(); - const proposalInfo = useAppSelector( - (state: RootState) => state.gov.proposalDetails - ); - const isStatusVoting = - get(proposalInfo, 'status') === 'PROPOSAL_STATUS_VOTING_PERIOD'; - const networkLogo = useAppSelector( - (state: RootState) => state.wallet.networks[chainID]?.network.logos.menu - ); - const currency = useAppSelector( - (state: RootState) => - state.wallet.networks[chainID]?.network.config.currencies[0] - ); - const depositParams = useAppSelector( - (state: RootState) => state.gov.chains[chainID]?.depositParams.params - ); - - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const tallyResult = useAppSelector((state: RootState) => { - if ( - state.gov.chains[chainID] && - state.gov.chains[chainID].tally && - state.gov.chains[chainID].tally.proposalTally - ) { - return state.gov.chains[chainID].tally.proposalTally[proposalId]; - } - }); - const poolInfo = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.pool - ); - const tallyParams = useAppSelector( - (state: RootState) => - state.gov.chains[chainID]?.tallyParams.params.tally_params - ); - const quorumRequired = (parseFloat(tallyParams?.quorum) * 100).toFixed(1); - - const totalVotes = - Number(get(tallyResult, 'yes')) + - Number(get(tallyResult, 'no')) + - Number(get(tallyResult, 'abstain')) + - Number(get(tallyResult, 'no_with_veto')); - - const getVotesPercentage = (votesCount: number) => { - return ( - (votesCount && - totalVotes && - ((votesCount / totalVotes) * 100).toFixed(2)) || - 0 - ); - }; - - const data = [ - { - value: getVotesPercentage(Number(get(tallyResult, 'yes', 0))) || 0, - color: '#4AA29C', - label: 'Yes', - }, - { - value: getVotesPercentage(Number(get(tallyResult, 'no', 0))) || 0, - color: '#E57575', - label: 'No', - }, - { - value: getVotesPercentage(Number(get(tallyResult, 'abstain', 0))) || 0, - color: '#EFFF34', - label: 'Abstain', - }, - { - value: - getVotesPercentage(Number(get(tallyResult, 'no_with_veto', 0))) || 0, - color: '#5885AF', - label: 'Veto', - }, - ]; - - const getChainName = (chainID: string) => { - let chain: string = ''; - Object.keys(nameToChainIDs).forEach((chainName) => { - if (nameToChainIDs[chainName] === chainID) chain = chainName; - }); - return chain; - }; - const [showRawData, setShowRawData] = useState(false); - const [isVotePopupOpen, setIsVotePopupOpen] = useState(false); - const handleCloseVotePopup = () => { - setIsVotePopupOpen(false); - }; - - const [isDepositPopupOpen, setIsDepositPopupOpen] = useState(false); - const handleCloseDepositPopup = () => { - setIsDepositPopupOpen(false); - }; - - useEffect(() => { - const allChainInfo = networks[chainID]; - const chainInfo = allChainInfo?.network; - dispatch( - getProposal({ - chainID, - baseURL: chainInfo?.config.rest, - proposalId: proposalId, - }) - ); - - dispatch( - getProposalTally({ - baseURL: chainInfo?.config.rest, - proposalId, - chainID: chainID, - }) - ); - - dispatch( - getPoolInfo({ - baseURL: chainInfo?.config.rest, - chainID: chainID, - }) - ); - - dispatch( - getDepositParams({ - baseURL: chainInfo?.config.rest, - chainID: chainID, - }) - ); - - dispatch(getGovTallyParams({ chainID, baseURL: chainInfo?.config.rest })); - }, []); - - const [quorumPercent, setQuorumPercent] = useState('0'); - useEffect(() => { - if (poolInfo?.bonded_tokens) { - const value = totalVotes / parseInt(poolInfo.bonded_tokens); - setQuorumPercent((value * 100).toFixed(1)); - } - }, [poolInfo]); - - useEffect(() => { - if ( - depositParams?.min_deposit?.length && - proposalInfo?.total_deposit?.length - ) { - const min_deposit = parseBalance( - depositParams.min_deposit, - currency.coinDecimals, - currency.coinMinimalDenom - ); - const total_deposit = parseBalance( - proposalInfo.total_deposit, - currency.coinDecimals, - currency.coinMinimalDenom - ); - // const deposit_percent = Math.floor((total_deposit / min_deposit) * 100); - const deposit_required = min_deposit - total_deposit; - setDepositRequired(deposit_required); - } - }, [depositParams, proposalInfo]); - - useEffect(() => { - if (chainName?.length) { - dispatch(setSelectedNetwork({ chainName: chainName })); - } - }, [chainName]); - - useEffect(() => { - const proposalDescription = get(proposalInfo, 'content.description', ''); - setProposalMarkdown(proposalDescription.replace(/\\n/g, '\n')); - }, [proposalInfo]); - - return ( -
-
-
-
-
Governance
- -
- - BackArrow - - -
Proposal Overview
-
-
-
-
-
-
- Network-Logo -

- # {get(proposalInfo, 'proposal_id')} -

-
-
- -
-
-
-
-
- {get(proposalInfo, 'content.title') || - get(proposalInfo, 'content.@type')} -
- -
- {proposalMarkdown} -
-
-
setShowRawData(true)} - > - View Raw -
- {showRawData && ( - setShowRawData(false)} - proposals={proposalInfo} - /> - )} -
- - - - -
- - {isStatusVoting ? ( -
-
-
- - -
-
-
-
-
-
- Vote-Icon -

- Proposal Projection -

-
-
- = - parseFloat(quorumRequired) - } - quorumPercent={quorumPercent} - quorumRequired={quorumRequired} - totalVotes={totalVotes} - tallyResult={tallyResult || emptyTallyResult} - /> -
-
-
- - {/* TODO: Write explaination for proposal Rejected */} -
- {''} -
-
-
-
-
-
-
-
-
Quorum
- - {quorumPercent ? ( - -
-
-
-
-
-
- Turnout{' '} -
- -
- {quorumPercent}% -
-
-
/
-
-
- Quorum{' '} -
- -
- {quorumRequired}% -
-
-
- -
-
-
-
-
-
-
-
- ) : null} -
- -
- {data.map((item, index) => ( -
- -
{`${Math.floor( - parseFloat(String(item.value)) - )}% ${item.label}`}
-
- ))} -
-
-
-
- -
-
-
- ) : ( -
-
-
- - -
- -
-
-
- -
-
{' '} -
- )} -
-
- ); -}; - -export default ProposalOverviewVote; diff --git a/frontend/src/app/(routes)/governance/ProposalPage.tsx b/frontend/src/app/(routes)/governance/ProposalPage.tsx deleted file mode 100644 index 0f7f7faac..000000000 --- a/frontend/src/app/(routes)/governance/ProposalPage.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { useParams } from 'next/navigation'; -import React from 'react'; -import ProposalOverviewVote from './ProposalOverviewVote'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; - -const ProposalPage = () => { - const params = useParams(); - const { network, proposalId: id } = params; - const chainName = typeof network === 'string' ? network : ''; - const proposalId = typeof id === 'string' ? id : ''; - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const validChain = Object.keys(nameToChainIDs).some( - (chain) => chainName.toLowerCase() === chain.toLowerCase() - ); - const validId = () => { - const parsedValue = parseInt(proposalId, 10); - return !isNaN(parsedValue) && Number.isInteger(parsedValue); - }; - return ( -
- {validChain && validId() ? ( - - ) : null} - {!validChain ? ( -
- Chain not found -
- ) : !validId() ? ( -
- Invalid Proposal ID -
- ) : null} -
- ); -}; - -export default ProposalPage; diff --git a/frontend/src/app/(routes)/governance/ProposalProjection.tsx b/frontend/src/app/(routes)/governance/ProposalProjection.tsx index ce382216f..05e7ddbf6 100644 --- a/frontend/src/app/(routes)/governance/ProposalProjection.tsx +++ b/frontend/src/app/(routes)/governance/ProposalProjection.tsx @@ -1,3 +1,5 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { TxStatus } from '@/types/enums'; import { get, sum } from 'lodash'; import React from 'react'; @@ -15,6 +17,7 @@ interface ProposalProjectionProps { tallyResult: TallyResult; quorumRequired: string; quorumPercent: string; + chainID: string; } const REJECTED = 'Will Be Rejected'; @@ -26,22 +29,34 @@ const ProposalProjection = ({ tallyResult, quorumRequired, quorumPercent, + chainID, }: ProposalProjectionProps) => { const getVotesProportion = (votesCount: number, totalVotes: number) => { return ( (votesCount && totalVotes && (votesCount / totalVotes).toFixed(2)) || 0 ); }; - const yes = Number(get(tallyResult, 'yes')); - const no = Number(get(tallyResult, 'no')); - const abstian = Number(get(tallyResult, 'abstain')); - const no_with_veto = Number(get(tallyResult, 'no_with_veto')); + const yes = Number(get(tallyResult, 'yes', get(tallyResult, 'yes_count'))); + const no = Number(get(tallyResult, 'no', get(tallyResult, 'no_count'))); + const abstian = Number( + get(tallyResult, 'abstain', get(tallyResult, 'abstain_count')) + ); + const no_with_veto = Number( + get(tallyResult, 'no_with_veto', get(tallyResult, 'no_with_veto_count')) + ); const abstain_proportion = Number(getVotesProportion(abstian, totalVotes)); const veto_proportion = Number(getVotesProportion(no_with_veto, totalVotes)); const yes_proportion_without_abstain = Number( getVotesProportion(yes, sum([yes, no, no_with_veto])) ); + const tallyParamsStatus = useAppSelector( + (state) => state.gov.chains?.[chainID]?.tallyParams?.status + ); + const proposalTallyStatus = useAppSelector( + (state) => state.gov.chains?.[chainID]?.tally.status + ); + const getProposalStatus = (): { status: string; reason: string } => { let reason = '-'; let status = '-'; @@ -76,18 +91,65 @@ const ProposalProjection = ({ const { reason, status } = getProposalStatus(); return ( -
- {status === PASSED ? ( -
{PASSED}
- ) : null} - {status === REJECTED ? ( -
-
{status}
-
-
  • {reason}
  • +
    +
    +

    Proposal Projection

    +
    +
    + {proposalTallyStatus === TxStatus.PENDING || + tallyParamsStatus === TxStatus.PENDING ? ( + <> +
    +
    +
    +
    +
    + + ) : ( + <> +
    +
    + {status === PASSED ? ( +
    + {PASSED} +
    + ) : null} + {status === REJECTED ? ( +
    +
    {status}
    +
    +
  • {reason}
  • +
    +
    + ) : null} +
    +
    +
    +
    +

    + {Number(quorumPercent) > 0 ? ( + {quorumPercent} + ) : null}{' '} + % +

    +

    + Turnout +

    +
    +
    +

    + {Number(quorumRequired) > 0 ? ( + {quorumRequired} + ) : null}{' '} + % +

    +

    + Quorum +

    +
    -
    - ) : null} + + )}
    ); }; diff --git a/frontend/src/app/(routes)/governance/ProposalViewRaw.tsx b/frontend/src/app/(routes)/governance/ProposalViewRaw.tsx deleted file mode 100644 index 87ce33348..000000000 --- a/frontend/src/app/(routes)/governance/ProposalViewRaw.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; -import { CLOSE_ICON_PATH } from '@/utils/constants'; -import { Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React from 'react'; - -import { GovProposal } from '@/types/gov'; - -interface ProposalViewRawProps { - open: boolean; - onClose: () => void; - proposals: GovProposal; -} - -const ProposalViewRaw: React.FC = (props) => { - const { open, onClose, proposals } = props; - const handleClose = () => { - onClose(); - }; - return ( - handleClose()} - maxWidth="lg" - PaperProps={{ - sx: dialogBoxPaperPropStyles, - }} - > - -
    -
    -
    { - handleClose(); - }} - > - Close -
    -
    -
    -
    Raw
    -
    - {proposals ? ( -
    {JSON.stringify(proposals, undefined, 2)}
    - ) : ( -
    - NO DATA -
    - )} -
    -
    -
    -
    -
    - ); -}; -export default ProposalViewRaw; diff --git a/frontend/src/app/(routes)/governance/Proposals.tsx b/frontend/src/app/(routes)/governance/Proposals.tsx deleted file mode 100644 index e65713f5a..000000000 --- a/frontend/src/app/(routes)/governance/Proposals.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; - -import React from 'react'; -import './style.css'; -import TopNav from '@/components/TopNav'; - -type ProposalStatusUpdate = (status: string) => void; - -const Proposals = ({ - handleChangeProposalState, - proposalStatus, -}: { - handleChangeProposalState: ProposalStatusUpdate; - proposalStatus: string; -}) => { - return ( -
    -
    -
    Governance
    - - -
    - -
    -
    Proposals
    -
    - - - -
    -
    -
    - ); -}; - -export default Proposals; diff --git a/frontend/src/app/(routes)/governance/RightOverview.tsx b/frontend/src/app/(routes)/governance/RightOverview.tsx deleted file mode 100644 index b29c4df65..000000000 --- a/frontend/src/app/(routes)/governance/RightOverview.tsx +++ /dev/null @@ -1,421 +0,0 @@ -'use client'; -import React, { useEffect, useState } from 'react'; -import Image from 'next/image'; -import Link from 'next/link'; -import CustomPieChart from './CustomPiechart'; -import './style.css'; -import VotePopup from './VotePopup'; -import { CircularProgress, Tooltip } from '@mui/material'; -import { RootState } from '@/store/store'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { getGovTallyParams, getProposal } from '@/store/features/gov/govSlice'; -import { get } from 'lodash'; -import { - getTimeDifference, - getTimeDifferenceToFutureDate, -} from '@/utils/dataTime'; -import DepositPopup from './DepositPopup'; -import { getPoolInfo } from '@/store/features/staking/stakeSlice'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { deepPurple } from '@mui/material/colors'; -import DepositProposalInfo from './DepositProposalInfo'; -import DepositProposalDetails from './DepositProposalDetails'; -import { formatCoin } from '@/utils/util'; - -type handleCloseOverview = () => void; - -const RightOverview = ({ - proposalId, - handleCloseOverview, - chainID, - status, - handleProposalSelected, -}: { - proposalId: number; - handleCloseOverview: handleCloseOverview; - chainID: string; - status: string; - handleProposalSelected: (value: boolean) => void; -}) => { - const dispatch = useAppDispatch(); - const proposalInfo = useAppSelector( - (state: RootState) => state.gov.proposalDetails - ); - const networkLogo = useAppSelector( - (state: RootState) => state.wallet.networks[chainID]?.network.logos.menu - ); - - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const tallyResult = useAppSelector( - (state: RootState) => - state.gov.chains[chainID].tally.proposalTally[proposalId] - ); - - const isStatusVoting = - get(proposalInfo, 'status') === 'PROPOSAL_STATUS_VOTING_PERIOD'; - const { getChainInfo, isFeeAvailable } = useGetChainInfo(); - const isFeeEnough = isFeeAvailable(chainID); - const { chainName } = getChainInfo(chainID); - - useEffect(() => { - const allChainInfo = networks[chainID]; - const chainInfo = allChainInfo.network; - dispatch( - getProposal({ - chainID, - baseURL: chainInfo.config.rest, - proposalId: proposalId, - }) - ); - dispatch( - getGovTallyParams({ - chainID, - baseURL: chainInfo.config.rest, - }) - ); - dispatch( - getPoolInfo({ - baseURL: chainInfo.config.rest, - chainID: chainID, - }) - ); - }, [proposalId]); - - const poolInfo = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.pool - ); - const tallyParams = useAppSelector( - (state: RootState) => - state.gov.chains[chainID]?.tallyParams.params.tally_params - ); - const proposalLoadingStatus = useAppSelector( - (state: RootState) => state.gov.proposalInfo.status - ); - const quorumRequired = (parseFloat(tallyParams.quorum) * 100).toFixed(1); - - const totalVotes = - Number(get(tallyResult, 'yes')) + - Number(get(tallyResult, 'no')) + - Number(get(tallyResult, 'abstain')) + - Number(get(tallyResult, 'no_with_veto')); - - const getVotesPercentage = (votesCount: number) => { - return ((votesCount / totalVotes) * 100).toFixed(2); - }; - const maxCharacters = 300; - const truncatedDescription = get( - proposalInfo, - 'content.description', - '' - ).slice(0, maxCharacters); - const isDescriptionTruncated = - truncatedDescription.length < - get(proposalInfo, 'content.description', '').length; - - const data = [ - { - value: getVotesPercentage(Number(get(tallyResult, 'yes'))), - color: '#4AA29C', - label: 'Yes', - }, - { - value: getVotesPercentage(Number(get(tallyResult, 'no'))), - color: '#E57575', - label: 'No', - }, - { - value: getVotesPercentage(Number(get(tallyResult, 'abstain'))), - color: '#EFFF34', - label: 'Abstain', - }, - { - value: getVotesPercentage(Number(get(tallyResult, 'no_with_veto'))), - color: '#5885AF', - label: 'Veto', - }, - ]; - - const proposalSubmittedOn = getTimeDifference( - get(proposalInfo, 'submit_time') - ); - const [depositRequired] = useState(0); - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const [isVotePopupOpen, setIsVotePopupOpen] = useState(false); - const handleCloseVotePopup = () => { - setIsVotePopupOpen(false); - }; - const getChainName = (chainID: string) => { - let chain: string = ''; - Object.keys(nameToChainIDs).forEach((chainName) => { - if (nameToChainIDs[chainName] === chainID) chain = chainName; - }); - return chain; - }; - const currency = useAppSelector( - (state: RootState) => - state.wallet.networks[chainID]?.network.config.currencies[0] - ); - const [isDepositPopupOpen, setIsDepositPopupOpen] = useState(false); - const handleCloseDepositPopup = () => { - setIsDepositPopupOpen(false); - }; - - const handleCloseClick = () => { - handleCloseOverview(); - }; - - const [quorumPercent, setQuorumPercent] = useState('0'); - useEffect(() => { - if (poolInfo?.bonded_tokens) { - const value = totalVotes / parseInt(poolInfo.bonded_tokens); - setQuorumPercent((value * 100).toFixed(1)); - } - }, [poolInfo]); - - return ( -
    -
    -
    -
    Proposal Overview
    - Close icon { - handleCloseClick(); - handleProposalSelected(false); - }} - /> -
    - {proposalLoadingStatus !== 'pending' ? ( - <> -
    -
    -
    -
    -
    -
    - Network Logo - -

    - # {get(proposalInfo, 'proposal_id')} -

    - {/*

    */} -
    -
    - {status === 'active' ? ( - <> - {`Voting ends in ${getTimeDifferenceToFutureDate( - get(proposalInfo, 'voting_end_time') - )}`} - - ) : ( - <> - {`Deposit period ends in ${getTimeDifferenceToFutureDate( - get(proposalInfo, 'deposit_end_time') - )}`} - - )} -
    -
    -
    - {get(proposalInfo, 'content.title') || - get(proposalInfo, 'content.@type')} -
    -
    - -
    -
    - {truncatedDescription} - {isDescriptionTruncated && '...'} -
    -
    - -
    - - View Full Proposal - -
    -
    -
    -
    -
    - - - -
    - - {isStatusVoting ? ( -
    -
    -
    -
    -
    Quorum
    - - {quorumPercent ? ( - -
    -
    -
    -
    -
    -
    - Turnout{' '} -
    - -
    - {quorumPercent}% -
    -
    -
    /
    -
    -
    - Quorum{' '} -
    - -
    - {quorumRequired}% -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - ) : null} -
    - -
    - {data.map((item, index) => ( -
    - -
    {`${Math.floor( - parseFloat(item.value) - )}% ${item.label}`}
    -
    - ))} -
    -
    -
    - Submitted on {proposalSubmittedOn} -
    -
    - Quorum reached icon -
    - {parseFloat(quorumPercent) >= - parseFloat(quorumRequired) - ? 'Quorum Reached' - : 'Quorum Not Reached'} -
    -
    -
    -
    -
    -
    - ) : ( -
    -
    - -
    -
    - -
    -
    - )} -
    -
    - - ) : ( -
    - -
    - )} -
    -
    - ); -}; - -export default RightOverview; diff --git a/frontend/src/app/(routes)/governance/VotePopup.tsx b/frontend/src/app/(routes)/governance/VotePopup.tsx deleted file mode 100644 index 552280191..000000000 --- a/frontend/src/app/(routes)/governance/VotePopup.tsx +++ /dev/null @@ -1,213 +0,0 @@ -'use client'; -import React, { useState } from 'react'; -import Image from 'next/image'; -import './style.css'; -import RadioButton from './CustomRadioButton'; -import { CircularProgress, Dialog, DialogContent } from '@mui/material'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { txVote } from '@/store/features/gov/govSlice'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; -import { RootState } from '@/store/store'; -import { TxStatus } from '@/types/enums'; - -interface VoteOptionNumber { - [key: string]: number; -} - -const voteOptionNumber: VoteOptionNumber = { - yes: 1, - no: 3, - abstain: 2, - veto: 4, -}; - -const VotePopup = ({ - votingEndsInDays, - proposalId, - proposalname, - chainID, - open, - onClose, - networkLogo, -}: { - votingEndsInDays: string; - proposalId: number; - proposalname: string; - chainID: string; - open: boolean; - onClose: () => void; - networkLogo: string; -}) => { - const [voteOption, setVoteOption] = useState(''); - - const handleVoteChange = (option: string) => { - setVoteOption(option); - }; - - const { getChainInfo, getDenomInfo } = useGetChainInfo(); - - const handleClose = () => { - onClose(); - }; - - const loading = useAppSelector( - (state: RootState) => state.gov.chains?.[chainID]?.tx?.status - ); - - const dispatch = useAppDispatch(); - - const handleVote = () => { - const { address, aminoConfig, feeAmount, prefix, rest, rpc } = - getChainInfo(chainID); - const { minimalDenom } = getDenomInfo(chainID); - - dispatch( - txVote({ - voter: address, - proposalId: proposalId, - option: voteOptionNumber[voteOption], - denom: minimalDenom, - chainID: chainID, - rpc: rpc, - rest: rest, - aminoConfig: aminoConfig, - prefix: prefix, - feeAmount: feeAmount, - feegranter: '', - justification: '', - }) - ); - }; - - return ( - - -
    -
    - Close -
    -
    -
    - Vote-Image -
    -
    -
    -
    Vote
    -
    -
    -
    -
    -
    - logo -

    #{proposalId}

    -
    -
    - {`Voting ends in ${votingEndsInDays}`} -
    -
    -
    - {proposalname} -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - ); -}; - -export default VotePopup; diff --git a/frontend/src/app/(routes)/governance/[network]/[proposalId]/SingleProposal.tsx b/frontend/src/app/(routes)/governance/[network]/[proposalId]/SingleProposal.tsx new file mode 100644 index 000000000..cb3e48e7c --- /dev/null +++ b/frontend/src/app/(routes)/governance/[network]/[proposalId]/SingleProposal.tsx @@ -0,0 +1,654 @@ +import React, { useEffect, useState } from 'react'; +import '../../style.css'; +import Image from 'next/image'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { useRemark } from 'react-remark'; +import { RootState } from '@/store/store'; +import { getPoolInfo } from '@/store/features/staking/stakeSlice'; +import { + getProposal, + getGovTallyParams, + getProposalTally, + getDepositParams, + getVotes, +} from '@/store/features/gov/govSlice'; +import { get } from 'lodash'; +import { getLocalTime, getTimeDifferenceToFutureDate } from '@/utils/dataTime'; +import Vote from '../../gov-dashboard/Vote'; +import ProposalProjection from '../../ProposalProjection'; +import { Tooltip } from '@mui/material'; +import DialogDeposit from '../../popups/DialogDeposit'; +import CustomButton from '@/components/common/CustomButton'; +import SingleProposalLoading from '../../loaders/SingleProposalLoading'; +import { useRouter } from 'next/navigation'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { formatAmount } from '@/utils/util'; +import DepositCollected from '../../utils-components/DepositCollected'; +import { PROPOSAL_STATUS_VOTING_PERIOD } from '@/utils/constants'; +import { TxStatus } from '@/types/enums'; +import useAddressConverter from '@/custom-hooks/useAddressConverter'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; + +const emptyTallyResult = { + yes: '', + abstain: '', + no: '', + no_with_veto: '', + proposal_id: '', +}; + +interface SingleProposalProps { + chainID: string; + proposalID: string; +} + +const SingleProposal: React.FC = ({ + chainID, + proposalID, +}) => { + const [showFullText, setShowFullText] = useState(false); + const [proposalMarkdown, setProposalMarkdown] = useRemark(); + const [contentLength, setContentLength] = useState(0); + const [quorumPercent, setQuorumPercent] = useState('0'); + const [depositDialogOpen, setDepositDialogOpen] = useState(false); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); + const showAuthzAlert = useGetShowAuthzAlert(); + + + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { convertAddress } = useAddressConverter(); + const { + restURLs: baseURLs, + baseURL, + govV1, + chainName, + chainLogo, + address, + } = getChainInfo(chainID); + const authzGranterAddress = convertAddress(chainID, authzAddress); + + const proposalInfo = useAppSelector( + (state: RootState) => state.gov.proposalDetails + ); + const isStatusVoting = + get(proposalInfo, 'status') === PROPOSAL_STATUS_VOTING_PERIOD; + const poolInfo = useAppSelector( + (state: RootState) => state.staking.chains[chainID]?.pool + ); + const tallyParams = useAppSelector( + (state: RootState) => + state.gov.chains[chainID]?.tallyParams.params.tally_params + ); + const quorumRequired = (parseFloat(tallyParams?.quorum) * 100).toFixed(1); + const tallyResult = useAppSelector( + (state: RootState) => + state.gov.chains[chainID]?.tally?.proposalTally?.[proposalID] + ); + const proposalStatus = useAppSelector( + (state) => state.gov.proposalInfo.status + ); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const totalVotes = ['yes', 'no', 'abstain', 'no_with_veto'].reduce( + (sum, key) => + sum + + Number(get(tallyResult, key, get(tallyResult, `${key}_count`)) || 0), + 0 + ); + const { decimals, displayDenom } = getDenomInfo(chainID); + + const fetchProposalData = () => { + dispatch( + getProposal({ + chainID, + baseURLs, + baseURL, + proposalId: Number(proposalID), + govV1, + }) + ); + dispatch( + getProposalTally({ + chainID, + baseURLs, + baseURL, + proposalId: Number(proposalID), + govV1, + }) + ); + dispatch(getPoolInfo({ chainID, baseURLs })); + dispatch(getDepositParams({ chainID, baseURLs, baseURL })); + dispatch(getGovTallyParams({ chainID, baseURL, baseURLs })); + }; + + const handleToggleText = () => setShowFullText(!showFullText); + + useEffect(() => { + const proposalDescription = get( + proposalInfo, + 'content.description', + get(proposalInfo, 'summary', '') + ); + setContentLength(proposalDescription.length); + setProposalMarkdown(proposalDescription.replace(/\\n/g, '\n')); + }, [proposalInfo]); + + useEffect(() => { + fetchProposalData(); + }, []); + + useEffect(() => { + if (poolInfo?.bonded_tokens) { + const value = totalVotes / parseInt(poolInfo.bonded_tokens); + setQuorumPercent((value * 100).toFixed(1)); + } + }, [poolInfo, totalVotes]); + + const getVotesPercentage = (votesCount: number) => { + return votesCount && totalVotes + ? ((votesCount / totalVotes) * 100).toFixed(2) + : '0'; + }; + + const data = [ + { + value: getVotesPercentage( + Number(get(tallyResult, 'yes', get(tallyResult, 'yes_count'))) + ), + count: Number(get(tallyResult, 'yes', get(tallyResult, 'yes_count'))), + color: 'linear-gradient(90deg, #2ba472 0%, rgba(43, 164, 114, 0.5) 100%)', + label: 'Yes', + }, + { + value: getVotesPercentage( + Number(get(tallyResult, 'no', get(tallyResult, 'no_count'))) + ), + count: Number(get(tallyResult, 'no', get(tallyResult, 'no_count'))), + color: 'linear-gradient(90deg, #d92101 0%, rgba(217, 33, 1, 0.5) 100%)', + label: 'No', + }, + { + value: getVotesPercentage( + Number(get(tallyResult, 'abstain', get(tallyResult, 'abstain_count'))) + ), + count: Number( + get(tallyResult, 'abstain', get(tallyResult, 'abstain_count')) + ), + color: 'linear-gradient(90deg, #ffc13c 0%, rgba(255, 193, 60, 0.5) 100%)', + label: 'Abstain', + }, + { + value: getVotesPercentage( + Number( + get( + tallyResult, + 'no_with_veto', + get(tallyResult, 'no_with_veto_count') + ) + ) + ), + count: Number( + get(tallyResult, 'no_with_veto', get(tallyResult, 'no_with_veto_count')) + ), + color: 'linear-gradient(90deg, #da561e 0%, rgba(218, 86, 30, 0.5) 100%)', + label: 'Veto', + }, + ]; + + const isProposal2daysgo = () => { + const targetDate = new Date(get(proposalInfo, 'voting_end_time')); + const currentDate = new Date(); + const twoDaysInMilliseconds = 2 * 24 * 60 * 60 * 1000; + + const timeDifference = targetDate.getTime() - currentDate.getTime(); + return timeDifference <= twoDaysInMilliseconds && timeDifference > 0; + }; + + const proposalTallyStatus = useAppSelector( + (state) => state.gov.chains?.[chainID]?.tally.status + ); + const txVoteStatus = useAppSelector( + (state) => state.gov.chains?.[chainID]?.tx?.status + ); + + const fetchVotes = () => { + dispatch( + getVotes({ + baseURL, + baseURLs, + proposalId: Number(proposalID), + voter: isAuthzMode ? authzGranterAddress : address, + chainID, + govV1, + }) + ); + }; + + useEffect(() => { + if (isWalletConnected) { + fetchVotes(); + } + }, [isWalletConnected, isAuthzMode]); + + useEffect(() => { + if (txVoteStatus === TxStatus.IDLE) { + fetchVotes(); + } + }, [txVoteStatus]); + + return ( + <> + {proposalStatus === 'pending' ? ( + + ) : ( + <> + {/* Banner */} + {isProposal2daysgo() ? ( +
    + info-icon +

    + Important +

    +

    + Voting ends in{' '} + {getTimeDifferenceToFutureDate( + get(proposalInfo, 'voting_end_time') + )} +

    +
    + ) : null} + +
    +
    +
    +
    +
    +
    router.back()} + > + Go back +
    +
    +
    +

    + {/* Aave v3.1 Cantina competitione */} + {get( + proposalInfo, + 'proposal_id', + get(proposalInfo, 'id') + )} + .     + {get( + proposalInfo, + 'content.title', + get(proposalInfo, 'title', '-') + ) || get(proposalInfo, 'content.@type', '')} +

    + {isStatusVoting ? ( +
    + Active +
    + ) : ( +
    + Deposit +
    + )} +
    +
    +
    + {isStatusVoting ? ( + <> +

    + Voting +

    +

    + ends in{' '} + {getTimeDifferenceToFutureDate( + get(proposalInfo, 'voting_end_time') + )} +

    + + ) : ( + <> +

    + Deposit +

    +

    + ends in{' '} + {getTimeDifferenceToFutureDate( + get(proposalInfo, 'deposit_end_time') + )} +

    + + )} +
    +
    +

    + on +

    + Network-logo +

    + {chainName} +

    +
    +
    +
    +
    + +
    +

    900 ? (showFullText ? 'overflow-scroll' : 'overflow-hidden') : 'overflow-scroll'}`} + > + {proposalMarkdown} +

    + + {contentLength > 900 ? ( + showFullText ? ( +

    + Show Less + Less-icon +

    + ) : ( +
    +
    + Continue Reading{' '} + more-icon +
    +
    + {' '} +
    +
    + ) + ) : null} +
    +
    +
    + {isStatusVoting ? ( + <> + {/*
    +

    Cast your vote

    +

    + Voting ends in{' '} + {getTimeDifferenceToFutureDate( + get(proposalInfo, 'voting_end_time') + )} +

    +
    */} + + + + ) : ( + <> + { + setDepositDialogOpen(true); + }} + btnStyles="items-center w-full" + /> + setDepositDialogOpen(false)} + open={depositDialogOpen} + proposalId={proposalID} + proposalTitle={ + get( + proposalInfo, + 'content.title', + get(proposalInfo, 'title', '-') + ) || get(proposalInfo, 'content.@type', '') + } + /> + + )} +
    +
    +
    + + {/* RightSide View */} +
    + {isStatusVoting ? ( + = parseFloat(quorumRequired) + } + quorumPercent={quorumPercent} + quorumRequired={quorumRequired} + totalVotes={totalVotes} + tallyResult={tallyResult || emptyTallyResult} + chainID={chainID} + /> + ) : ( + + )} + + {isStatusVoting ? ( + proposalTallyStatus === TxStatus.PENDING ? ( +
    + ) : ( +
    +
    +

    Current Status

    +
    +
    + {data.map((v) => ( +
    +
    +

    + {formatAmount( + Number((v.count / 10 ** decimals).toFixed(0)) + )}{' '} + {displayDenom} +

    +

    + Voted {v.label} +

    +
    +
    +
    +
    +
    + +

    + {Number(v.value) > 0 ? ( + {v.value} + ) : null} + % +

    +
    +
    + ))} +
    + ) + ) : null} + +
    +
    +

    Proposal Timeline

    +
    +
    +
    +
    +
    + Proposal-Created +
    +
    +
    + +

    + {getTimeDifferenceToFutureDate( + get(proposalInfo, 'submit_time'), + true + )}{' '} + ago +

    +
    +

    + Proposal Created +

    +
    +
    +
    +
    + Proposal-Created +
    +
    +
    + +

    + {isStatusVoting + ? getTimeDifferenceToFutureDate( + get(proposalInfo, 'voting_start_time', '-'), + true + ) + : getTimeDifferenceToFutureDate( + get(proposalInfo, 'submit_time', '-'), + true + )}{' '} + ago +

    +
    +

    + {isStatusVoting ? 'Voting ' : 'Deposit Time '} + started +

    +
    +
    +
    +
    + Proposal-Created +
    +
    + +

    + in{' '} + {isStatusVoting + ? getTimeDifferenceToFutureDate( + get(proposalInfo, 'voting_end_time', '-') + ) + : getTimeDifferenceToFutureDate( + get(proposalInfo, 'deposit_end_time', '-') + )} +

    +
    +

    + {isStatusVoting ? 'Voting' : 'Deposit Time '} ends +

    +
    +
    +
    +
    +
    +
    +
    + + )} + + ); +}; + +export default SingleProposal; diff --git a/frontend/src/app/(routes)/governance/[network]/[proposalId]/error.tsx b/frontend/src/app/(routes)/governance/[network]/[proposalId]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/governance/[network]/[proposalId]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/governance/[network]/[proposalId]/loading.tsx b/frontend/src/app/(routes)/governance/[network]/[proposalId]/loading.tsx new file mode 100644 index 000000000..7bf794bb7 --- /dev/null +++ b/frontend/src/app/(routes)/governance/[network]/[proposalId]/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import React from 'react'; +import SingleProposalLoading from '../../loaders/SingleProposalLoading'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/governance/[network]/[proposalId]/page.tsx b/frontend/src/app/(routes)/governance/[network]/[proposalId]/page.tsx index 27dcc82b5..4f5d2c37c 100644 --- a/frontend/src/app/(routes)/governance/[network]/[proposalId]/page.tsx +++ b/frontend/src/app/(routes)/governance/[network]/[proposalId]/page.tsx @@ -1,12 +1,55 @@ -import React from 'react'; -import ProposalPage from '../../ProposalPage'; +'use client'; -const page = () => { - return ( -
    - -
    +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import '../../style.css'; +import { useParams } from 'next/navigation'; +import { RootState } from '@/store/store'; +import SingleProposal from './SingleProposal'; + +interface pageInterface{} + + +const SingleProposalComponent = () => { + const params = useParams(); + const { network, proposalId: id } = params; + const chainName = typeof network === 'string' ? network : ''; + const proposalId = typeof id === 'string' ? id : ''; + const nameToChainIDs = useAppSelector( + (state: RootState) => state.common.nameToChainIDs ); -}; + const validChain = Object.keys(nameToChainIDs).some( + (chain) => chainName.toLowerCase() === chain.toLowerCase() + ); + + // const chainID = Object.keys(nameToChainIDs).find( + // (chain) => chain === chainName.toLowerCase() + // ); + + const chainID = nameToChainIDs[chainName] -export default page; + + const validId = () => { + const parsedValue = parseInt(proposalId, 10); + return !isNaN(parsedValue) && Number.isInteger(parsedValue); + }; + + + + return ( + <> + {!validChain ? ( +
    - Chain not found -
    + ) : !validId() ? ( +
    - Invalid Proposal ID -
    + ) : } + + ) +} + + +const page: React.FC = () => { + return ( + + ) +}; +export default page; \ No newline at end of file diff --git a/frontend/src/app/(routes)/governance/[network]/error.tsx b/frontend/src/app/(routes)/governance/[network]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/governance/[network]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/governance/[network]/loading.tsx b/frontend/src/app/(routes)/governance/[network]/loading.tsx new file mode 100644 index 000000000..c4430c65b --- /dev/null +++ b/frontend/src/app/(routes)/governance/[network]/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import React from 'react'; +import GovDashboardLoading from '../loaders/GovDashboardLoading'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/governance/[network]/page.tsx b/frontend/src/app/(routes)/governance/[network]/page.tsx index 29a330cdd..1f4fb460e 100644 --- a/frontend/src/app/(routes)/governance/[network]/page.tsx +++ b/frontend/src/app/(routes)/governance/[network]/page.tsx @@ -3,14 +3,15 @@ import { useAppSelector } from '@/custom-hooks/StateHooks'; import { RootState } from '@/store/store'; import { useParams } from 'next/navigation'; import React from 'react'; -import GovPage from '../GovPage'; +import GovDashboard from '../gov-dashboard/GovDashboard'; +import '../style.css'; const ChainProposals = () => { const params = useParams(); const paramChain = params.network; const chainName = typeof paramChain === 'string' ? paramChain : ''; const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs + (state: RootState) => state.common.nameToChainIDs ); let chainID: string = ''; Object.keys(nameToChainIDs).forEach((chain) => { @@ -19,9 +20,9 @@ const ChainProposals = () => { return ( <> {chainID.length ? ( - + ) : ( -
    +
    - Chain Not found -
    )} diff --git a/frontend/src/app/(routes)/governance/error.tsx b/frontend/src/app/(routes)/governance/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/governance/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/governance/gov-dashboard/GovDashboard.tsx b/frontend/src/app/(routes)/governance/gov-dashboard/GovDashboard.tsx new file mode 100644 index 000000000..13adc6de2 --- /dev/null +++ b/frontend/src/app/(routes)/governance/gov-dashboard/GovDashboard.tsx @@ -0,0 +1,273 @@ +'use client'; + +import PageHeader from '@/components/common/PageHeader'; +import useGetProposals from '@/custom-hooks/governance/useGetProposals'; +import useInitGovernance from '@/custom-hooks/governance/useInitGovernance'; +import React, { useRef, useState } from 'react'; +import { + HandleInputChangeEvent, + ProposalsData, + SelectedProposal, +} from '@/types/gov'; +import ProposalsList from './ProposalsList'; +import SearchProposalInput from './SearchProposalInput'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import GovDashboardLoading from '../loaders/GovDashboardLoading'; +import ProposalOverview from './ProposalOverivew'; +import { CSSTransition } from 'react-transition-group'; + +const GovDashboard = ({ chainIDs }: { chainIDs: string[] }) => { + useInitGovernance({ chainIDs }); + const { getProposals } = useGetProposals(); + // const [showAll, setShowAll] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [showDeposits, setShowDeposits] = useState(false); + const [filterDays, setFilterDays] = useState(0); + const [showAnimation, toggleAnimation] = useState(false); + const propsData = getProposals({ chainIDs, showAll: true }); + const proposalsData = getProposals({ chainIDs, showAll: true }); + const depositProposals = getProposals({ chainIDs, deposits: true }); + const [filteredProposals, setFilteredProposals] = useState( + [] + ); + const debounceTimeout = useRef(null); + const [selectedProposal, setSelectedProposal] = + useState(null); + + const proposalsLoading = + useAppSelector((state) => state.gov?.activeProposalsLoading) > + chainIDs?.length; + + const handleViewProposal = ({ + chainID, + proposalId, + isActive, + }: SelectedProposal) => { + setSelectedProposal((proposal) => { + if ( + proposal?.chainID === chainID && + proposal?.proposalId === proposalId + ) { + toggleAnimation(false); + return null; + } + toggleAnimation(true); + return { + chainID, + proposalId, + isActive, + }; + }); + }; + + const handleSearchQueryChange = (e: React.ChangeEvent) => { + const query = e.target.value; + setSearchQuery(query); + + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + debounceTimeout.current = window.setTimeout(() => { + const filtered = propsData.filter((proposal) => { + return ( + proposal.proposalInfo.proposalId.includes(query) || + proposal.proposalInfo.proposalTitle + .toLowerCase() + .includes(query.toLowerCase()) || + proposal.chainName.toLowerCase().includes(query.toLowerCase()) + ); + }); + setFilteredProposals(filtered); + }, 100); + }; + + const handleFiltersChange = (days: number) => { + setFilterDays(days); + + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + debounceTimeout.current = window.setTimeout(() => { + const filtered = propsData.filter((proposal) => { + if (!days) { + return true; + } + + const daysNo = proposal.proposalInfo.endTime.match(/\d+/) || 0; + if (daysNo && daysNo.length) { + if ( + Number(daysNo[0]) <= days || + !proposal.proposalInfo.endTime.includes('days') + ) { + return true; + } + } + }); + + setFilteredProposals(filtered); + }, 100); + }; + + const handleShowDeposits = (showDeposits: boolean) => { + setShowDeposits(showDeposits); + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + debounceTimeout.current = window.setTimeout(() => { + setFilteredProposals(depositProposals); + }, 100); + }; + + // const handleShowAllProposals = (e: boolean) => { + // setShowAll(e); + // setSelectedProposal(null); + // }; + + return ( +
    +
    + +
    +
    +
    +
    + +
    +
    + {proposalsLoading ? ( + + ) : ( + <> + {searchQuery?.length || filterDays || showDeposits ? ( + + ) : ( + + )} + + )} +
    +
    + +
    + {selectedProposal && ( + { + toggleAnimation(false); + setTimeout(() => { + handleViewProposal({ chainID, isActive, proposalId }); + }, 300); + }} + /> + )} +
    +
    +
    +
    + ); +}; + +export default GovDashboard; + +const QuickFilters = ({ + handleSearchQueryChange, + searchQuery, + // handleShowAllProposals, + handleFiltersChange, + filterDays, + selectedProposal, + handleShowDeposits, +}: { + searchQuery: string; + handleSearchQueryChange: HandleInputChangeEvent; + // handleShowAllProposals: (arg: boolean) => void; + handleFiltersChange: (n: number) => void; + filterDays: number; + selectedProposal: SelectedProposal | null; + handleShowDeposits: (arg: boolean) => void; +}) => { + return ( +
    +
    + + + +
    + +
    + {!selectedProposal && ( + + )} +
    +
    + ); +}; + +const GovHeader = () => { + return ( + + ); +}; diff --git a/frontend/src/app/(routes)/governance/gov-dashboard/ProposalItem.tsx b/frontend/src/app/(routes)/governance/gov-dashboard/ProposalItem.tsx new file mode 100644 index 000000000..ee2be818a --- /dev/null +++ b/frontend/src/app/(routes)/governance/gov-dashboard/ProposalItem.tsx @@ -0,0 +1,248 @@ +import { REDIRECT_ICON, TIMER_ICON } from '@/constants/image-names'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import { HandleSelectProposalEvent, SelectedProposal } from '@/types/gov'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; +import DialogDeposit from '../popups/DialogDeposit'; +import DialogVote from '../popups/DialogVote'; +import useGetProposals from '@/custom-hooks/governance/useGetProposals'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getColorForVoteOption } from '@/utils/util'; +import { getVotes } from '@/store/features/gov/govSlice'; +import useAddressConverter from '@/custom-hooks/useAddressConverter'; +import { TxStatus } from '@/types/enums'; + +const ProposalItem = ({ + chainLogo, + chainName, + endTime, + handleViewProposal, + isActive, + proposalId, + proposalTitle, + selectedProposal, + chainID, +}: { + selectedProposal: SelectedProposal | null; + proposalId: string; + chainLogo: string; + handleViewProposal: HandleSelectProposalEvent; + proposalTitle: string; + isActive: boolean; + chainName: string; + endTime: string; + chainID: string; +}) => { + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { address, baseURL, restURLs: baseURLs, govV1 } = getChainInfo(chainID); + const { getVote } = useGetProposals(); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); + const { convertAddress } = useAddressConverter(); + const authzGranterAddress = convertAddress(chainID, authzAddress); + + const [depositDialogOpen, setDepositDialogOpen] = useState(false); + const [voteDialogOpen, setVoteDialogOpen] = useState(false); + + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const alreadyVotedOption = getVote({ + address: isAuthzMode ? authzGranterAddress : address, + chainID, + proposalId, + }); + const txVoteStatus = useAppSelector( + (state) => state.gov.chains?.[chainID]?.tx?.status + ); + + const fetchVotes = () => { + dispatch( + getVotes({ + baseURL, + baseURLs, + proposalId: Number(proposalId), + voter: isAuthzMode ? authzGranterAddress : address, + chainID, + govV1, + }) + ); + } + + useEffect(() => { + if (isWalletConnected) { + fetchVotes(); + } + }, [isWalletConnected, isAuthzMode]); + + useEffect(() => { + if(txVoteStatus === TxStatus.IDLE) { + fetchVotes(); + } + }, [txVoteStatus]) + + return ( +
    +
    { + handleViewProposal({ + proposalId, + chainID, + isActive, + }); + }} + > +
    +
    + {proposalId} +
    + Network-Logo +
    +
    +
    +
    +
    +

    + {proposalTitle} +

    + +
    + + {!selectedProposal && ( +
    + {isActive ? ( +
    Active
    + ) : ( +
    Deposit
    + )} +
    + )} +
    +
    +
    + timer-icon +

    + {isActive ? 'Voting ends in' : 'Deposit ends in'} {endTime} +

    +
    +
    + +

    + {chainName} Network +

    +
    + {!selectedProposal && ( +
    + {alreadyVotedOption?.length ? ( +
    +
    +
    + You have voted +
    +
    + {alreadyVotedOption} +
    +
    +
    + ) : null} +
    + )} +
    +
    +
    + {selectedProposal ? null : ( +
    + +
    + )} +
    + {/*
    */} + {depositDialogOpen ? ( + setDepositDialogOpen(false)} + open={depositDialogOpen} + proposalTitle={proposalTitle} + endTime={endTime} + proposalId={proposalId} + /> + ) : null} + {voteDialogOpen ? ( + setVoteDialogOpen(false)} + open={voteDialogOpen} + proposalTitle={proposalTitle} + endTime={endTime} + proposalId={proposalId} + /> + ) : null} +
    + ); +}; + +export default ProposalItem; diff --git a/frontend/src/app/(routes)/governance/gov-dashboard/ProposalOverivew.tsx b/frontend/src/app/(routes)/governance/gov-dashboard/ProposalOverivew.tsx new file mode 100644 index 000000000..98b008cac --- /dev/null +++ b/frontend/src/app/(routes)/governance/gov-dashboard/ProposalOverivew.tsx @@ -0,0 +1,327 @@ +import { REDIRECT_ICON, TIMER_ICON } from '@/constants/image-names'; +import useGetProposals from '@/custom-hooks/governance/useGetProposals'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; +import Vote from './Vote'; +import CustomButton from '@/components/common/CustomButton'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import DialogDeposit from '../popups/DialogDeposit'; +import { useRouter } from 'next/navigation'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { RootState } from '@/store/store'; +import { get } from 'lodash'; +import { + getDepositParams, + getGovTallyParams, + getProposal, + getProposalTally, +} from '@/store/features/gov/govSlice'; +import { getPoolInfo } from '@/store/features/staking/stakeSlice'; +import { Tooltip } from '@mui/material'; +import { useRemark } from 'react-remark'; + +const ProposalOverview = ({ + chainID, + proposalId, + isActive, + onClose, +}: { + proposalId: string; + chainID: string; + isActive: boolean; + onClose: ({ + chainID, + proposalId, + isActive, + }: { + chainID: string; + proposalId: string; + isActive: boolean; + }) => void; +}) => { + const { getProposalOverview } = useGetProposals(); + const { chainLogo, chainName, proposalInfo } = getProposalOverview({ + chainID, + proposalId, + isActive, + }); + const [proposalMarkdown, setProposalMarkdown] = useRemark(); + const { endTime, proposalDescription, proposalTitle } = proposalInfo || {}; + const [depositDialogOpen, setDepositDialogOpen] = useState(false); + + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const { getChainInfo } = useGetChainInfo(); + const { restURLs: baseURLs, baseURL, govV1 } = getChainInfo(chainID); + + const tallyResult = useAppSelector( + (state: RootState) => + state.gov.chains[chainID]?.tally?.proposalTally?.[proposalId] + ); + + const totalVotes = ['yes', 'no', 'abstain', 'no_with_veto'].reduce( + (sum, key) => + sum + + Number(get(tallyResult, key, get(tallyResult, `${key}_count`)) || 0), + 0 + ); + + useEffect(() => { + setProposalMarkdown(proposalDescription.replace(/\\n/g, '\n')); + }, [proposalInfo.proposalTitle]); + + const fetchProposalData = () => { + dispatch( + getProposal({ + chainID, + baseURLs, + baseURL, + proposalId: Number(proposalId), + govV1, + }) + ); + dispatch( + getProposalTally({ + chainID, + baseURLs, + baseURL, + proposalId: Number(proposalId), + govV1, + }) + ); + dispatch(getPoolInfo({ chainID, baseURLs })); + dispatch(getDepositParams({ chainID, baseURLs, baseURL })); + dispatch(getGovTallyParams({ chainID, baseURL, baseURLs })); + }; + + useEffect(() => { + fetchProposalData(); + }, [chainID, proposalId]); + + const getVotesPercentage = (votesCount: number) => { + return votesCount && totalVotes + ? ((votesCount / totalVotes) * 100).toFixed(2) + : '0'; + }; + + const navigateToProposal = () => { + router.push(`/governance/${chainName}/${proposalId}`); + }; + const data = [ + { + value: getVotesPercentage( + Number(get(tallyResult, 'yes', get(tallyResult, 'yes_count'))) + ), + count: Number(get(tallyResult, 'yes', get(tallyResult, 'yes_count'))), + color: 'linear-gradient(90deg, #2ba472 0%, rgba(43, 164, 114, 0.5) 100%)', + label: 'Yes', + }, + { + value: getVotesPercentage( + Number(get(tallyResult, 'no', get(tallyResult, 'no_count'))) + ), + count: Number(get(tallyResult, 'no', get(tallyResult, 'no_count'))), + color: 'linear-gradient(90deg, #d92101 0%, rgba(217, 33, 1, 0.5) 100%)', + label: 'No', + }, + { + value: getVotesPercentage( + Number(get(tallyResult, 'abstain', get(tallyResult, 'abstain_count'))) + ), + count: Number( + get(tallyResult, 'abstain', get(tallyResult, 'abstain_count')) + ), + color: 'linear-gradient(90deg, #ffc13c 0%, rgba(255, 193, 60, 0.5) 100%)', + label: 'Abstain', + }, + { + value: getVotesPercentage( + Number( + get( + tallyResult, + 'no_with_veto', + get(tallyResult, 'no_with_veto_count') + ) + ) + ), + count: Number( + get(tallyResult, 'no_with_veto', get(tallyResult, 'no_with_veto_count')) + ), + color: 'linear-gradient(90deg, #da561e 0%, rgba(218, 86, 30, 0.5) 100%)', + label: 'Veto', + }, + ]; + + return ( +
    +
    +
    +
    +
    +
    +
    +
    +

    + {proposalTitle} +

    + View Proposal +
    + {isActive ? ( +
    Active
    + ) : ( +
    Deposit
    + )} +
    +
    + +
    +
    +
    +
    + timer-icon +

    + {isActive ? 'Voting' : 'Deposit'} +

    +

    ends in {endTime}

    +
    +
    +

    on

    +
    + Network-logo +

    {chainName}

    +
    +
    +
    +
    + {isActive ? ( +
    +
    +
    Current Status
    +
    +
    +

    +

    Yes

    +
    +
    +

    +

    No

    +
    +
    +

    +

    Abstain

    +
    +
    +

    +

    Veto

    +
    +
    +
    +
    + {data.map((v, index) => ( + +
    acc + parseFloat(item.value), + 0 + )}%`, + }} + >
    +
    + ))} +
    +
    + ) : null} + +
    +
    Summary
    +
    +
    + {proposalMarkdown} +
    +
    +
    + {isActive ? ( + + ) : ( + { + if (isWalletConnected) { + setDepositDialogOpen(true); + } else { + dispatch(setConnectWalletOpen(true)); + } + }} + /> + )} +
    + {isActive ? null : ( + setDepositDialogOpen(false)} + open={depositDialogOpen} + proposalId={proposalId} + proposalTitle={proposalTitle} + /> + )} +
    +
    + ); +}; + +export default ProposalOverview; diff --git a/frontend/src/app/(routes)/governance/gov-dashboard/ProposalsList.tsx b/frontend/src/app/(routes)/governance/gov-dashboard/ProposalsList.tsx new file mode 100644 index 000000000..c91db971c --- /dev/null +++ b/frontend/src/app/(routes)/governance/gov-dashboard/ProposalsList.tsx @@ -0,0 +1,56 @@ +import { + HandleSelectProposalEvent, + ProposalsData, + SelectedProposal, +} from '@/types/gov'; +import React from 'react'; +import ProposalItem from './ProposalItem'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { NO_DATA_ILLUSTRATION } from '@/constants/image-names'; + +const ProposalsList = ({ + proposals, + selectedProposal, + handleViewProposal, +}: { + proposals: ProposalsData[]; + + selectedProposal: SelectedProposal | null; + handleViewProposal: HandleSelectProposalEvent; +}) => { + return ( +
    + {proposals?.length ? ( + proposals.map((proposalsData) => { + const { chainID, chainLogo, chainName, isActive, proposalInfo } = + proposalsData; + const { endTime, proposalId, proposalTitle } = proposalInfo; + return ( + + ); + }) + ) : ( + + )} +
    + ); +}; + +export default ProposalsList; diff --git a/frontend/src/app/(routes)/governance/gov-dashboard/SearchProposalInput.tsx b/frontend/src/app/(routes)/governance/gov-dashboard/SearchProposalInput.tsx new file mode 100644 index 000000000..dc2c6eccc --- /dev/null +++ b/frontend/src/app/(routes)/governance/gov-dashboard/SearchProposalInput.tsx @@ -0,0 +1,67 @@ +import { SEARCH_ICON } from '@/constants/image-names'; +import { HandleInputChangeEvent } from '@/types/gov'; +import Image from 'next/image'; +import React, { useState } from 'react'; + +const SearchProposalInput = ({ + searchQuery, + handleSearchQueryChange, + // handleShowAllProposals, + handleShowDeposits, +}: { + searchQuery: string; + handleSearchQueryChange: HandleInputChangeEvent; + // handleShowAllProposals: (arg: boolean) => void; + handleShowDeposits: (arg: boolean) => void; +}) => { + const [check, SetCheck] = useState(false); + return ( +
    +
    +
    + + +
    +
    { + handleShowDeposits(!check); + SetCheck(!check); + }} + > + {check ? ( + after-check-icon + ) : ( + before-check-icon + )} + + +
    +
    +
    + ); +}; + +export default SearchProposalInput; diff --git a/frontend/src/app/(routes)/governance/gov-dashboard/Vote.tsx b/frontend/src/app/(routes)/governance/gov-dashboard/Vote.tsx new file mode 100644 index 000000000..42c01c60f --- /dev/null +++ b/frontend/src/app/(routes)/governance/gov-dashboard/Vote.tsx @@ -0,0 +1,174 @@ +import { GOV_VOTE_OPTIONS } from '@/constants/gov-constants'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useAuthzExecHelper from '@/custom-hooks/useAuthzExecHelper'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { txVote } from '@/store/features/gov/govSlice'; +import { TxStatus } from '@/types/enums'; +import { voteOptionNumber } from '@/utils/constants'; +import React, { useEffect, useState } from 'react'; +import useGetFeegranter from '@/custom-hooks/useGetFeegranter'; +import { MAP_TXN_MSG_TYPES } from '@/utils/feegrant'; +import CustomButton from '@/components/common/CustomButton'; +import { setError } from '@/store/features/common/commonSlice'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import { getColorForVoteOption } from '@/utils/util'; +import useGetProposals from '@/custom-hooks/governance/useGetProposals'; +import useAddressConverter from '@/custom-hooks/useAddressConverter'; + +const Vote = ({ + chainID, + proposalId, + colCount, +}: { + proposalId: string; + chainID: string; + colCount: number; +}) => { + const { getFeegranter } = useGetFeegranter(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { convertAddress } = useAddressConverter(); + const [voteOption, setVoteOption] = useState(''); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const handleOptionClick = (optionLabel: string) => { + setVoteOption((prev) => (prev === optionLabel ? '' : optionLabel)); + }; + + const loading = useAppSelector( + (state) => state.gov.chains?.[chainID]?.tx?.status + ); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); + const authzGranter = useAppSelector((state) => state.authz.authzAddress); + const { txAuthzVote } = useAuthzExecHelper(); + + const authzLoading = useAppSelector( + (state) => state.authz.chains?.[chainID]?.tx?.status || TxStatus.INIT + ); + + const dispatch = useAppDispatch(); + const basicChainInfo = getChainInfo(chainID); + const { address, aminoConfig, feeAmount, prefix, rest, rpc } = basicChainInfo; + const authzGranterAddress = convertAddress(chainID, authzAddress); + const { minimalDenom } = getDenomInfo(chainID); + const { getVote } = useGetProposals(); + const alreadyVotedOption = getVote({ + address: isAuthzMode ? authzGranterAddress : address, + chainID, + proposalId, + }); + + const handleVote = () => { + if (!isWalletConnected) { + dispatch(setConnectWalletOpen(true)); + return; + } + if (!voteOption) { + dispatch( + setError({ type: 'error', message: 'Please select vote option' }) + ); + return; + } + + if (isAuthzMode) { + txAuthzVote({ + grantee: address, + proposalId: Number(proposalId), + option: voteOptionNumber[voteOption.toLowerCase()], + granter: authzGranter, + chainID, + memo: '', + }); + return; + } + + dispatch( + txVote({ + basicChainInfo, + isAuthzMode: false, + voter: address, + proposalId: Number(proposalId), + option: voteOptionNumber[voteOption.toLowerCase()], + denom: minimalDenom, + chainID: chainID, + rpc: rpc, + rest: rest, + aminoConfig: aminoConfig, + prefix: prefix, + feeAmount: feeAmount, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['vote']), + justification: '', + }) + ); + }; + + useEffect(() => { + setVoteOption(''); + }, [proposalId]); + + return ( +
    + {alreadyVotedOption?.length ? ( +
    +
    + You have voted + + {alreadyVotedOption} + +
    +
    + ) : null} + +
    + {GOV_VOTE_OPTIONS?.map((option) => ( + + ))} +
    + + +
    + ); +}; + +export default Vote; diff --git a/frontend/src/app/(routes)/governance/loaders/GovDashboardLoading.tsx b/frontend/src/app/(routes)/governance/loaders/GovDashboardLoading.tsx new file mode 100644 index 000000000..5bc0fa664 --- /dev/null +++ b/frontend/src/app/(routes)/governance/loaders/GovDashboardLoading.tsx @@ -0,0 +1,31 @@ +'use client'; + +import React from 'react'; + +const GovDashboardLoading = () => { + return ( +
    +
    + {[1, 2, 3, 4].map((_, index) => ( +
    +
    +
    +

    +

    +
    +
    +

    +

    +
    + +
    +
    +
    +
    + ))} +
    +
    + ); +}; + +export default GovDashboardLoading; diff --git a/frontend/src/app/(routes)/governance/loaders/SingleProposalLoading.tsx b/frontend/src/app/(routes)/governance/loaders/SingleProposalLoading.tsx new file mode 100644 index 000000000..84ee00d0c --- /dev/null +++ b/frontend/src/app/(routes)/governance/loaders/SingleProposalLoading.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +const SingleProposalLoading = () => { + return ( +
    +
    +
    +
    +
    Go back
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + {/* Rightview */} +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default SingleProposalLoading; diff --git a/frontend/src/app/(routes)/governance/loading.tsx b/frontend/src/app/(routes)/governance/loading.tsx new file mode 100644 index 000000000..19feab03b --- /dev/null +++ b/frontend/src/app/(routes)/governance/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import React from 'react'; +import GovDashboardLoading from './loaders/GovDashboardLoading'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/governance/page.tsx b/frontend/src/app/(routes)/governance/page.tsx index 3490352db..9856b17c9 100644 --- a/frontend/src/app/(routes)/governance/page.tsx +++ b/frontend/src/app/(routes)/governance/page.tsx @@ -2,18 +2,19 @@ import React from 'react'; import { useAppSelector } from '@/custom-hooks/StateHooks'; import { RootState } from '@/store/store'; -import GovPage from './GovPage'; +import GovDashboard from './gov-dashboard/GovDashboard'; +import './style.css'; const Page = () => { const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs + (state: RootState) => state.common.nameToChainIDs ); const chainIDs = Object.keys(nameToChainIDs).map( (chainName) => nameToChainIDs[chainName] ); - return ; + return ; }; export default Page; diff --git a/frontend/src/app/(routes)/governance/popups/DialogDeposit.tsx b/frontend/src/app/(routes)/governance/popups/DialogDeposit.tsx new file mode 100644 index 000000000..1c268d712 --- /dev/null +++ b/frontend/src/app/(routes)/governance/popups/DialogDeposit.tsx @@ -0,0 +1,204 @@ +import CustomButton from '@/components/common/CustomButton'; +import CustomDialog from '@/components/common/CustomDialog'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { parseBalance } from '@/utils/denom'; +import React, { useEffect, useState } from 'react'; +import AmountInputWrapper from '../utils-components/AmountInputWrapper'; +import { TxStatus } from '@/types/enums'; +import useGetTxInputs from '@/custom-hooks/useGetTxInputs'; +import useAuthzExecHelper from '@/custom-hooks/useAuthzExecHelper'; +import useGetFeegranter from '@/custom-hooks/useGetFeegranter'; +import { txDeposit } from '@/store/features/gov/govSlice'; +import { MAP_TXN_MSG_TYPES } from '@/utils/feegrant'; + +const DialogDeposit = ({ + onClose, + open, + chainID, + endTime, + proposalTitle, + proposalId, +}: { + open: boolean; + onClose: () => void; + chainID: string; + proposalTitle: string; + endTime: string; + proposalId: string; +}) => { + const dispatch = useAppDispatch(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { getVoteTxInputs } = useGetTxInputs(); + const { txAuthzDeposit } = useAuthzExecHelper(); + + const { decimals, minimalDenom, displayDenom } = getDenomInfo(chainID); + const { feeAmount } = getChainInfo(chainID); + + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + + const [availableBalance, setAvailableBalance] = useState(0); + const [depositAmount, setDepositAmount] = useState(''); + const balanceLoading = useAppSelector((state) => + isAuthzMode + ? state.bank?.authz?.balances?.[chainID]?.status + : state.bank?.balances?.[chainID]?.status + ); + const balance = useAppSelector((state) => + isAuthzMode + ? state.bank?.authz?.balances?.[chainID] + : state.bank?.balances?.[chainID] + ); + const authzGranter = useAppSelector((state) => state.authz.authzAddress); + + const quickSelectAmount = (value: string) => { + if (value === 'half') { + let halfAmount = Math.max(0, (availableBalance || 0) - feeAmount) / 2; + halfAmount = +halfAmount.toFixed(6); + setDepositAmount(halfAmount.toString()); + } else { + let maxAmount = Math.max(0, (availableBalance || 0) - feeAmount); + maxAmount = +maxAmount.toFixed(6); + setDepositAmount(maxAmount.toString()); + } + }; + + const handleDepositAmountChange = ( + e: React.ChangeEvent + ) => { + const input = e.target.value; + if (/^-?\d*\.?\d*$/.test(input)) { + if ((input.match(/\./g) || []).length <= 1) { + setDepositAmount(input); + } + } + }; + + const loading = useAppSelector( + (state) => state.gov.chains?.[chainID]?.tx?.status + ); + const authzLoading = useAppSelector( + (state) => state.authz.chains?.[chainID]?.tx?.status || TxStatus.INIT + ); + + const { getFeegranter } = useGetFeegranter(); + + const handleDeposit = () => { + const { + aminoConfig, + prefix, + rest, + feeAmount, + address, + rpc, + minimalDenom, + basicChainInfo, + } = getVoteTxInputs(chainID); + + if (isAuthzMode) { + txAuthzDeposit({ + grantee: address, + proposalId: Number(proposalId), + amount: Number(depositAmount) * 10 ** decimals, + granter: authzGranter, + chainID: chainID, + memo: '', + }); + return; + } + + if (isAuthzMode) { + txAuthzDeposit({ + grantee: address, + proposalId: Number(proposalId), + amount: Number(depositAmount) * 10 ** decimals, + granter: authzGranter, + chainID: chainID, + memo: '', + }); + return; + } + + dispatch( + txDeposit({ + isAuthzMode: false, + basicChainInfo, + depositer: address, + proposalId: Number(proposalId), + amount: Number(depositAmount) * 10 ** decimals, + denom: minimalDenom, + chainID: chainID, + rpc: rpc, + rest: rest, + aminoConfig: aminoConfig, + prefix: prefix, + feeAmount: feeAmount, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['deposit']), + }) + ); + }; + + useEffect(() => { + if (balance) { + setAvailableBalance( + parseBalance( + balance?.list?.length ? balance.list : [], + decimals, + minimalDenom + ) + ); + } + }, [balance]); + + return ( + +
    +
    +
    +
    + {proposalTitle} +
    +
    + Deposit +

    ends in {endTime}

    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + ); +}; + +export default DialogDeposit; diff --git a/frontend/src/app/(routes)/governance/popups/DialogVote.tsx b/frontend/src/app/(routes)/governance/popups/DialogVote.tsx new file mode 100644 index 000000000..3035a3183 --- /dev/null +++ b/frontend/src/app/(routes)/governance/popups/DialogVote.tsx @@ -0,0 +1,180 @@ +import CustomButton from '@/components/common/CustomButton'; +import CustomDialog from '@/components/common/CustomDialog'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import React, { useEffect, useState } from 'react'; +import { TxStatus } from '@/types/enums'; +import useAuthzExecHelper from '@/custom-hooks/useAuthzExecHelper'; +import useGetFeegranter from '@/custom-hooks/useGetFeegranter'; +import { MAP_TXN_MSG_TYPES } from '@/utils/feegrant'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import { setError } from '@/store/features/common/commonSlice'; +import { voteOptionNumber } from '@/utils/constants'; +import { txVote } from '@/store/features/gov/govSlice'; +import { GOV_VOTE_OPTIONS } from '@/constants/gov-constants'; + +const DialogVote = ({ + onClose, + open, + chainID, + endTime, + proposalTitle, + proposalId, +}: { + open: boolean; + onClose: () => void; + chainID: string; + proposalTitle: string; + endTime: string; + proposalId: string; +}) => { + const { getFeegranter } = useGetFeegranter(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const [voteOption, setVoteOption] = useState(''); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const handleOptionClick = (optionLabel: string) => { + setVoteOption((prev) => (prev === optionLabel ? '' : optionLabel)); + }; + + const loading = useAppSelector( + (state) => state.gov.chains?.[chainID]?.tx?.status + ); + + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzGranter = useAppSelector((state) => state.authz.authzAddress); + const { txAuthzVote } = useAuthzExecHelper(); + + const authzLoading = useAppSelector( + (state) => state.authz.chains?.[chainID]?.tx?.status || TxStatus.INIT + ); + + const dispatch = useAppDispatch(); + + const handleVote = () => { + if (!isWalletConnected) { + dispatch(setConnectWalletOpen(true)); + return; + } + if (!voteOption) { + dispatch( + setError({ type: 'error', message: 'Please select vote option' }) + ); + return; + } + const basicChainInfo = getChainInfo(chainID); + const { address, aminoConfig, feeAmount, prefix, rest, rpc } = + basicChainInfo; + const { minimalDenom } = getDenomInfo(chainID); + + if (isAuthzMode) { + txAuthzVote({ + grantee: address, + proposalId: Number(proposalId), + option: voteOptionNumber[voteOption.toLowerCase()], + granter: authzGranter, + chainID, + memo: '', + }); + return; + } + + dispatch( + txVote({ + basicChainInfo, + isAuthzMode: false, + voter: address, + proposalId: Number(proposalId), + option: voteOptionNumber[voteOption.toLowerCase()], + denom: minimalDenom, + chainID: chainID, + rpc: rpc, + rest: rest, + aminoConfig: aminoConfig, + prefix: prefix, + feeAmount: feeAmount, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['vote']), + justification: '', + }) + ); + }; + + useEffect(() => { + setVoteOption(''); + }, [proposalId]); + + return ( + +
    +
    +
    +
    + {proposalTitle} +
    +
    + Voting +

    ends in {endTime}

    +
    +
    +
    +
    +
    + {GOV_VOTE_OPTIONS?.map((option) => ( + + ))} +
    +
    + +
    +
    +
    + ); +}; + +export default DialogVote; diff --git a/frontend/src/app/(routes)/governance/proposalData.json b/frontend/src/app/(routes)/governance/proposalData.json deleted file mode 100644 index e5b9ce6d6..000000000 --- a/frontend/src/app/(routes)/governance/proposalData.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "id": "123", - "title": "Introduce Take Rate and deployment deposit for axlUSDC", - "expires": "Expires in two days", - "votingStatus": "Active Voting" - }, - { - "id": "124", - "title": "Introduce Take Rate and deployment deposit for axlUSDC", - "expires": "Expires in two days", - "votingStatus": "Active Voting" - }, - { - "id": "125", - "title": "Yet Another Proposal", - "expires": "Expires in four days", - "votingStatus": "Active Voting" - }, - { - "id": "126", - "title": "One More Proposal", - "expires": "Expires in five days", - "votingStatus": "Active Voting" - } -] diff --git a/frontend/src/app/(routes)/governance/proposalDepositdata.json b/frontend/src/app/(routes)/governance/proposalDepositdata.json deleted file mode 100644 index 735e92bcc..000000000 --- a/frontend/src/app/(routes)/governance/proposalDepositdata.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "proposalOverviewData": { - "proposalId": 123, - "proposalText": "Your proposal text goes here...", - "proposalname": "Introduce Take Rate and deployment deposit for axlUSDC", - "quorum": 50, - "stake": 0.8, - "submittedAt": "23-2023-10 10_23_56", - "endsAt": "23-2023-10 10_23_56", - "proposalNetwork": "Cosmos", - "depositrequired": "10 Stake", - "atomsValue": "ATOMS" - } -} diff --git a/frontend/src/app/(routes)/governance/proposalvoteData.json b/frontend/src/app/(routes)/governance/proposalvoteData.json deleted file mode 100644 index 74d5d5a08..000000000 --- a/frontend/src/app/(routes)/governance/proposalvoteData.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "proposalOverviewVoteData": { - "proposalId": "#123", - "proposalText": "Your proposal text goes here...", - "proposalname": "Introduce Take Rate and deployment deposit for axlUSDC", - "quorum": 50, - "createdAt": "23rd October 2023", - "startedAt": "24th October 2023", - "endsAt": "29th October 2023", - "proposalNetwork": "Cosmos", - "totalvotes": "123,345,876", - "createdby": "Name of the creator", - "depositamount": "123 ATOMS", - "data": [ - { "value": 75, "color": "#4AA29C", "label": "Yes" }, - { "value": 23, "color": "#E57575", "label": "No" }, - { "value": 2, "color": "#EFFF34", "label": "Veto" }, - { "value": 0, "color": "#EFFF34", "label": "Veto" } - ], - "dataset": [ - { "value": 75, "color": "#759BE5", "label": "Quorum" }, - { "value": 23, "color": "#75E5A2", "label": "Turn out" }, - { "value": 2, "color": "#B373CA", "label": "Threshold" } - ] - } -} diff --git a/frontend/src/app/(routes)/governance/style.css b/frontend/src/app/(routes)/governance/style.css index dc89301a5..936c98b5e 100644 --- a/frontend/src/app/(routes)/governance/style.css +++ b/frontend/src/app/(routes)/governance/style.css @@ -1,198 +1,106 @@ -.proposals-head { - @apply flex flex-col items-start gap-4; -} -.cstm-btn { - @apply flex h-8 flex-col justify-center items-start gap-2 backdrop-blur-[2px] px-6 py-2 rounded-[100px]; - background: rgba(255, 255, 255, 0.1); +.gov-main { + @apply py-10 relative flex flex-col; + height: calc(100vh - 60px) !important; } -.cstm-btn-selected { - @apply flex h-8 flex-col justify-center items-start gap-2 backdrop-blur-[2px] px-6 py-2 rounded-[100px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.proposal-id { + @apply relative flex items-center justify-center font-bold rounded-lg w-16 text-[14px] tracking-[1.96px] h-[52px]; + background: linear-gradient( + 180deg, + rgba(122, 126, 156, 0.2) 0.5%, + rgba(9, 9, 10, 0.2) 100% + ); } -.main-page { - @apply flex flex-col items-start gap-10 px-10 py-6; -} -.view-history { - @apply flex flex-col justify-center cursor-pointer text-[rgba(255,255,255,0.50)] text-xs font-extralight leading-[normal] underline; +.bottom-network-logo { + @apply absolute bottom-[-8px] right-[-4px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] rounded-[100px] border-2 border-solid border-[#111115]; + background: lightgray 50% / cover no-repeat; } -.proposal-id { - @apply flex w-8 h-8 justify-center items-center gap-2.5 rounded-[100px] text-[8px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.active-badge { + @apply h-[25px] w-[85px] border flex justify-center items-center gap-2 px-3 py-1 rounded-[100px] border-solid border-[#2BA472]; + background: rgba(43, 164, 114, 0.5); } -.proposal-id-static { - background: rgba(255, 255, 255, 0.1); + +.deposit-badge { + @apply h-[25px] w-[85px] flex justify-center items-center gap-2 border px-3 py-1 rounded-[100px] border-solid border-[#DA561E]; + background: rgba(218, 86, 30, 0.5); } -.proposal { - @apply px-2 py-4 flex items-start gap-4; + +.search-proposal-input { + @apply w-full border-none cursor-text focus:outline-none bg-transparent placeholder:text-[#FFFFFF30] placeholder:font-normal text-[#ffffff]; } -.proposal:hover, -.proposal-selected { - @apply rounded-2xl cursor-pointer bg-[#FFFFFF0D]; - box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); +.search-proposal-field { + @apply py-4 px-6 bg-[#FFFFFF05] rounded-full flex w-full max-w-[600px]; } -.proposal:hover .proposal-id { - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.amount-input-field { + @apply bg-transparent w-full border-none focus:outline-none text-[28px] font-bold placeholder:text-[#FFFFFF33]; } -.proposal-text-small { - @apply flex items-center text-[rgba(255,255,255,0.75)] text-sm font-light leading-[14px]; +/* New css */ + +.text-bg { + background: linear-gradient(270deg, #fff -67.89%, #999 99.95%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.proposal-text-extralight { - @apply text-white text-xs font-extralight text-[14px] leading-[14px]; +.selected-btns { + @apply flex justify-center items-center gap-4 px-4 py-[10.5px] rounded-lg border-[0.25px] hover:bg-[#ffffff14] hover:border-transparent; +} + +.proposal-view { + @apply shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] flex flex-col gap-4 p-10 rounded-2xl; + background: rgba(255, 255, 255, 0.02); } -.proposal-text-normal { - @apply text-white text-sm font-normal leading-6; +.vote-optn-btn { + @apply border flex h-8 justify-center items-center gap-2 px-4 py-[10.5px] rounded-[100px] border-solid border-[rgba(255,255,255,0.20)]; } -.proposal-text-normal-base { - @apply text-white text-base font-normal leading-[18px]; +.header { + @apply flex flex-col justify-center items-start gap-4 px-6 py-4 top-0 sticky; + background: #ffc13c; } -.proposal-text-medium { - @apply flex items-center text-white text-base font-medium; +.cast-vote-grid { + @apply flex flex-col items-end gap-6 w-full pt-4 pb-6 px-4 rounded-3xl; + background: #ffffff05; } -.proposal-text-big { - @apply text-white text-xl leading-[normal]; +.proposal-passed-text { + @apply bg-clip-text text-sm font-light leading-[normal]; + background: linear-gradient(90deg, #b7e183 0%, #71de56 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.proposal-text-main { - @apply text-white text-xl font-medium leading-[normal]; +.vertical-line { + @apply w-[0.25px] h-14 bg-[#FFFFFF80] opacity-20; } -.popup-grid { - @apply flex w-[889px] flex-col justify-center items-center gap-6 opacity-95 backdrop-blur-[2px] rounded-3xl; +.blur { + @apply backdrop-blur-[2px]; background: linear-gradient( - 178deg, - #241b61 1.71%, - #69448d 98.35%, - #69448d 98.35% + 174deg, + rgba(217, 217, 217, 0) -235.54%, + rgba(14, 16, 27, 0.02) 95.6% ); } -.cross { - @apply flex justify-end items-center gap-2.5 self-stretch pl-6 pr-10 py-10; -} -.image-grid { - @apply flex justify-end items-center gap-10 pr-0; -} -.text-grid { - @apply w-[451px]; -} -.placeholder-text { - @apply flex h-[54px] flex-col justify-center items-start gap-2 px-4 py-0 rounded-2xl; - background: rgba(255, 255, 255, 0.1); -} -.placeholder-text input::placeholder { - @apply text-white text-xs font-extralight; -} -.placeholder-text input { - background-color: transparent; -} -.button { - @apply flex justify-center items-center gap-6 px-10 py-2.5 rounded-2xl; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); -} -.proposal-div { - @apply flex justify-between gap-10 space-y-6; -} -.proposal-brief { - @apply flex-1 flex-col items-start gap-4 p-6 rounded-2xl; - background: #0e0b26; -} -.status-grid { - @apply flex flex-col justify-center items-start gap-4 backdrop-blur-[2px] px-0 py-6 rounded-2xl; - background: #0e0b26; -} -.status-view-grid { - @apply flex flex-col justify-center items-start gap-6 px-6 py-6; -} -.status-view { - @apply flex flex-col justify-center items-start gap-6; -} -.status-pass { - @apply flex justify-center items-center gap-2.5 py-0; -} -.voting-grid { - @apply flex flex-col justify-center items-start gap-4 backdrop-blur-[2px] px-6 py-6 rounded-2xl; -} -.voting-view { - @apply flex flex-col justify-center items-center gap-6; -} -.horizontal-line { - @apply h-2 rounded-[100px]; - background: white; -} -.proposal-details-grid { - @apply flex flex-col justify-center items-start gap-6 backdrop-blur-[2px] px-6 py-6 rounded-2xl; -} -.proposal-details { - @apply flex justify-between items-start px-6 py-0; -} -.right-bar { - @apply flex w-[500px] h-screen flex-col items-center gap-10 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] p-6; - background: #0e0b26; -} -.radio-buttons { - @apply flex flex-row w-full justify-between items-center; -} -.radio-container { - @apply relative cursor-pointer block mb-2.5 pl-[30px] text-white items-center; -} -.radio-container input { - @apply absolute opacity-0 cursor-pointer; -} -.radio-checkmark { - @apply absolute h-4 w-4 border rounded-[50%] border-solid border-white left-0 top-0 flex justify-center items-center; -} -.radio-container input:checked ~ .radio-checkmark .radio-check { - @apply bg-white w-[6px] h-[6px] rounded-full; -} -.vote-grid { - @apply flex flex-col w-full items-start gap-4 backdrop-blur-[2px] py-6 px-6 rounded-2xl; - background: rgba(255, 255, 255, 0.05); -} -.view-full { - @apply flex flex-col justify-center text-center leading-[normal] underline cursor-pointer text-[#FFFFFFBF] text-[12px] font-extralight underline-offset-2; -} -.scrollable-container { - @apply max-h-screen overflow-y-scroll; +.yes-bg { + background: linear-gradient(90deg, #2ba472 0%, rgba(43, 164, 114, 0.5) 100%); } -.ad-close { - @apply absolute right-[-2px] top-[-2px] cursor-pointer rounded-full bg-[#ffffff1a]; -} - -.custom-radio-button-label { - @apply flex items-center cursor-pointer gap-2; +.no-bg { + background: linear-gradient(90deg, #d92101 0%, rgba(217, 33, 1, 0.5) 100%); } - -.custom-radio-button { - @apply border-2 w-4 h-4 border-[#FFFFFF80] rounded-full flex justify-center items-center; -} - -.custom-radio-button-checked { - @apply h-[6px] w-[6px] bg-white rounded-full; +.abstain-bg { + background: linear-gradient(90deg, #ffc13c 0%, rgba(255, 193, 60, 0.5) 100%); } -.cstm-btn:hover { - background-color: #8f8a8a1a; +.veto-bg { + background: linear-gradient(90deg, #da561e 0%, rgba(218, 86, 30, 0.5) 100%); } -.search-validator-input { - @apply w-full pl-2 border-none cursor-pointer focus:outline-none bg-transparent placeholder:text-[16px] placeholder:text-[#FFFFFFBF]; -} -.amount-chip { - @apply opacity-80 rounded-lg text-[#E57575] text-center text-sm not-italic font-normal leading-[normal] max-w-fit py-1 px-2 truncate; - background: rgba(229, 117, 117, 0.2); -} -.raw-content { - @apply p-2 bg-black max-h-[600px] overflow-y-scroll; -} -.back { - @apply rounded-2xl w-52 h-10 p-4 pb-8; - background: rgba(255, 255, 255, 0.1); +.badge-bg { + @apply flex justify-center items-center gap-2 px-4 py-2 rounded-[100px] bg-[#FFFFFF05] w-[158px]; } .proposal-description-markdown { - @apply opacity-80; + @apply opacity-90 text-[#fffffff0]; } .proposal-description-markdown p, @@ -228,7 +136,40 @@ margin-left: 1em; } -.vote-popup-btn, -.deposit-popup-btn { - @apply button w-36 min-h-[44px]; +.proposal-overview-enter { + opacity: 0; + transform: translateX(100%); +} + +.proposal-overview-enter-active { + opacity: 1; + transform: translateX(0); + transition: + opacity 300ms, + transform 300ms; +} + +.proposal-overview-exit { + opacity: 1; + transform: translateX(0); +} + +.proposal-overview-exit-active { + opacity: 0; + transform: translateX(100%); + transition: + opacity 300ms, + transform 300ms; +} +.yes-option { + @apply flex h-2 w-2 justify-center items-center gap-2 border rounded-full border-solid border-[#2BA472] bg-[#2CA472]; +} +.no-option { + @apply h-2 w-2 rounded-full bg-[#D92200]; +} +.abstain-option { + @apply h-2 w-2 rounded-full bg-[#FFC03D]; +} +.veto-option { + @apply h-2 w-2 rounded-full bg-[#DA571E]; } diff --git a/frontend/src/app/(routes)/governance/utils-components/AmountInputWrapper.tsx b/frontend/src/app/(routes)/governance/utils-components/AmountInputWrapper.tsx new file mode 100644 index 000000000..992853cf0 --- /dev/null +++ b/frontend/src/app/(routes)/governance/utils-components/AmountInputWrapper.tsx @@ -0,0 +1,85 @@ +import React from 'react'; + +type QuickSelectAmountFunc = (value: string) => void; + +const AmountInputWrapper = ({ + quickSelectAmount, + balance, + displayDenom, + depositAmount, + handleInputChange, + balanceLoading, +}: { + quickSelectAmount: QuickSelectAmountFunc; + balance: number; + displayDenom: string; + depositAmount: string; + handleInputChange: HandleChangeEvent; + balanceLoading: boolean; +}) => { + return ( +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    Available Balance:
    + {balanceLoading ? ( +
    + Fetching balance{' '} +
    + ) : ( + <> + {balance ? ( +
    + {balance} {displayDenom} +
    + ) : ( + '-' + )} + + )} +
    +
    +
    + ); +}; +export default AmountInputWrapper; + +const QuickSetAmountButton = ({ + value, + quickSelectAmount, +}: { + value: string; + quickSelectAmount: QuickSelectAmountFunc; +}) => { + return ( + + ); +}; diff --git a/frontend/src/app/(routes)/governance/utils-components/DepositCollected.tsx b/frontend/src/app/(routes)/governance/utils-components/DepositCollected.tsx new file mode 100644 index 000000000..4f65d6ce7 --- /dev/null +++ b/frontend/src/app/(routes)/governance/utils-components/DepositCollected.tsx @@ -0,0 +1,93 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { TxStatus } from '@/types/enums'; +import { parseBalance } from '@/utils/denom'; +import React, { useEffect, useState } from 'react'; + +const DepositCollected = ({ + proposalInfo, + chainID, +}: { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + proposalInfo: any; + chainID: string; +}) => { + const { getDenomInfo } = useGetChainInfo(); + const { displayDenom, minimalDenom, decimals } = getDenomInfo(chainID); + + const depositParams = useAppSelector( + (state) => state.gov.chains?.[chainID]?.depositParams.params + ); + const depositParamsLoading = useAppSelector( + (state) => state.gov.chains?.[chainID]?.depositParams?.status + ); + + const [minDeposit, setMinDeposit] = useState(0); + const [totalDeposit, setTotalDeposit] = useState(0); + const [depositPercent, setDepositPercent] = useState(0); + + useEffect(() => { + if ( + depositParams?.min_deposit?.length && + proposalInfo?.total_deposit?.length + ) { + const min_deposit = parseBalance( + depositParams.min_deposit, + decimals, + minimalDenom + ); + const total_deposit = parseBalance( + proposalInfo.total_deposit, + decimals, + minimalDenom + ); + console.log(proposalInfo.total_deposit, decimals, minimalDenom); + const deposit_percent = Math.floor((total_deposit / min_deposit) * 100); + setMinDeposit(min_deposit); + setTotalDeposit(total_deposit); + setDepositPercent(deposit_percent); + } + }, [depositParams, proposalInfo]); + + return ( +
    +
    +

    Deposit Collected

    +
    +
    + {minDeposit ? ( + <> +
    +
    + {totalDeposit}/{minDeposit} {displayDenom} +
    +
    + {depositPercent}% deposit collected +
    +
    +
    +
    +
    + + ) : ( + <> + {depositParamsLoading === TxStatus.PENDING ? ( + <> +
    +
    + + ) : null} + + )} +
    + ); +}; + +export default DepositCollected; diff --git a/frontend/src/app/(routes)/groups/page.tsx b/frontend/src/app/(routes)/groups/page.tsx deleted file mode 100644 index bdb0cb9e5..000000000 --- a/frontend/src/app/(routes)/groups/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const page = () => { - return
    Groups
    ; -}; - -export default page; diff --git a/frontend/src/app/(routes)/history/page.tsx b/frontend/src/app/(routes)/history/page.tsx deleted file mode 100644 index 983ebb85b..000000000 --- a/frontend/src/app/(routes)/history/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' - -const page = () => { - return ( -
    page
    - ) -} - -export default page \ No newline at end of file diff --git a/frontend/src/app/(routes)/multiops/Multiops.tsx b/frontend/src/app/(routes)/multiops/Multiops.tsx new file mode 100644 index 000000000..fa44737b3 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/Multiops.tsx @@ -0,0 +1,35 @@ +'use client'; +import Image from 'next/image'; +import React from 'react'; + +const Multiops = () => { + const message = + 'All Networks page is not supported for Multiops, Please select a network.'; + return ( +
    +
    +

    Multiops

    +
    +
    + {'No +

    {message}

    + +
    +
    + ); +}; + +export default Multiops; diff --git a/frontend/src/app/(routes)/multiops/[network]/ChainMultiops.tsx b/frontend/src/app/(routes)/multiops/[network]/ChainMultiops.tsx new file mode 100644 index 000000000..06a36f427 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/[network]/ChainMultiops.tsx @@ -0,0 +1,25 @@ +'use client'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React from 'react'; +import PageMultiops from './PageMultiops'; + +const ChainMultiops = ({ network }: { network: string }) => { + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const chainName = network.toLowerCase(); + const validChain = chainName in nameToChainIDs; + return ( +
    + {validChain ? ( + + ) : ( + <> +
    + - The {chainName} is not supported - +
    + + )} +
    + ); +}; + +export default ChainMultiops; diff --git a/frontend/src/app/(routes)/multiops/[network]/PageMultiops.tsx b/frontend/src/app/(routes)/multiops/[network]/PageMultiops.tsx new file mode 100644 index 000000000..e8585ef6f --- /dev/null +++ b/frontend/src/app/(routes)/multiops/[network]/PageMultiops.tsx @@ -0,0 +1,23 @@ +'use client'; +import React from 'react'; +import TxnBuilder from '../components/TxnBuilder'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; + +const PageMultiops = ({ chainName }: { chainName: string }) => { + const nameToChainIDs: Record = useAppSelector( + (state) => state.wallet.nameToChainIDs + ); + const chainID = nameToChainIDs[chainName]; + return ( +
    +
    +

    MultiOps

    +
    +
    + +
    +
    + ); +}; + +export default PageMultiops; diff --git a/frontend/src/app/(routes)/multiops/[network]/error.tsx b/frontend/src/app/(routes)/multiops/[network]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/[network]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/multiops/[network]/loading.tsx b/frontend/src/app/(routes)/multiops/[network]/loading.tsx new file mode 100644 index 000000000..0b08e2215 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/[network]/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/multiops/[network]/page.tsx b/frontend/src/app/(routes)/multiops/[network]/page.tsx new file mode 100644 index 000000000..38c12396b --- /dev/null +++ b/frontend/src/app/(routes)/multiops/[network]/page.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ChainMultiops from './ChainMultiops'; +import '../multiops.css'; + +const page = ({ params: { network } }: { params: { network: string } }) => { + return ; +}; + +export default page; diff --git a/frontend/src/app/(routes)/multiops/components/AddressField.tsx b/frontend/src/app/(routes)/multiops/components/AddressField.tsx new file mode 100644 index 000000000..fe38de7d1 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/AddressField.tsx @@ -0,0 +1,41 @@ +import { TextField } from '@mui/material'; +import React from 'react'; +import { Control, Controller } from 'react-hook-form'; +import { sendTxnTextFieldStyles } from '../styles'; + +const AddressField = ({ + control, + name, +}: { + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: Control; + name: string; +}) => { + return ( + ( + + )} + /> + ); +}; + +export default AddressField; diff --git a/frontend/src/app/(routes)/multiops/components/AmountInputField.tsx b/frontend/src/app/(routes)/multiops/components/AmountInputField.tsx new file mode 100644 index 000000000..5288a7983 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/AmountInputField.tsx @@ -0,0 +1,84 @@ +import { INSUFFICIENT_BALANCE } from '@/utils/errors'; +import { InputAdornment, TextField } from '@mui/material'; +import React from 'react'; +import { Control, Controller } from 'react-hook-form'; +import { textFieldInputPropStyles, textFieldStyles } from '../styles'; + +interface AmountInputFieldProps { + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: Control; + availableBalance: number; + displayDenom: string; + setValue: any; + feeAmount: number; +} + +const AmountInputField = (props: AmountInputFieldProps) => { + const { availableBalance, control, displayDenom, feeAmount, setValue } = + props; + return ( + { + const amount = Number(value); + if (isNaN(amount) || amount <= 0) return 'Invalid Amount'; + if (amount > availableBalance) return INSUFFICIENT_BALANCE; + }, + }} + render={({ field }) => ( + +
    + + +
    + + {displayDenom} + +
    + ), + sx: textFieldInputPropStyles, + }} + /> + )} + /> + ); +}; + +export default AmountInputField; diff --git a/frontend/src/app/(routes)/multiops/components/FileUpload.tsx b/frontend/src/app/(routes)/multiops/components/FileUpload.tsx new file mode 100644 index 000000000..1da0015e2 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/FileUpload.tsx @@ -0,0 +1,173 @@ +import { IconButton } from '@mui/material'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; +import ClearIcon from '@mui/icons-material/Clear'; +import { MULTIOPS_MSG_TYPES, MULTIOPS_SAMPLE_FILES } from '@/utils/constants'; + +interface FileUploadProps { + onFileContents: (content: string, type: string) => boolean; + msgType: string; + resetMessages: () => void; + messagesCount: number; +} + +const FileUpload = (props: FileUploadProps) => { + const { onFileContents, resetMessages, msgType, messagesCount } = props; + const [uploadedFiles, setUploadedFiles] = useState([]); + + useEffect(() => { + if (!messagesCount) { + setUploadedFiles([]); + } + }, [messagesCount]); + + return ( +
    +
    +
    +
    { + switch (msgType) { + case MULTIOPS_MSG_TYPES.send: + window.open( + MULTIOPS_SAMPLE_FILES.send, + '_blank', + 'noopener,noreferrer' + ); + break; + case MULTIOPS_MSG_TYPES.delegate: + window.open( + MULTIOPS_SAMPLE_FILES.delegate, + '_blank', + 'noopener,noreferrer' + ); + break; + case MULTIOPS_MSG_TYPES.undelegate: + window.open( + MULTIOPS_SAMPLE_FILES.undelegate, + '_blank', + 'noopener,noreferrer' + ); + break; + case MULTIOPS_MSG_TYPES.redelegate: + window.open( + MULTIOPS_SAMPLE_FILES.redelegate, + '_blank', + 'noopener,noreferrer' + ); + break; + case MULTIOPS_MSG_TYPES.vote: + window.open( + MULTIOPS_SAMPLE_FILES.vote, + '_blank', + 'noopener,noreferrer' + ); + break; + case MULTIOPS_MSG_TYPES.deposit: + window.open( + MULTIOPS_SAMPLE_FILES.deposit, + '_blank', + 'noopener,noreferrer' + ); + break; + default: + alert('unknown message type'); + } + }} + > +
    Download sample CSV file
    + download +
    +
    +
    +
    { + document.getElementById('multiops_file')!.click(); + }} + > + {uploadedFiles.length ? ( +
    +
    + {uploadedFiles?.map((file) => ( +
    + {file}{' '} +
    + ))} +
    +
    { + setUploadedFiles([]); + resetMessages(); + e.stopPropagation(); + }} + className="flex gap-2 items-center" + > +
    + Clear uploads +
    + + + +
    +
    + ) : ( + <> + Upload file +
    Upload file here
    + + )} + { + const files = e.target.files; + if (!files) { + return; + } + const file = files[0]; + if (!file) { + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const contents = (e?.target?.result as string) || ''; + const isValid = onFileContents(contents, msgType); + if (isValid) { + setUploadedFiles((prev) => [...prev, file?.name]); + } + }; + reader.onerror = (e) => { + alert(e); + }; + reader.readAsText(file); + e.target.value = ''; + }} + /> + {uploadedFiles.length ? ( +
    + Click anywhere in the box to upload one more file +
    + ) : null} +
    +
    + ); +}; + +export default FileUpload; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/Delegate.tsx b/frontend/src/app/(routes)/multiops/components/Messages/Delegate.tsx new file mode 100644 index 000000000..46de4137b --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/Delegate.tsx @@ -0,0 +1,226 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import React, { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { Decimal } from '@cosmjs/math'; +import { + Autocomplete, + CircularProgress, + Paper, + TextField, +} from '@mui/material'; +import { autoCompleteStyles, autoCompleteTextFieldStyles } from '../../styles'; +import AddressField from '../AddressField'; +import AmountInputField from '../AmountInputField'; +import { TxStatus } from '@/types/enums'; + +interface DelegateProps { + chainID: string; + address: string; + onDelegate: (payload: Msg) => void; + currency: Currency; + availableBalance: number; + baseURLs: string[]; + feeAmount: number; +} + +const Delegate: React.FC = (props) => { + const { + chainID, + address, + onDelegate, + currency, + availableBalance, + feeAmount, + } = props; + const { + handleSubmit, + control, + formState: { errors }, + setValue, + } = useForm({ + defaultValues: { + amount: '', + validator: null, + delegator: address, + }, + }); + const validators = useAppSelector( + (state: RootState) => state.staking.chains[chainID]?.validators + ); + const [data, setData] = useState<{ label: string; value: string }[]>([]); + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + + useEffect(() => { + if (validators) { + const data = []; + for (let i = 0; i < validators.activeSorted.length; i++) { + const validator = validators.active[validators.activeSorted[i]]; + const temp = { + label: validator.description.moniker, + value: validators.activeSorted[i], + }; + data.push(temp); + } + + for (let i = 0; i < validators.inactiveSorted.length; i++) { + const validator = validators.inactive[validators.inactiveSorted[i]]; + if (!validator.jailed) { + const temp = { + label: validator.description.moniker, + value: validators.inactiveSorted[i], + }; + data.push(temp); + } + } + setData(data); + } + }, [validators]); + + const onSubmit = (data: { + amount: string; + validator: null | { + value: string; + }; + delegator: string; + }) => { + if (data.validator) { + const baseAmount = Decimal.fromUserInput( + data.amount.toString(), + Number(currency?.coinDecimals) + ).atomics; + const msgDelegate = { + delegatorAddress: data.delegator, + validatorAddress: data.validator?.value, + amount: { + amount: baseAmount, + denom: currency?.coinMinimalDenom, + }, + }; + + onDelegate({ + typeUrl: '/cosmos.staking.v1beta1.MsgDelegate', + value: msgDelegate, + }); + } + }; + + return ( +
    +
    +
    Delegator
    + +
    + +
    +
    Select Validator
    +
    + ( + + option.value === value.value + } + options={data} + onChange={(event, item) => { + onChange(item); + }} + renderInput={(params) => ( + + )} + PaperComponent={({ children }) => ( + + {validatorsLoading === TxStatus.PENDING ? ( +
    + +
    + Fetching validators{' '} + {' '} +
    +
    + ) : ( + children + )} +
    + )} + /> + )} + /> +
    + + {errors.validator?.message} + +
    +
    +
    + +
    +
    +
    Enter Amount
    +
    + Available Balance: {availableBalance}{' '} + {currency.coinDenom} +
    +
    +
    + +
    + + {errors.amount?.message} + +
    +
    +
    + +
    + ); +}; + +export default Delegate; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/DelegateMessage.tsx b/frontend/src/app/(routes)/multiops/components/Messages/DelegateMessage.tsx new file mode 100644 index 000000000..e0125926e --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/DelegateMessage.tsx @@ -0,0 +1,44 @@ +import { REMOVE_ICON } from '@/constants/image-names'; +import { parseBalance } from '@/utils/denom'; +import { shortenAddress } from '@/utils/util'; +import Image from 'next/image'; +import React from 'react'; + +const DelegateMessage = (props: TxnMsgProps) => { + const { msg, index, currency, onDelete } = props; + return ( +
    +
    +
    + Delegate  + + {parseBalance( + [msg.value.amount], + currency.coinDecimals, + currency.coinMinimalDenom + )} +   + {currency.coinDenom}  + + to  + + {shortenAddress(msg.value.validatorAddress, 20)} + +
    +
    + {onDelete ? ( + onDelete(index)}> + Remove + + ) : null} +
    + ); +}; + +export default DelegateMessage; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/Deposit.tsx b/frontend/src/app/(routes)/multiops/components/Messages/Deposit.tsx new file mode 100644 index 000000000..a0f6dac01 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/Deposit.tsx @@ -0,0 +1,240 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getProposalsInDeposit } from '@/store/features/gov/govSlice'; +import { shortenName } from '@/utils/util'; +import { get } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import AddressField from '../AddressField'; +import { + Autocomplete, + CircularProgress, + Paper, + TextField, +} from '@mui/material'; +import { autoCompleteStyles, autoCompleteTextFieldStyles } from '../../styles'; +import { TxStatus } from '@/types/enums'; +import AmountInputField from '../AmountInputField'; +import { GovDepositMsg } from '@/txns/gov/deposit'; +import { Decimal } from '@cosmjs/math'; + +interface DepositProps { + address: string; + onDeposit: (payload: Msg) => void; + currency: Currency; + chainID: string; + availableBalance: number; + feeAmount: number; +} + +interface ProposalOption { + label: string; + value: string; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const renderOption = (props: any, option: ProposalOption) => ( +
  • +
    + #{option.value} + {shortenName(option.label, 36)} +
    +
  • +); + +const Deposit: React.FC = (props) => { + const { address, availableBalance, chainID, currency, feeAmount, onDeposit } = + props; + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { govV1, baseURL, restURLs: baseURLs } = getChainInfo(chainID); + + const [data, setData] = useState([]); + + const proposals = useAppSelector( + (state) => state.gov.chains?.[chainID]?.deposit?.proposals + ); + const proposalsLoading = useAppSelector( + (state) => state.gov.chains?.[chainID]?.deposit?.status + ); + const { + handleSubmit, + control, + formState: { errors }, + setValue, + } = useForm({ + defaultValues: { + proposalID: null, + amount: '', + from: address, + }, + }); + + const onSubmit = (data: { + proposalID: null | { + value: string; + }; + amount: string; + from: string; + }) => { + const amountInAtomics = Decimal.fromUserInput( + data.amount.toString(), + Number(currency.coinDecimals) + ).atomics; + + const msg = GovDepositMsg( + Number(data.proposalID?.value), + data.from, + Number(amountInAtomics), + currency.coinMinimalDenom + ); + console.log(msg); + onDeposit(msg); + }; + + useEffect(() => { + dispatch( + getProposalsInDeposit({ + baseURL, + baseURLs, + chainID, + govV1, + }) + ); + }, [chainID]); + + useEffect(() => { + const proposalsData: ProposalOption[] = []; + proposals?.forEach((proposal) => { + const proposalTitle = + get(proposal, 'content.title', get(proposal, 'title')) || + get(proposal, 'content.@type', get(proposal, 'message[0].@type', '')); + proposalsData.push({ + value: get(proposal, 'proposal_id') || get(proposal, 'id', ''), + label: shortenName(proposalTitle, 40), + }); + }); + setData(proposalsData); + }, [proposals]); + + return ( +
    +
    +
    Depositer
    + +
    +
    +
    Select Proposal
    + ( + + option.value === value.value + } + options={data} + onChange={(event, item) => { + onChange(item); + }} + renderOption={renderOption} + renderInput={(params) => ( + + )} + PaperComponent={({ children }) => ( + + {proposalsLoading === TxStatus.PENDING ? ( +
    + +
    + Fetching proposals{' '} + {' '} +
    +
    + ) : proposals.length ? ( + children + ) : ( +
    + - No Proposals - +
    + )} +
    + )} + /> + )} + /> +
    + + {errors.proposalID?.message} + +
    +
    +
    +
    +
    Enter Amount
    +
    + Available Balance: {availableBalance}{' '} + {currency.coinDenom} +
    +
    +
    + +
    + + {errors.amount?.message} + +
    +
    +
    + +
    + ); +}; + +export default Deposit; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/DepositMessage.tsx b/frontend/src/app/(routes)/multiops/components/Messages/DepositMessage.tsx new file mode 100644 index 000000000..111fb725e --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/DepositMessage.tsx @@ -0,0 +1,49 @@ +import { REMOVE_ICON } from '@/constants/image-names'; +import { parseBalance } from '@/utils/denom'; +import Image from 'next/image'; +import React from 'react'; + +interface DepositMessageProps { + msg: Msg; + onDelete: (index: number) => void; + index: number; + currency: Currency; +} + +const DepositMessage: React.FC = (props) => { + const { msg, index, onDelete, currency } = props; + return ( +
    +
    +
    + Deposit  + + {parseBalance( + msg.value.amount, + currency.coinDecimals, + currency.coinMinimalDenom + )} +   + {currency.coinDenom}  + +  on  + proposal  + #{Number(msg.value.proposalId)} +
    +
    + {onDelete ? ( + onDelete(index)}> + Remove + + ) : null} +
    + ); +}; + +export default DepositMessage; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/Redelegate.tsx b/frontend/src/app/(routes)/multiops/components/Messages/Redelegate.tsx new file mode 100644 index 000000000..aed84d933 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/Redelegate.tsx @@ -0,0 +1,397 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import React, { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { Decimal } from '@cosmjs/math'; +import { + Autocomplete, + CircularProgress, + Paper, + TextField, +} from '@mui/material'; +import { autoCompleteStyles, autoCompleteTextFieldStyles } from '../../styles'; +import { getDelegations } from '@/store/features/staking/stakeSlice'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import AddressField from '../AddressField'; +import { TxStatus } from '@/types/enums'; +import { getFormattedAmount, getParsedAmount } from './Undelegate'; +import AmountInputField from '../AmountInputField'; + +interface RedelegateProps { + chainID: string; + address: string; + onRedelegate: (payload: Msg) => void; + currency: Currency; + baseURLs: string[]; + feeAmount: number; +} + +interface StakeBal { + amount: string; + denom: string; +} + +interface ValidatorOption { + label: string; + value: string; + amount: StakeBal; +} + +const isValueExists = ( + valueToCheck: string, + valsData: ValidatorOption[] +): boolean => { + return valsData.some((destVal) => destVal.value === valueToCheck); +}; + +const Redelegate: React.FC = (props) => { + const { chainID, address, onRedelegate, currency } = props; + const { getChainInfo } = useGetChainInfo(); + const { restURLs } = getChainInfo(chainID); + const dispatch = useAppDispatch(); + + const { + handleSubmit, + control, + formState: { errors }, + setValue, + getValues, + } = useForm({ + defaultValues: { + amount: '', + validatorSrcAddress: null, + validatorDstAddress: null, + delegator: address, + }, + }); + + const validators = useAppSelector( + (state: RootState) => state.staking.chains[chainID]?.validators + ); + + const delegations = useAppSelector( + (state: RootState) => state.staking.chains[chainID]?.delegations + ); + + const delegationsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.delegations.status + ); + + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + + const [selectedValBal, setSelectedValBal] = useState({ + amount: '', + denom: '', + }); + + const [data, setData] = useState([]); + const [destVals, setDestVals] = useState([]); + + useEffect(() => { + const srcValsData = []; + const destValsData: ValidatorOption[] = []; + + const totalDelegations = + delegations?.delegations?.delegation_responses || []; + + for (let j = 0; j < totalDelegations.length; j++) { + const del = totalDelegations[j]; + + for (let i = 0; i < validators.activeSorted.length; i++) { + const validator = validators.active[validators.activeSorted[i]]; + if (del?.delegation?.validator_address === validator.operator_address) { + const temp = { + label: validator.description.moniker, + value: validators.activeSorted[i], + amount: del.balance, + }; + + srcValsData.push(temp); + } + + const temp = { + label: validator.description.moniker, + value: validators.activeSorted[i], + amount: del.balance, + }; + if (!isValueExists(temp.value, destValsData)) destValsData.push(temp); + } + + for (let i = 0; i < validators.inactiveSorted.length; i++) { + const validator = validators.inactive[validators.inactiveSorted[i]]; + if (!validator.jailed) { + if ( + del?.delegation?.validator_address === validator.operator_address + ) { + const temp = { + label: validator.description.moniker, + value: validators.inactiveSorted[i], + amount: del.balance, + }; + + srcValsData.push(temp); + } + + const temp = { + label: validator.description.moniker, + value: validators.activeSorted[i], + amount: del.balance, + }; + if (!isValueExists(temp.value, destValsData)) destValsData.push(temp); + } + } + } + + setData(srcValsData); + setDestVals(destValsData); + }, [validators]); + + const onSubmit = (data: { + amount: string; + validatorSrcAddress: null | { + value: string; + }; + validatorDstAddress: null | { + value: string; + }; + delegator: string; + }) => { + if (data?.validatorSrcAddress && data?.validatorDstAddress) { + const baseAmount = Decimal.fromUserInput( + data.amount.toString(), + Number(currency?.coinDecimals) + ).atomics; + const msgRedelegate = { + delegatorAddress: data.delegator, + validatorSrcAddress: data.validatorSrcAddress?.value, + validatorDstAddress: data.validatorDstAddress?.value, + amount: { + amount: baseAmount, + denom: currency?.coinMinimalDenom, + }, + }; + + onRedelegate({ + typeUrl: '/cosmos.staking.v1beta1.MsgBeginRedelegate', + value: msgRedelegate, + }); + } + }; + + useEffect(() => { + dispatch(getDelegations({ address, chainID, baseURLs: restURLs })); + }, [chainID]); + + return ( +
    +
    +
    Delegator
    + +
    + +
    +
    Select Validator
    +
    +
    + ( + + option.value === value.value + } + options={data} + onChange={(event, item) => { + onChange(item); + setSelectedValBal({ + amount: + ( + Number(item?.amount?.amount) / + 10 ** currency.coinDecimals + ).toFixed(6) || '', + denom: item?.amount?.denom || '', + }); + }} + renderInput={(params) => ( + + )} + PaperComponent={({ children }) => ( + + {validatorsLoading === TxStatus.PENDING ? ( +
    + +
    + Fetching validators{' '} + {' '} +
    +
    + ) : ( + children + )} +
    + )} + /> + )} + /> +
    + + {errors.validatorSrcAddress?.message} + +
    +
    +
    + ( + + option.value === value.value + } + options={destVals} + onChange={(event, item) => { + onChange(item); + }} + renderInput={(params) => ( + + )} + PaperComponent={({ children }) => ( + + {validatorsLoading === TxStatus.PENDING ? ( +
    + +
    + Fetching validators{' '} + {' '} +
    +
    + ) : ( + children + )} +
    + )} + /> + )} + /> +
    + + {errors.validatorDstAddress?.message} + +
    +
    +
    +
    + +
    +
    +
    Enter Amount
    + {getValues('validatorSrcAddress') ? ( +
    + {delegationsLoading === TxStatus.PENDING ? ( +
    + + Fetching delegations +
    + ) : ( +
    + Available to Redelegate:{' '} + {getFormattedAmount(selectedValBal?.amount)}{' '} + {currency.coinDenom} +
    + )} +
    + ) : null} +
    +
    + +
    + + {errors.amount?.message} + +
    +
    +
    + +
    + ); +}; + +export default Redelegate; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/RedelegateMessage.tsx b/frontend/src/app/(routes)/multiops/components/Messages/RedelegateMessage.tsx new file mode 100644 index 000000000..026705569 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/RedelegateMessage.tsx @@ -0,0 +1,49 @@ +import { REMOVE_ICON } from '@/constants/image-names'; +import { TxnMsgProps } from '@/types/multisig'; +import { parseBalance } from '@/utils/denom'; +import { shortenName } from '@/utils/util'; +import Image from 'next/image'; +import React from 'react'; + +const RedelegateMessage: React.FC = (props) => { + const { msg, index, currency, onDelete } = props; + return ( +
    +
    +
    + ReDelegate  + + {parseBalance( + [msg.value.amount], + currency.coinDecimals, + currency.coinMinimalDenom + )} +   + {currency.coinDenom}  + + from  + + {shortenName(msg.value.validatorSrcAddress, 20)}  + + to  + + {shortenName(msg.value.validatorDstAddress, 20)} + +
    +
    + {onDelete ? ( + onDelete(index)}> + Remove + + ) : null} +
    + ); +}; + +export default RedelegateMessage; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/Send.tsx b/frontend/src/app/(routes)/multiops/components/Messages/Send.tsx new file mode 100644 index 000000000..b4bf69193 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/Send.tsx @@ -0,0 +1,164 @@ +import { TextField } from '@mui/material'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { fromBech32 } from '@cosmjs/encoding'; +import { Decimal } from '@cosmjs/math'; +import { sendTxnTextFieldStyles } from '../../styles'; +import AddressField from '../AddressField'; +import AmountInputField from '../AmountInputField'; + +interface SendProps { + address: string; + onSend: (payload: Msg) => void; + currency: Currency; + availableBalance: number; + feeAmount: number; +} + +const Send: React.FC = (props) => { + const { address, onSend, currency, availableBalance, feeAmount } = props; + + const { + handleSubmit, + control, + formState: { errors }, + setValue, + } = useForm({ + defaultValues: { + amount: '', + recipient: '', + from: address, + }, + }); + + const onSubmit = (data: { + amount: string; + recipient: string; + from: string; + }) => { + const amountInAtomics = Decimal.fromUserInput( + data.amount.toString(), + Number(currency.coinDecimals) + ).atomics; + + const msgSend = { + fromAddress: data.from, + toAddress: data.recipient, + amount: [ + { + amount: amountInAtomics, + denom: currency.coinMinimalDenom, + }, + ], + }; + + const msg = { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: msgSend, + }; + + onSend(msg); + }; + return ( +
    +
    +
    From
    + +
    +
    +
    Recepient Address
    +
    + { + try { + fromBech32(value); + return true; + } catch (error) { + return false; + } + }, + }} + render={({ field, fieldState: { error } }) => ( + + )} + /> +
    + + {errors?.recipient?.type === 'validate' + ? 'Invalid recipient address' + : errors?.recipient?.message} + +
    +
    +
    +
    +
    +
    Enter Amount
    +
    + Available Balance: {availableBalance}{' '} + {currency.coinDenom} +
    +
    +
    + +
    + + {errors.amount?.message} + +
    +
    +
    + +
    + ); +}; + +export default Send; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/SendMessage.tsx b/frontend/src/app/(routes)/multiops/components/Messages/SendMessage.tsx new file mode 100644 index 000000000..08c4307c2 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/SendMessage.tsx @@ -0,0 +1,54 @@ +import { REMOVE_ICON } from '@/constants/image-names'; +import useGetAllAssets from '@/custom-hooks/multisig/useGetAllAssets'; +import { shortenAddress } from '@/utils/util'; +import Image from 'next/image'; +import React from 'react'; + +interface TxnMsgProps { + msg: Msg; + onDelete: (index: number) => void; + currency: Currency; + index: number; + chainID: string; +} + +const SendMessage = (props: TxnMsgProps) => { + const { msg, index, onDelete, chainID } = props; + const { getParsedAsset } = useGetAllAssets(); + const { assetInfo } = getParsedAsset({ + amount: msg.value?.amount?.[0]?.amount, + chainID, + denom: msg.value?.amount?.[0]?.denom, + }); + return ( +
    +
    +
    + Send  + + {assetInfo?.amountInDenom} +   + {assetInfo?.displayDenom}  + + to  + + {shortenAddress(msg.value.toAddress, 20)} + +
    +
    + {onDelete ? ( + onDelete(index)}> + Remove + + ) : null} +
    + ); +}; + +export default SendMessage; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/Undelegate.tsx b/frontend/src/app/(routes)/multiops/components/Messages/Undelegate.tsx new file mode 100644 index 000000000..66ff1c90a --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/Undelegate.tsx @@ -0,0 +1,305 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getDelegations } from '@/store/features/staking/stakeSlice'; +import React, { useEffect, useState } from 'react'; +import { Decimal } from '@cosmjs/math'; +import { Controller, useForm } from 'react-hook-form'; +import { + Autocomplete, + CircularProgress, + Paper, + TextField, +} from '@mui/material'; +import { autoCompleteStyles, autoCompleteTextFieldStyles } from '../../styles'; +import AddressField from '../AddressField'; +import AmountInputField from '../AmountInputField'; +import { TxStatus } from '@/types/enums'; + +interface UnDelegateProps { + chainID: string; + address: string; + onUndelegate: (payload: Msg) => void; + currency: Currency; + baseURLs: string[]; + feeAmount: number; +} + +interface StakeBal { + amount: string; + denom: string; +} + +export const getParsedAmount = (amount: string) => { + const parsedAmount = parseFloat(amount); + if (isNaN(parsedAmount)) return 0; + return parsedAmount; +}; + +export const getFormattedAmount = (amount: string): string => { + const parsedAmount = getParsedAmount(amount); + if (parsedAmount < 0.01) return '< 0.01'; + return parsedAmount.toString(); +}; + +const Undelegate = (props: UnDelegateProps) => { + const { chainID, address, currency, onUndelegate, baseURLs } = props; + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { restURLs } = getChainInfo(chainID); + const validators = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators + ); + + const delegations = useAppSelector( + (state) => state.staking.chains?.[chainID]?.delegations + ); + + const delegationsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.delegations.status + ); + + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + + useEffect(() => { + dispatch(getDelegations({ address, chainID, baseURLs: restURLs })); + }, [chainID]); + + const [selectedValBal, setSelectedValBal] = useState({ + amount: '', + denom: '', + }); + + const [data, setData] = useState< + { label: string; value: string; amount: StakeBal }[] + >([]); + + const { + handleSubmit, + control, + formState: { errors }, + setValue, + getValues, + } = useForm({ + defaultValues: { + amount: '', + validator: null, + delegator: address, + }, + }); + + useEffect(() => { + const data = []; + + const totalDelegations = + delegations?.delegations?.delegation_responses || []; + + for (let j = 0; j < totalDelegations.length; j++) { + const del = totalDelegations[j]; + + for (let i = 0; i < validators.activeSorted.length; i++) { + const validator = validators.active[validators.activeSorted[i]]; + if (del?.delegation?.validator_address === validator.operator_address) { + const temp = { + label: validator.description.moniker, + value: validators.activeSorted[i], + amount: del.balance, + }; + + data.push(temp); + } + } + + for (let i = 0; i < validators.inactiveSorted.length; i++) { + const validator = validators.inactive[validators.inactiveSorted[i]]; + if (!validator.jailed) { + if ( + del?.delegation?.validator_address === validator.operator_address + ) { + const temp = { + label: validator.description.moniker, + value: validators.inactiveSorted[i], + amount: del.balance, + }; + + data.push(temp); + } + } + } + } + + setData(data); + }, [validators]); + + const onSubmit = (data: { + amount: string; + validator: null | { + value: string; + }; + delegator: string; + }) => { + if (data.validator) { + const baseAmount = Decimal.fromUserInput( + data.amount.toString(), + Number(currency?.coinDecimals) + ).atomics; + const msgUnDelegate = { + delegatorAddress: data.delegator, + validatorAddress: data.validator?.value, + amount: { + amount: baseAmount, + denom: currency?.coinMinimalDenom, + }, + }; + + onUndelegate({ + typeUrl: '/cosmos.staking.v1beta1.MsgUndelegate', + value: msgUnDelegate, + }); + } + }; + + useEffect(() => { + dispatch( + getDelegations({ + baseURLs, + chainID, + address, + }) + ); + }, [chainID]); + + return ( +
    +
    +
    Delegator
    + +
    +
    +
    Select Validator
    + ( + + option.value === value.value + } + options={data} + onChange={(event, item) => { + onChange(item); + setSelectedValBal({ + amount: + ( + Number(item?.amount?.amount) / + 10 ** currency.coinDecimals + ).toFixed(6) || '', + denom: item?.amount?.denom || '', + }); + }} + renderInput={(params) => ( + + )} + PaperComponent={({ children }) => ( + + {validatorsLoading === TxStatus.PENDING ? ( +
    + +
    + Fetching validators{' '} + {' '} +
    +
    + ) : ( + children + )} +
    + )} + /> + )} + /> +
    + + {errors.validator?.message} + +
    +
    +
    +
    +
    Enter Amount
    + {getValues('validator') ? ( +
    + {delegationsLoading === TxStatus.PENDING ? ( +
    + + Fetching delegations +
    + ) : ( +
    + Available to Undelegate:{' '} + {getFormattedAmount(selectedValBal?.amount)}{' '} + {currency.coinDenom} +
    + )} +
    + ) : null} +
    +
    + +
    + + {errors.amount?.message} + +
    +
    +
    + +
    + ); +}; + +export default Undelegate; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/UndelegateMessage.tsx b/frontend/src/app/(routes)/multiops/components/Messages/UndelegateMessage.tsx new file mode 100644 index 000000000..6c98be78a --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/UndelegateMessage.tsx @@ -0,0 +1,45 @@ +import { REMOVE_ICON } from '@/constants/image-names'; +import { TxnMsgProps } from '@/types/multisig'; +import { parseBalance } from '@/utils/denom'; +import { shortenAddress } from '@/utils/util'; +import Image from 'next/image'; +import React from 'react'; + +const UndelegateMessage: React.FC = (props) => { + const { msg, index, currency, onDelete } = props; + return ( +
    +
    +
    + UnDelegate  + + {parseBalance( + [msg.value.amount], + currency.coinDecimals, + currency.coinMinimalDenom + )} +   + {currency.coinDenom}  + + from  + + {shortenAddress(msg.value.validatorAddress, 20)} + +
    +
    + {onDelete ? ( + onDelete(index)}> + Remove + + ) : null} +
    + ); +}; + +export default UndelegateMessage; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/Vote.tsx b/frontend/src/app/(routes)/multiops/components/Messages/Vote.tsx new file mode 100644 index 000000000..f6e45ccfb --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/Vote.tsx @@ -0,0 +1,305 @@ +import { msgVoteTypeUrl } from '@/txns/gov/vote'; +import React, { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import AddressField from '../AddressField'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getProposalsInVoting } from '@/store/features/gov/govSlice'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { get } from 'lodash'; +import { + Autocomplete, + CircularProgress, + Paper, + TextField, +} from '@mui/material'; +import { autoCompleteStyles, autoCompleteTextFieldStyles } from '../../styles'; +import { TxStatus } from '@/types/enums'; +import { shortenName } from '@/utils/util'; + +interface VoteProps { + address: string; + onVote: (payload: Msg) => void; + currency: Currency; + chainID: string; +} + +interface ProposalOption { + label: string; + value: string; +} + +interface VoteOption { + label: string; + value: number; +} + +const voteOptions: VoteOption[] = [ + { + label: 'Yes', + value: 1, + }, + { + label: 'No', + value: 3, + }, + { + label: 'Abstain', + value: 2, + }, + { + label: 'No With Veto', + value: 4, + }, +]; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const renderProposalOption = (props: any, option: ProposalOption) => ( +
  • +
    + #{option.value} + {shortenName(option.label, 36)} +
    +
  • +); + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const renderVoteOption = (props: any, option: VoteOption) => ( +
  • +
    + {option.label} +
    +
  • +); + +const Vote: React.FC = (props) => { + const { address, onVote, chainID } = props; + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { govV1, baseURL, restURLs: baseURLs } = getChainInfo(chainID); + const proposals = useAppSelector( + (state) => state.gov.chains?.[chainID]?.active?.proposals + ); + const proposalsLoading = useAppSelector( + (state) => state.gov.chains?.[chainID]?.active?.status + ); + + const { + handleSubmit, + control, + formState: { errors }, + } = useForm({ + defaultValues: { + proposalID: null, + voteOption: null, + from: address, + }, + }); + + const [data, setData] = useState([]); + + const onSubmit = (data: { + proposalID: null | { + value: string; + }; + voteOption: null | { + value: string; + }; + from: string; + }) => { + const msgVote = { + voter: data.from, + option: Number(data.voteOption?.value), + proposalId: Number(data.proposalID?.value), + }; + + const msg = { + typeUrl: msgVoteTypeUrl, + value: msgVote, + }; + + onVote(msg); + }; + + useEffect(() => { + dispatch( + getProposalsInVoting({ + baseURL, + baseURLs, + chainID, + govV1, + voter: address, + }) + ); + }, [chainID]); + + useEffect(() => { + const proposalsData: ProposalOption[] = []; + proposals?.forEach((proposal) => { + const proposalTitle = + get(proposal, 'content.title', get(proposal, 'title')) || + get(proposal, 'content.@type', get(proposal, 'message[0].@type', '')); + proposalsData.push({ + value: get(proposal, 'proposal_id') || get(proposal, 'id', ''), + label: shortenName(proposalTitle, 40), + }); + }); + setData(proposalsData); + }, [proposals]); + + return ( +
    +
    +
    Voter
    + +
    +
    +
    Select Proposal
    + ( + + option.value === value.value + } + options={data} + onChange={(event, item) => { + onChange(item); + }} + renderOption={renderProposalOption} + renderInput={(params) => ( + + )} + PaperComponent={({ children }) => ( + + {proposalsLoading === TxStatus.PENDING ? ( +
    + +
    + Fetching proposals{' '} + {' '} +
    +
    + ) : proposals.length ? ( + children + ) : ( +
    + - No Proposals - +
    + )} +
    + )} + /> + )} + /> +
    + + {errors.proposalID?.message} + +
    +
    +
    +
    Select Vote Option
    + ( + + option.value === value.value + } + options={voteOptions} + onChange={(event, item) => { + onChange(item); + }} + renderOption={renderVoteOption} + renderInput={(params) => ( + + )} + PaperComponent={({ children }) => ( + + {proposalsLoading === TxStatus.PENDING ? ( +
    + +
    + Fetching proposals{' '} + {' '} +
    +
    + ) : ( + children + )} +
    + )} + /> + )} + /> +
    + + {errors.proposalID?.message} + +
    +
    + +
    + ); +}; + +export default Vote; diff --git a/frontend/src/app/(routes)/multiops/components/Messages/VoteMessage.tsx b/frontend/src/app/(routes)/multiops/components/Messages/VoteMessage.tsx new file mode 100644 index 000000000..4fd8eb4cc --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/Messages/VoteMessage.tsx @@ -0,0 +1,49 @@ +import { REMOVE_ICON } from '@/constants/image-names'; +import Image from 'next/image'; +import React from 'react'; + +interface VoteMessageProps { + msg: Msg; + onDelete: (index: number) => void; + index: number; +} + +const voteOptions: Record = { + '1': 'Yes', + '2': 'Abstain', + '3': 'No', + '4': 'No With Veto', +}; + +const VoteMessage: React.FC = (props) => { + const { msg, index, onDelete } = props; + return ( +
    +
    + +
    + Vote  + + {voteOptions?.[msg.value.option.toString()]} + +  on  + proposal  + #{msg.value.proposalId} +
    +
    + {onDelete ? ( + onDelete(index)}> + Remove + + ) : null} +
    + ); +}; + +export default VoteMessage; diff --git a/frontend/src/app/(routes)/multiops/components/MessagesList.tsx b/frontend/src/app/(routes)/multiops/components/MessagesList.tsx new file mode 100644 index 000000000..519d17ad0 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/MessagesList.tsx @@ -0,0 +1,117 @@ +import { + DELEGATE_TYPE_URL, + DEPOSIT_TYPE_URL, + REDELEGATE_TYPE_URL, + SEND_TYPE_URL, + UNDELEGATE_TYPE_URL, + VOTE_TYPE_URL, +} from '@/utils/constants'; +import { Pagination } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { paginationComponentStyles } from '../../staking/styles'; +import SendMessage from './Messages/SendMessage'; +import DelegateMessage from './Messages/DelegateMessage'; +import UndelegateMessage from './Messages/UndelegateMessage'; +import RedelegateMessage from './Messages/RedelegateMessage'; +import VoteMessage from './Messages/VoteMessage'; +import DepositMessage from './Messages/DepositMessage'; + +const PER_PAGE = 5; + +const renderMessage = ( + msg: Msg, + index: number, + currency: Currency, + onDelete: (index: number) => void, + chainID: string +) => { + switch (msg.typeUrl) { + case SEND_TYPE_URL: + return SendMessage({ msg, index, currency, onDelete, chainID }); + case DELEGATE_TYPE_URL: + return DelegateMessage({ msg, index, currency, onDelete }); + case UNDELEGATE_TYPE_URL: + return UndelegateMessage({ msg, index, currency, onDelete }); + case REDELEGATE_TYPE_URL: + return RedelegateMessage({ msg, index, currency, onDelete }); + case VOTE_TYPE_URL: + return VoteMessage({ msg, index, onDelete }); + case DEPOSIT_TYPE_URL: + return DepositMessage({ msg, index, currency, onDelete }); + default: + return ''; + } +}; + +const MessagesList = ({ + messages, + currency, + onDeleteMsg, + chainID, +}: { + messages: Msg[]; + currency: Currency; + onDeleteMsg: (index: number) => void; + chainID: string; +}) => { + const [slicedMsgs, setSlicedMsgs] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + + useEffect(() => { + if (messages.length < PER_PAGE) { + setSlicedMsgs(messages); + } else { + const page = Math.ceil(messages.length / PER_PAGE); + setCurrentPage(page); + setSlicedMsgs( + messages?.slice( + (page - 1) * PER_PAGE, + (page - 1) * PER_PAGE + 1 * PER_PAGE + ) + ); + } + }, [messages]); + + return ( +
    +
    +
    + {slicedMsgs.map((msg, index) => { + return ( +
    + {renderMessage( + msg, + index + PER_PAGE * (currentPage - 1), + currency, + onDeleteMsg, + chainID + )} +
    + ); + })} +
    + +
    PER_PAGE + ? 'mt-2 flex justify-end opacity-100' + : 'mt-2 flex justify-end opacity-0' + } + > + { + setCurrentPage(v); + setSlicedMsgs(messages?.slice((v - 1) * PER_PAGE, v * PER_PAGE)); + }} + /> +
    +
    +
    + ); +}; + +export default MessagesList; diff --git a/frontend/src/app/(routes)/multiops/components/SelectMsgType.tsx b/frontend/src/app/(routes)/multiops/components/SelectMsgType.tsx new file mode 100644 index 000000000..3bf084266 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/SelectMsgType.tsx @@ -0,0 +1,54 @@ +import { MULTIOPS_MSG_TYPES } from '@/utils/constants'; +import { + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, +} from '@mui/material'; +import React from 'react'; +import { selectTxnStyles } from '../styles'; + +const SelectMsgType = ({ + handleMsgTypeChange, + msgType, +}: { + handleMsgTypeChange: (event: SelectChangeEvent) => void; + msgType: string; +}) => { + return ( +
    +
    Select transaction type
    + + + Select Transaction + + + +
    + ); +}; + +export default SelectMsgType; diff --git a/frontend/src/app/(routes)/multisig/components/SelectTransactionType.tsx b/frontend/src/app/(routes)/multiops/components/SelectTransactionType.tsx similarity index 82% rename from frontend/src/app/(routes)/multisig/components/SelectTransactionType.tsx rename to frontend/src/app/(routes)/multiops/components/SelectTransactionType.tsx index f7de0c736..8431f8217 100644 --- a/frontend/src/app/(routes)/multisig/components/SelectTransactionType.tsx +++ b/frontend/src/app/(routes)/multiops/components/SelectTransactionType.tsx @@ -8,7 +8,7 @@ interface SelectTransactionTypeProps { const SelectTransactionType: React.FC = (props) => { const { isFileUpload, onSelect } = props; return ( -
    +
    onSelect(false)} @@ -18,7 +18,7 @@ const SelectTransactionType: React.FC = (props) => {
    ) : null} -
    Add Manually
    +
    Add Manually
    onSelect(true)}> -
    File Upload
    +
    Upload File
    ); diff --git a/frontend/src/app/(routes)/multiops/components/TxnBuilder.tsx b/frontend/src/app/(routes)/multiops/components/TxnBuilder.tsx new file mode 100644 index 000000000..d9f6c8043 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/components/TxnBuilder.tsx @@ -0,0 +1,499 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import SelectTransactionType from './SelectTransactionType'; +import { CircularProgress, SelectChangeEvent, TextField } from '@mui/material'; +import { + MULTIOPS_MSG_TYPES, + MULTIOPS_NOTE, + NO_MESSAGES_ILLUSTRATION, +} from '@/utils/constants'; +import { sendTxnTextFieldStyles } from '../styles'; +import Send from './Messages/Send'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { parseBalance } from '@/utils/denom'; +import { getBalances } from '@/store/features/bank/bankSlice'; +import MessagesList from './MessagesList'; +import Image from 'next/image'; +import { Controller, useForm } from 'react-hook-form'; +import { + resetTx, + txExecuteMultiMsg, +} from '@/store/features/multiops/multiopsSlice'; +import { TxStatus } from '@/types/enums'; +import Delegate from './Messages/Delegate'; +import Undelegate from './Messages/Undelegate'; +import Redelegate from './Messages/Redelegate'; +import Vote from './Messages/Vote'; +import Deposit from './Messages/Deposit'; +import FileUpload from './FileUpload'; +import { setError } from '@/store/features/common/commonSlice'; +import { + parseDelegateMsgsFromContent, + parseDepositMsgsFromContent, + parseReDelegateMsgsFromContent, + parseSendMsgsFromContent, + parseUnDelegateMsgsFromContent, + parseVoteMsgsFromContent, +} from '@/utils/parseMsgs'; +import SelectMsgType from './SelectMsgType'; + +const TxnBuilder = ({ chainID }: { chainID: string }) => { + const dispatch = useAppDispatch(); + const [isFileUpload, setIsFileUpload] = useState(false); + const [msgType, setMsgType] = useState('Send'); + const [messages, setMessages] = useState([]); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const basicChainInfo = getChainInfo(chainID); + const { + address, + baseURL, + restURLs: baseURLs, + feeAmount, + prefix, + rest, + rpc, + } = basicChainInfo; + + const txStatus = useAppSelector((state) => state.multiops.tx.status); + + const onSelect = (value: boolean) => { + setMessages([]); + setIsFileUpload(value); + }; + const handleMsgTypeChange = (event: SelectChangeEvent) => { + setMsgType(event.target.value); + }; + const balance = useAppSelector( + (state) => state.bank.balances?.[chainID]?.list + ); + const [availableBalance, setAvailableBalance] = useState(0); + + const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); + const currency = { + coinDenom: displayDenom, + coinDecimals: decimals, + coinMinimalDenom: minimalDenom, + }; + + const onDeleteMsg = (index: number) => { + const arr = messages.filter((_, i) => i !== index); + setMessages(arr); + }; + + const { handleSubmit, control } = useForm({ + defaultValues: { + gas: 900000, + memo: '', + fees: feeAmount * 10 ** currency.coinDecimals, + }, + }); + + const onSubmit = (data: { gas: number; memo: string; fees: number }) => { + dispatch( + txExecuteMultiMsg({ + basicChainInfo, + aminoConfig: basicChainInfo.aminoConfig, + denom: currency.coinMinimalDenom, + feeAmount: data.fees, + feegranter: '', + memo: data.memo, + msgs: messages, + prefix, + rest, + rpc, + gas: data.gas, + address, + }) + ); + }; + + const onFileContents = (content: string, type: string) => { + let isValid = false; + switch (type) { + case MULTIOPS_MSG_TYPES.send: { + const [parsedTxns, error] = parseSendMsgsFromContent(address, content); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + setMessages((prev) => [...prev, ...parsedTxns]); + isValid = true; + } + break; + } + case MULTIOPS_MSG_TYPES.delegate: { + const [parsedTxns, error] = parseDelegateMsgsFromContent( + address, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + setMessages((prev) => [...prev, ...parsedTxns]); + isValid = true; + } + break; + } + case MULTIOPS_MSG_TYPES.redelegate: { + const [parsedTxns, error] = parseReDelegateMsgsFromContent( + address, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + setMessages((prev) => [...prev, ...parsedTxns]); + isValid = true; + } + break; + } + case MULTIOPS_MSG_TYPES.undelegate: { + const [parsedTxns, error] = parseUnDelegateMsgsFromContent( + address, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + setMessages((prev) => [...prev, ...parsedTxns]); + isValid = true; + } + break; + } + case MULTIOPS_MSG_TYPES.vote: { + const [parsedTxns, error] = parseVoteMsgsFromContent(address, content); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + setMessages((prev) => [...prev, ...parsedTxns]); + isValid = true; + } + break; + } + case MULTIOPS_MSG_TYPES.deposit: { + const [parsedTxns, error] = parseDepositMsgsFromContent( + address, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + setMessages((prev) => [...prev, ...parsedTxns]); + isValid = true; + } + break; + } + default: + setMessages([]); + } + return isValid; + }; + + useEffect(() => { + if (balance) { + setAvailableBalance( + parseBalance(balance, currency.coinDecimals, currency.coinMinimalDenom) + ); + } + }, [balance]); + + useEffect(() => { + if (address && chainID) { + dispatch( + getBalances({ + address, + baseURL, + baseURLs, + chainID, + }) + ); + } + }, [address]); + + useEffect(() => { + dispatch(resetTx()); + }, []); + + return ( +
    +
    +
    +
    Create Transaction
    +
    + Multiops allows to create single transaction with multiple + messages of same or different type. +
    +
    +
    + +
    +
    +
    +
    +
    + {isFileUpload ? ( +
    + + setMessages([])} + msgType={msgType} + messagesCount={messages.length} + /> +
    + ) : ( +
    + +
    + {msgType === MULTIOPS_MSG_TYPES.send ? ( + { + setMessages([...messages, payload]); + }} + availableBalance={availableBalance} + currency={currency} + feeAmount={feeAmount} + /> + ) : null} + {msgType === MULTIOPS_MSG_TYPES.delegate ? ( + { + setMessages([...messages, payload]); + }} + baseURLs={baseURLs} + feeAmount={feeAmount} + /> + ) : null} + {msgType === MULTIOPS_MSG_TYPES.undelegate ? ( + { + setMessages([...messages, payload]); + }} + baseURLs={baseURLs} + feeAmount={feeAmount} + /> + ) : null} + {msgType === MULTIOPS_MSG_TYPES.redelegate ? ( + { + setMessages([...messages, payload]); + }} + baseURLs={baseURLs} + feeAmount={feeAmount} + /> + ) : null} + {msgType === MULTIOPS_MSG_TYPES.vote ? ( + { + setMessages([...messages, payload]); + }} + /> + ) : null} + {msgType === MULTIOPS_MSG_TYPES.deposit ? ( + { + setMessages([...messages, payload]); + }} + feeAmount={feeAmount} + /> + ) : null} +
    +
    + )} +
    +
    +
    +
    +
    +
    + Messages {messages.length ? <>({messages.length}) : null} +
    + {messages.length ? ( +
    { + setMessages([]); + }} + > + Clear All +
    + ) : null} +
    +
    + {messages.length ? ( +
    + +
    +
    +
    +
    +
    + Enter Gas +
    + ( + + )} + /> +
    +
    +
    + Enter Memo (optional) +
    + ( + + )} + /> +
    +
    +
    + {MULTIOPS_NOTE} +
    +
    +
    +
    + ) : ( +
    + No Messages +
    + )} +
    +
    +
    +
    +
    + +
    +
    +
    + ); +}; + +export default TxnBuilder; diff --git a/frontend/src/app/(routes)/multiops/error.tsx b/frontend/src/app/(routes)/multiops/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/multiops/loading.tsx b/frontend/src/app/(routes)/multiops/loading.tsx new file mode 100644 index 000000000..0b08e2215 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/multiops/multiops.css b/frontend/src/app/(routes)/multiops/multiops.css new file mode 100644 index 000000000..09ee8ec04 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/multiops.css @@ -0,0 +1,12 @@ +.msg-btn { + @apply border-[0.25px] h-8 flex items-center justify-center rounded-lg px-4 py-2 text-[14px] border-[#FFFFFF30] hover:bg-[#ffffff10]; +} + +.msg-btn-selected { + @apply bg-[#FFFFFF10] border-transparent; +} + +.upload-box { + @apply rounded-3xl px-6 py-[10.5px] flex items-center justify-center cursor-pointer gap-2 h-[55px]; + border: 2px dashed #ffffff20; +} diff --git a/frontend/src/app/(routes)/multiops/page.tsx b/frontend/src/app/(routes)/multiops/page.tsx new file mode 100644 index 000000000..3ecd4151d --- /dev/null +++ b/frontend/src/app/(routes)/multiops/page.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import Multiops from './Multiops'; +import './multiops.css'; + +const page = () => { + return ; +}; + +export default page; diff --git a/frontend/src/app/(routes)/multiops/styles.ts b/frontend/src/app/(routes)/multiops/styles.ts new file mode 100644 index 000000000..3c661c231 --- /dev/null +++ b/frontend/src/app/(routes)/multiops/styles.ts @@ -0,0 +1,148 @@ +export const selectTxnStyles = { + '& .MuiOutlinedInput-input': { + color: 'white', + }, + '& .MuiOutlinedInput-root': { + padding: '0px !important', + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: 'white', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none !important', + }, + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + borderRadius: '8px', + mb: '40px', +}; + +export const sendTxnTextFieldStyles = { + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .Mui-disabled': { + '-webkit-text-fill-color': '#ffffff6b !important', + }, + borderRadius: '8px', + '& .MuiOutlinedInput-root': { + border: '1px solid transparent', + borderRadius: '8px', + }, + '& .Mui-focused': { + border: '1px solid #ffffff4a', + borderRadius: '8px', + }, +}; + +export const textFieldStyles = { + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '1px solid transparent', + borderRadius: '8px', + }, + '& .Mui-focused': { + border: '1px solid #ffffff4a', + borderRadius: '8px', + }, +}; + +export const textFieldInputPropStyles = { + input: { + color: 'white', + fontSize: '14px', + padding: 2, + }, + 'input[type=number]::-webkit-inner-spin-button ': { + WebkitAppearance: 'none', + appearance: 'none', + }, +}; + +export const autoCompleteStyles = { + '& .MuiAutocomplete-inputRoot': { + padding: '7px !important', + '& input': { + color: 'white', + }, + '& button': { + color: 'white', + }, + }, + '& .MuiAutocomplete-popper': { + display: 'none !important', + }, + borderRadius: '8px', + width: '100%', +}; + +export const autoCompleteTextFieldStyles = { + '& .MuiTypography-body1': { + color: 'white', + fontSize: '14px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + borderRadius: '8px', +}; + +export const customMUITextFieldStyles = { + '& .MuiInputBase-input': { + color: 'white', + fontSize: '14px', + fontWeight: 200, + fontFamily: 'Libre Franklin', + lineHeight: '19px', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '0.25px solid #ffffff10', + borderRadius: '100px', + height: '40px', + }, + '& .Mui-focused': { + border: '0.25px solid #ffffff4a', + borderRadius: '100px', + }, +}; + +export const customMessageValueFieldStyles = { + '& .MuiInputBase-input': { + color: 'white', + fontSize: '14px', + fontWeight: 200, + fontFamily: 'Libre Franklin', + lineHeight: '19px', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '0.25px solid #ffffff10', + borderRadius: '16px', + }, + '& .Mui-focused': { + border: '0.25px solid #ffffff4a', + borderRadius: '16px', + }, +}; diff --git a/frontend/src/app/(routes)/multisig/Multisig.tsx b/frontend/src/app/(routes)/multisig/Multisig.tsx index 5793e3750..b906ea698 100644 --- a/frontend/src/app/(routes)/multisig/Multisig.tsx +++ b/frontend/src/app/(routes)/multisig/Multisig.tsx @@ -1,34 +1,46 @@ 'use client'; -import TopNav from '@/components/TopNav'; -import Image from 'next/image'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import PageHeader from '@/components/common/PageHeader'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setChangeNetworkDialogOpen } from '@/store/features/common/commonSlice'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import { MULTISIG_DESCRIPTION } from '@/utils/constants'; import React from 'react'; const Multisig = () => { - const message = - 'All Networks page is not supported for Multisig, Please select a network.'; + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const openChangeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: true })); + }; + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + return ( -
    -
    -

    Multisig

    - -
    -
    - {'No -

    {message}

    - +
    + +
    +
    + {isWalletConnected ? ( + + ) : ( + + )} +
    ); diff --git a/frontend/src/app/(routes)/multisig/[network]/ChainMultisig.tsx b/frontend/src/app/(routes)/multisig/[network]/ChainMultisig.tsx index e75191b74..b6c788f80 100644 --- a/frontend/src/app/(routes)/multisig/[network]/ChainMultisig.tsx +++ b/frontend/src/app/(routes)/multisig/[network]/ChainMultisig.tsx @@ -6,7 +6,7 @@ import PageMultisig from '../components/PageMultisig'; const ChainMultisig = ({ network }: { network: string }) => { const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs + (state: RootState) => state.common.nameToChainIDs ); const chainName = network.toLowerCase(); const validChain = chainName in nameToChainIDs; @@ -16,7 +16,7 @@ const ChainMultisig = ({ network }: { network: string }) => { ) : ( <> -
    +
    - The {chainName} is not supported -
    diff --git a/frontend/src/app/(routes)/multisig/[network]/[address]/MultisigAccount.tsx b/frontend/src/app/(routes)/multisig/[network]/[address]/MultisigAccount.tsx deleted file mode 100644 index 559fc3242..000000000 --- a/frontend/src/app/(routes)/multisig/[network]/[address]/MultisigAccount.tsx +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import React from 'react'; -import PageMultisigInfo from '../../components/PageMultisigInfo'; - -const MultisigAccount = ({ - paramChain, - paramAddress, -}: { - paramChain: string; - paramAddress: string; -}) => { - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const chainName = paramChain.toLowerCase(); - const validChain = chainName in nameToChainIDs; - return ( -
    - {validChain ? ( - - ) : ( - <> -
    - - The {chainName} is not supported - -
    - - )} -
    - ); -}; - -export default MultisigAccount; diff --git a/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/PageTxnBuilder.tsx b/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/PageTxnBuilder.tsx new file mode 100644 index 000000000..e840690af --- /dev/null +++ b/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/PageTxnBuilder.tsx @@ -0,0 +1,223 @@ +'use client'; + +import EmptyScreen from '@/components/common/EmptyScreen'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; +import { + createTxn, + getMultisigBalances, + setVerifyDialogOpen, +} from '@/store/features/multisig/multisigSlice'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import React, { useEffect, useState } from 'react'; +import PageHeader from '@/components/common/PageHeader'; +import TxnBuilder from '@/components/txn-builder/TxnBuilder'; +import DialogVerifyAccount from '../../../components/common/DialogVerifyAccount'; +import { fee } from '@/txns/execute'; +import { getAuthToken } from '@/utils/localStorage'; +import { COSMOS_CHAIN_ID } from '@/utils/constants'; +import { setError } from '@/store/features/common/commonSlice'; +import { useRouter } from 'next/navigation'; +import { parseBalance } from '@/utils/denom'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; + +const PageTxnBuilder = ({ + paramChain, + multisigAddress, +}: { + paramChain: string; + multisigAddress: string; +}) => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const chainName = paramChain.toLowerCase(); + const validChain = chainName in nameToChainIDs; + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + const showAuthzAlert = useGetShowAuthzAlert(); + + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + + const handleBackToMultisig = () => { + router.push(`/multisig/${chainName}/${multisigAddress}`); + }; + + return ( +
    +
    + + +
    + {validChain ? ( + <> + {isWalletConnected ? ( + + ) : ( +
    + +
    + )} + + ) : ( + <> +
    + - The {chainName} is not supported - +
    + + )} +
    + ); +}; + +export default PageTxnBuilder; + +const PageTxnBuilderEntry = ({ + chainName, + multisigAddress, + handleBackToMultisig, +}: { + chainName: string; + multisigAddress: string; + handleBackToMultisig: () => void; +}) => { + const dispatch = useAppDispatch(); + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const chainID = nameToChainIDs?.[chainName]; + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { address, baseURL, restURLs } = getChainInfo(chainID); + const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); + const currency = { + coinDenom: displayDenom, + coinDecimals: decimals, + coinMinimalDenom: minimalDenom, + }; + const { isAccountVerified } = useVerifyAccount({ address }); + + const [availableBalance, setAvailableBalance] = useState(0); + + const createRes = useAppSelector((state) => state.multisig.createTxnRes); + const handleVerifyAccount = () => { + dispatch(setVerifyDialogOpen(true)); + }; + + useEffect(() => { + if (!isAccountVerified()) { + handleVerifyAccount(); + } + }, []); + + const onSubmit = (data: TxnBuilderForm) => { + const feeObj = fee( + currency.coinMinimalDenom, + data.fees.toString(), + data.gas + ); + const authToken = getAuthToken(COSMOS_CHAIN_ID); + dispatch( + createTxn({ + data: { + address: multisigAddress, + chain_id: chainID, + messages: data.msgs, + fee: feeObj, + memo: data.memo, + gas: data.gas, + }, + queryParams: { + address, + signature: authToken?.signature || '', + }, + }) + ); + }; + + useEffect(() => { + if (createRes?.status === 'rejected') { + dispatch( + setError({ + type: 'error', + message: createRes?.error, + }) + ); + } else if (createRes?.status === 'idle') { + dispatch( + setError({ + type: 'success', + message: 'Transaction created', + }) + ); + setTimeout(handleBackToMultisig, 1000); + } + }, [createRes]); + + const balance = useAppSelector((state) => state.multisig.balance.balance); + useEffect(() => { + if (balance) { + setAvailableBalance( + parseBalance(balance, currency.coinDecimals, currency.coinMinimalDenom) + ); + } + }, [balance]); + + useEffect(() => { + if (chainID) { + dispatch( + getMultisigBalances({ + baseURL, + address: multisigAddress, + baseURLs: restURLs, + chainID, + }) + ); + } + }, []); + + return ( + <> + {isAccountVerified() ? ( + + ) : ( + + )} + + + ); +}; diff --git a/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/error.tsx b/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/loading.tsx b/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/loading.tsx new file mode 100644 index 000000000..0b08e2215 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/page.tsx b/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/page.tsx new file mode 100644 index 000000000..6006a49c5 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/[network]/[address]/[create-txn]/page.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import PageTxnBuilder from './PageTxnBuilder'; +import '../../../multisig.css'; + +const page = ({ params }: { params: { network: string; address: string } }) => { + return ( + + ); +}; + +export default page; diff --git a/frontend/src/app/(routes)/multisig/[network]/[address]/error.tsx b/frontend/src/app/(routes)/multisig/[network]/[address]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/[network]/[address]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/multisig/[network]/[address]/loading.tsx b/frontend/src/app/(routes)/multisig/[network]/[address]/loading.tsx new file mode 100644 index 000000000..b2b5378dd --- /dev/null +++ b/frontend/src/app/(routes)/multisig/[network]/[address]/loading.tsx @@ -0,0 +1,12 @@ +'use client'; + +import React from 'react'; +import MultisigInfoLoading from '../../components/loaders/MultisigInfoLoading'; + +const loading = () => ( +
    + +
    +); + +export default loading; diff --git a/frontend/src/app/(routes)/multisig/[network]/[address]/page.tsx b/frontend/src/app/(routes)/multisig/[network]/[address]/page.tsx index b48110881..2ac977d97 100644 --- a/frontend/src/app/(routes)/multisig/[network]/[address]/page.tsx +++ b/frontend/src/app/(routes)/multisig/[network]/[address]/page.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import MultisigAccount from './MultisigAccount'; import '../../multisig.css'; +import PageMultisigInfo from '../../components/multisig-account/PageMultisigInfo'; const page = ({ params }: { params: { network: string; address: string } }) => { return (
    -
    diff --git a/frontend/src/app/(routes)/multisig/[network]/error.tsx b/frontend/src/app/(routes)/multisig/[network]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/[network]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/multisig/[network]/loading.tsx b/frontend/src/app/(routes)/multisig/[network]/loading.tsx new file mode 100644 index 000000000..d7537fac1 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/[network]/loading.tsx @@ -0,0 +1,14 @@ +'use client'; + +import React from 'react'; +import MultisigAccountsLoading from '../components/loaders/MultisigAccountsLoading'; +import TransactionsLoading from '../components/loaders/TransactionsLoading'; + +const loading = () => ( + <> + + + +); + +export default loading; diff --git a/frontend/src/app/(routes)/multisig/[network]/page.tsx b/frontend/src/app/(routes)/multisig/[network]/page.tsx index 3d5aa063d..efd3614de 100644 --- a/frontend/src/app/(routes)/multisig/[network]/page.tsx +++ b/frontend/src/app/(routes)/multisig/[network]/page.tsx @@ -1,10 +1,10 @@ +'use client'; import React from 'react'; import ChainMultisig from './ChainMultisig'; import '../multisig.css'; const page = ({ params }: { params: { network: string } }) => { const { network } = params; - return ; }; diff --git a/frontend/src/app/(routes)/multisig/components/AccountInfo.tsx b/frontend/src/app/(routes)/multisig/components/AccountInfo.tsx deleted file mode 100644 index 47b8f3971..000000000 --- a/frontend/src/app/(routes)/multisig/components/AccountInfo.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { deleteMultisig } from '@/store/features/multisig/multisigSlice'; -import { RootState } from '@/store/store'; -import { MultisigAccount } from '@/types/multisig'; -import { parseBalance } from '@/utils/denom'; -import { formatCoin, formatStakedAmount, isMultisigMember } from '@/utils/util'; -import Image from 'next/image'; -import { useRouter } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; -import DialogDeleteMultisig from './DialogDeleteMultisig'; -import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; -import CommonCopy from '@/components/CommonCopy'; -import { getAuthToken } from '@/utils/localStorage'; - -interface AccountInfoProps { - chainID: string; - chainName: string; - address: string; - coinMinimalDenom: string; - coinDecimals: number; - coinDenom: string; - walletAddress: string; -} - -const AccountInfo: React.FC = (props) => { - const { - chainID, - chainName, - address, - coinMinimalDenom, - coinDecimals, - coinDenom, - walletAddress, - } = props; - const router = useRouter(); - const [availableBalance, setAvailableBalance] = useState(0); - - const handleGoBack = () => { - router.push(`/multisig/${chainName}`); - }; - - const multisigAccount = useAppSelector( - (state: RootState) => state.multisig.multisigAccount - ); - const multisigAccounts = useAppSelector( - (state: RootState) => state.multisig.multisigAccounts - ); - const balance = useAppSelector( - (state: RootState) => state.multisig.balance.balance - ); - const totalStaked = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.delegations.totalStaked - ); - const stakedTokens = [ - { - amount: totalStaked?.toString() || '', - denom: coinMinimalDenom, - }, - ]; - const isMember = isMultisigMember( - multisigAccount.pubkeys || [], - walletAddress - ); - - const { txnCounts = {} } = multisigAccounts; - const actionsRequired = txnCounts?.[address] || 0; - - useEffect(() => { - setAvailableBalance( - parseBalance([balance], coinDecimals, coinMinimalDenom) - ); - }, [balance]); - - return ( -
    -

    Multisig

    -
    -
    handleGoBack()}> - Go Back -
    -
    - {multisigAccount.account.name || '-'} -
    -
    - -
    - ); -}; - -export default AccountInfo; - -const AccountDetails = ({ - multisigAccount, - actionsRequired, - balance, - stakedBalance, - chainName, - chainID, - isMember, -}: { - multisigAccount: MultisigAccount; - actionsRequired: number; - balance: string; - stakedBalance: string; - chainName: string; - chainID: string; - isMember: boolean; -}) => { - const { account: accountInfo, pubkeys } = multisigAccount; - const { address, name, created_at, threshold } = accountInfo; - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const dispatch = useAppDispatch(); - const deleteMultisigRes = useAppSelector( - (state: RootState) => state.multisig.deleteMultisigRes - ); - - const router = useRouter(); - - const handleGoBack = () => { - router.push(`/multisig/${chainName}`); - }; - - const handleDeleteDialogClose = () => { - setDeleteDialogOpen(false); - }; - - useEffect(() => { - if (deleteMultisigRes?.status === 'idle') { - handleDeleteDialogClose(); - handleGoBack(); - } - }, [deleteMultisigRes?.status]); - - const authToken = getAuthToken(chainID); - - const handleDelete = () => { - if (isMember) { - dispatch( - deleteMultisig({ - data: { address: multisigAccount?.account?.address }, - queryParams: { - address: authToken?.address || '', - signature: authToken?.signature || '', - }, - }) - ); - } - }; - - return ( -
    -
    -
    -

    {name}

    - {created_at ? ( -

    - Created {getTimeDifferenceToFutureDate(created_at, true)} -  ago -

    - ) : null} -
    -
    -
    -
    - - -
    - } - /> - -
    -
    - - - -
    -
    -
    - {name} -
    Members
    -
    -
    - {pubkeys.map((pubkey, index) => ( - - ))} -
    -
    -
    - -
    -
    - - handleDeleteDialogClose()} - deleteTx={handleDelete} - /> -
    - ); -}; - -const AccountInfoItem = ({ - icon, - name, - value, -}: { - icon: string; - name: string; - value: string | number | React.ReactNode; -}) => { - return ( -
    -
    - {name} -
    {name}
    -
    -
    {value}
    -
    - ); -}; - -const MemberAddress = ({ address }: { address: string }) => { - return ; -}; diff --git a/frontend/src/app/(routes)/multisig/components/AllMultisigs.tsx b/frontend/src/app/(routes)/multisig/components/AllMultisigs.tsx deleted file mode 100644 index 0de3cb3b0..000000000 --- a/frontend/src/app/(routes)/multisig/components/AllMultisigs.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { - getMultisigAccounts, - resetCreateMultisigRes, -} from '@/store/features/multisig/multisigSlice'; -import { RootState } from '@/store/store'; -import { TxStatus } from '@/types/enums'; -import { shortenAddress } from '@/utils/util'; -import { CircularProgress } from '@mui/material'; -import Image from 'next/image'; -import React, { useEffect, useState } from 'react'; -import DialogCreateMultisig from './DialogCreateMultisig'; -import useGetAccountInfo from '@/custom-hooks/useGetAccountInfo'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import Link from 'next/link'; -import { copyToClipboard } from '@/utils/copyToClipboard'; -import { setError } from '@/store/features/common/commonSlice'; - -interface AllMultisigsProps { - address: string; - chainName: string; - chainID: string; -} - -const AllMultisigs: React.FC = (props) => { - const { address, chainName, chainID } = props; - const dispatch = useAppDispatch(); - const [dialogOpen, setDialogOpen] = useState(false); - const multisigAccounts = useAppSelector( - (state: RootState) => state.multisig.multisigAccounts - ); - const createMultiAccRes = useAppSelector( - (state: RootState) => state.multisig.createMultisigAccountRes - ); - const accounts = multisigAccounts.accounts; - const pendingTxns = multisigAccounts.txnCounts; - const status = multisigAccounts.status; - - const [accountInfo] = useGetAccountInfo(chainID); - const { pubkey } = accountInfo; - const { getChainInfo } = useGetChainInfo(); - const { prefix, baseURL } = getChainInfo(chainID); - - useEffect(() => { - if (address) { - dispatch(getMultisigAccounts(address)); - } - }, [address]); - - const handleClose = () => { - setDialogOpen(false); - }; - - useEffect(() => { - if (createMultiAccRes.status === 'idle') { - setDialogOpen(false); - dispatch(getMultisigAccounts(address)); - dispatch(resetCreateMultisigRes()); - } - }, [createMultiAccRes]); - - return ( -
    -
    -
    -

    Multisig

    -
    -

    All Accounts

    - {status !== TxStatus.PENDING && !accounts?.length ? null : ( -
    - -
    - )} -
    -
    - - {status === TxStatus.PENDING ? ( -
    - -
    - ) : ( - <> - {!accounts?.length ? ( -
    - {'No -
    - This looks empty ! go ahead and create MultiSig account Now ! -
    -
    - -
    -
    - ) : ( -
    - {accounts?.map((account) => ( - - ))} -
    - )} - - )} -
    - -
    - ); -}; - -export default AllMultisigs; - -interface MultisigAccountCardProps { - address: string; - threshold: number; - name: string; - actionsRequired: number; - chainName: string; -} - -const MultisigAccountCard = ({ - address, - threshold, - name, - actionsRequired, - chainName, -}: MultisigAccountCardProps) => { - const dispatch = useAppDispatch(); - return ( - -
    -
    {name}
    -
    -
    Address
    -
    - {shortenAddress(address, 27)} - { - copyToClipboard(address); - dispatch( - setError({ - type: 'success', - message: 'Copied', - }) - ); - e.preventDefault(); - e.stopPropagation(); - }} - src="/copy.svg" - width={24} - height={24} - alt="copy" - draggable={false} - /> -
    -
    -
    -
    -
    Actions Required
    -
    - {actionsRequired} -
    -
    -
    -
    Threshold
    -
    - {threshold} -
    -
    -
    -
    - - ); -}; diff --git a/frontend/src/app/(routes)/multisig/components/AllTransactionsList.tsx b/frontend/src/app/(routes)/multisig/components/AllTransactionsList.tsx deleted file mode 100644 index 881674bc3..000000000 --- a/frontend/src/app/(routes)/multisig/components/AllTransactionsList.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { MultisigAddressPubkey, Txn, Txns } from '@/types/multisig'; -import { EMPTY_TXN } from '@/utils/constants'; -import React, { useEffect, useMemo, useState } from 'react'; -import DialogViewRaw from './DialogViewRaw'; -import DialogTxnFailed from './DialogTxnFailed'; -import DialogViewTxnMessages from './DialogViewTxnMessages'; -import TransactionCard from './TransactionCard'; -import { isMultisigMember } from '@/utils/util'; -import Image from 'next/image'; -import { TxStatus } from '@/types/enums'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { CircularProgress } from '@mui/material'; - -interface AllTransactionsListProps { - chainID: string; - txnsState: Txns; - isHistory: boolean; -} - -const AllTransactionsList: React.FC = (props) => { - const { chainID, txnsState, isHistory } = props; - - const [msgDialogOpen, setMsgDialogOpen] = useState(false); - const [viewRawOpen, setViewRawDialogOpen] = useState(false); - const [viewErrorOpen, setViewErrorDialogOpen] = useState(false); - - const createSignRes = useAppSelector( - (state: RootState) => state.multisig.signTxRes - ); - const updateTxnState = useAppSelector( - (state: RootState) => state.multisig.updateTxnRes - ); - const toggleMsgDialogOpen = () => { - setMsgDialogOpen((prevState) => !prevState); - }; - const txnsLoading = useAppSelector( - (state: RootState) => state.multisig?.txns?.status - ); - const toggleViewRawDialogOpen = () => { - setViewRawDialogOpen((prevState) => !prevState); - }; - const handleMsgDialogClose = () => { - setMsgDialogOpen(false); - }; - - const [selectedTxn, setSelectedTxn] = useState(EMPTY_TXN); - const [errMsg, setErrMsg] = useState(''); - const [pubKeys, setPubKeys] = useState([]); - const [multisigAddress, setMultisigAddress] = useState(''); - const [threshold, setThreshold] = useState(0); - - const onViewMoreAction = (txn: Txn) => { - const { pubkeys = [], multisig_address = '', threshold = 0 } = txn; - setSelectedTxn(txn); - setMsgDialogOpen(true); - setPubKeys(pubkeys); - setMultisigAddress(multisig_address); - setThreshold(threshold); - }; - - const onViewRawAction = (txn: Txn) => { - setSelectedTxn(txn); - setViewRawDialogOpen(true); - }; - - const onViewError = (errMsg: string) => { - setErrMsg(errMsg); - setViewErrorDialogOpen(true); - }; - - const { getDenomInfo, getChainInfo } = useGetChainInfo(); - const { explorerTxHashEndpoint, address: walletAddress } = - getChainInfo(chainID); - const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); - const currency = useMemo( - () => ({ - coinMinimalDenom: minimalDenom, - coinDecimals: decimals, - coinDenom: displayDenom, - }), - [minimalDenom, decimals, displayDenom] - ); - - useEffect(() => { - if (createSignRes.status !== TxStatus.PENDING) { - setMsgDialogOpen(false); - } - }, [createSignRes.status]); - - useEffect(() => { - if (updateTxnState.status === TxStatus.IDLE) { - setMsgDialogOpen(false); - } - }, [updateTxnState.status]); - - return ( -
    - {txnsState.list.map((txn) => { - const mAddress = txn.multisig_address; - const pKeys = txn.pubkeys || []; - const threshold_value = txn.threshold || 0; - const isMember = isMultisigMember(pKeys, walletAddress); - - return ( - - ); - })} -
    - {txnsLoading !== TxStatus.PENDING && !txnsState.list.length ? ( -
    - {'No -
    No Transactions
    -
    - ) : null} - {txnsLoading === TxStatus.PENDING ? ( - - ) : null} -
    - - - - setViewErrorDialogOpen(false)} - errMsg={errMsg} - /> -
    - ); -}; - -export default AllTransactionsList; diff --git a/frontend/src/app/(routes)/multisig/components/BroadCastTxn.tsx b/frontend/src/app/(routes)/multisig/components/BroadCastTxn.tsx deleted file mode 100644 index 4ab3fc414..000000000 --- a/frontend/src/app/(routes)/multisig/components/BroadCastTxn.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { setError } from '@/store/features/common/commonSlice'; -import { - resetUpdateTxnState, - updateTxn, -} from '@/store/features/multisig/multisigSlice'; -import { RootState } from '@/store/store'; -import { getWalletAmino } from '@/txns/execute'; -import { MultisigAddressPubkey, Pubkey, Txn } from '@/types/multisig'; -import { getAuthToken } from '@/utils/localStorage'; -import { NewMultisigThresholdPubkey } from '@/utils/util'; -import { SigningStargateClient, makeMultisignedTx } from '@cosmjs/stargate'; -import { fromBase64 } from '@cosmjs/encoding'; -import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; -import React, { useEffect, useState } from 'react'; -import { MultisigTxStatus } from '@/types/enums'; -import { FAILED_TO_BROADCAST_ERROR } from '@/utils/errors'; -import { CircularProgress } from '@mui/material'; - -interface BroadCastTxnProps { - txn: Txn; - multisigAddress: string; - threshold: number; - pubKeys: MultisigAddressPubkey[]; - chainID: string; - isMember: boolean; -} - -const BroadCastTxn: React.FC = (props) => { - const { txn, multisigAddress, pubKeys, threshold, chainID, isMember } = props; - const dispatch = useAppDispatch(); - const [load, setLoad] = useState(false); - const { getChainInfo } = useGetChainInfo(); - const { rpc, address: walletAddress } = getChainInfo(chainID); - - const updateTxnRes = useAppSelector( - (state: RootState) => state.multisig.updateTxnRes - ); - - useEffect(() => { - if (updateTxnRes.status === 'rejected') { - dispatch( - setError({ - type: 'error', - message: updateTxnRes?.error || FAILED_TO_BROADCAST_ERROR, - }) - ); - } - }, [updateTxnRes]); - - useEffect(() => { - return () => { - dispatch(resetUpdateTxnState()); - }; - }, []); - - const broadcastTxn = async () => { - setLoad(true); - const authToken = getAuthToken(chainID); - const queryParams = { - address: walletAddress, - signature: authToken?.signature || '', - }; - try { - const client = await SigningStargateClient.connect(rpc); - - const multisigAcc = await client.getAccount(multisigAddress); - if (!multisigAcc) { - dispatch( - setError({ - type: 'error', - message: 'multisig account does not exist on chain', - }) - ); - setLoad(false); - return; - } - - const mapData = pubKeys || []; - let pubkeys_list: Pubkey[] = []; - - pubkeys_list = mapData.map((p) => { - const parsed = p?.pubkey; - const obj = { - type: parsed?.type, - value: parsed?.value, - }; - return obj; - }); - - const multisigThresholdPK = NewMultisigThresholdPubkey( - pubkeys_list, - `${threshold}` - ); - - const txBody = { - typeUrl: '/cosmos.tx.v1beta1.TxBody', - value: { - messages: txn.messages, - memo: txn.memo, - }, - }; - - const walletAmino = await getWalletAmino(chainID); - const offlineClient = await SigningStargateClient.offline(walletAmino[0]); - const txBodyBytes = offlineClient.registry.encode(txBody); - - const signedTx = makeMultisignedTx( - multisigThresholdPK, - multisigAcc.sequence, - txn?.fee, - txBodyBytes, - new Map( - txn?.signatures.map((s) => [s.address, fromBase64(s.signature)]) - ) - ); - - const result = await client.broadcastTx( - Uint8Array.from(TxRaw.encode(signedTx).finish()) - ); - - setLoad(false); - if (result.code === 0) { - dispatch( - updateTxn({ - queryParams: queryParams, - data: { - txId: txn?.id, - address: multisigAddress, - body: { - status: MultisigTxStatus.SUCCESS, - hash: result?.transactionHash || '', - error_message: '', - }, - }, - }) - ); - } else { - dispatch( - setError({ - type: 'error', - message: result?.rawLog || FAILED_TO_BROADCAST_ERROR, - }) - ); - dispatch( - updateTxn({ - queryParams: queryParams, - data: { - txId: txn.id, - address: multisigAddress, - body: { - status: MultisigTxStatus.FAILED, - hash: result?.transactionHash || '', - error_message: result?.rawLog || FAILED_TO_BROADCAST_ERROR, - }, - }, - }) - ); - } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - } catch (error: any) { - setLoad(false); - dispatch( - setError({ - type: 'error', - message: error?.message || FAILED_TO_BROADCAST_ERROR, - }) - ); - - dispatch( - updateTxn({ - queryParams: queryParams, - data: { - txId: txn?.id, - address: multisigAddress, - body: { - status: MultisigTxStatus.FAILED, - hash: '', - error_message: error?.message || FAILED_TO_BROADCAST_ERROR, - }, - }, - }) - ); - } - }; - return ( - - ); -}; - -export default BroadCastTxn; diff --git a/frontend/src/app/(routes)/multisig/components/DeleteTxn.tsx b/frontend/src/app/(routes)/multisig/components/DeleteTxn.tsx deleted file mode 100644 index 1476e5716..000000000 --- a/frontend/src/app/(routes)/multisig/components/DeleteTxn.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useAppDispatch } from '@/custom-hooks/StateHooks'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { deleteTxn } from '@/store/features/multisig/multisigSlice'; -import { getAuthToken } from '@/utils/localStorage'; -import Image from 'next/image'; -import React, { useState } from 'react'; -import DialogDeleteTxn from './DialogDeleteTxn'; - -interface DeleteTxnProps { - txId: number; - address: string; - chainID: string; - isMember: boolean; -} - -const DeleteTxn: React.FC = (props) => { - const { txId, address, chainID, isMember } = props; - const dispatch = useAppDispatch(); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const { getChainInfo } = useGetChainInfo(); - const { address: walletAddress } = getChainInfo(chainID); - const authToken = getAuthToken(chainID); - - const deleteTx = () => { - dispatch( - deleteTxn({ - queryParams: { - address: walletAddress, - signature: authToken?.signature || '', - }, - data: { - address: address, - id: txId, - }, - }) - ); - }; - - const handleDeleteDialogClose = () => { - setDeleteDialogOpen(false); - }; - - return ( - <> - - handleDeleteDialogClose()} - deleteTx={deleteTx} - /> - - ); -}; - -export default DeleteTxn; diff --git a/frontend/src/app/(routes)/multisig/components/DialogCreateMultisig.tsx b/frontend/src/app/(routes)/multisig/components/DialogCreateMultisig.tsx deleted file mode 100644 index 328856eb8..000000000 --- a/frontend/src/app/(routes)/multisig/components/DialogCreateMultisig.tsx +++ /dev/null @@ -1,525 +0,0 @@ -import { - CircularProgress, - Dialog, - DialogContent, - InputAdornment, - TextField, -} from '@mui/material'; -import Image from 'next/image'; -import React, { ChangeEvent, useEffect, useState } from 'react'; -import { setError } from '@/store/features/common/commonSlice'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import Axios from 'axios'; -import { cleanURL } from '@/utils/util'; -import { - generateMultisigAccount, - isValidPubKey, -} from '@/txns/multisig/multisig'; -import { getAuthToken } from '@/utils/localStorage'; -import { createAccount } from '@/store/features/multisig/multisigSlice'; -import { RootState } from '@/store/store'; -import { - createMultisigTextFieldStyles, - createMultisigThresholdStyles, -} from '../styles'; -import { - ADDRESS_NOT_FOUND, - DUPLICATE_PUBKEYS_ERROR, - FAILED_TO_GENERATE_MULTISIG, - INVALID_PUBKEY, - MAX_PUBKEYS_ERROR, - MAX_THRESHOLD_ERROR, - MIN_PUBKEYS_ERROR, - MIN_THRESHOLD_ERROR, -} from '@/utils/errors'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -interface DialogCreateMultisigProps { - open: boolean; - onClose: () => void; - addressPrefix: string; - chainID: string; - address: string; - pubKey: string; - baseURL: string; -} - -interface InputTextComponentProps { - index: number; - field: PubKeyFields; - handleRemoveValue: (index: number) => void; - handleChangeValue: ( - index: number, - e: ChangeEvent - ) => void; -} - -interface PubKeyFields { - name: string; - value: string; - label: string; - placeHolder: string; - required: boolean; - disabled: boolean; - pubKey: string; - address: string; - isPubKey: boolean; - error: string; -} - -const MAX_PUB_KEYS = 7; - -const InputTextComponent: React.FC = (props) => { - const { field, index, handleChangeValue, handleRemoveValue } = props; - return ( - <> - handleChangeValue(index, e)} - name={field.isPubKey ? 'pubKey' : 'address'} - value={field.isPubKey ? field.pubKey : field.address} - required={field?.required} - placeholder={field.isPubKey ? 'Public Key (Secp256k1)' : 'Address'} - sx={createMultisigTextFieldStyles} - fullWidth - disabled={field.disabled} - InputProps={{ - endAdornment: - index !== 0 ? ( - - !field.disabled - ? handleRemoveValue(index) - : alert('Cannot self remove') - } - position="end" - sx={{ - '&:hover': { - cursor: 'pointer', - }, - }} - > - Delete - - ) : ( - -
    (You)
    -
    - ), - sx: { - input: { - color: 'white', - fontSize: '14px', - padding: 2, - }, - }, - }} - /> -
    - {field.error.length ? field.error : ''} -
    - - ); -}; - -const getPubkey = async (address: string, baseURL: string) => { - try { - const { status, data } = await Axios.get( - `${cleanURL(baseURL)}/cosmos/auth/v1beta1/accounts/${address}` - ); - - if (status === 200) { - return data.account.pub_key.key || ''; - } else { - return ''; - } - } catch (error) { - console.log(error); - return ''; - } -}; - -const DialogCreateMultisig: React.FC = (props) => { - const { open, onClose, address, addressPrefix, chainID, pubKey, baseURL } = - props; - const dispatch = useAppDispatch(); - const [name, setName] = useState(''); - const [pubKeyFields, setPubKeyFields] = useState([]); - const [threshold, setThreshold] = useState(0); - const [formError, setFormError] = useState(''); - - const createMultiAccRes = useAppSelector( - (state: RootState) => state.multisig.createMultisigAccountRes - ); - - const pubKeyObj = { - name: 'pubKey', - value: '', - label: 'Public Key (Secp256k1)', - placeHolder: 'E. g. AtgCrYjD+21d1+og3inzVEOGbCf5uhXnVeltFIo7RcRp', - required: true, - disabled: false, - isPubKey: false, - address: '', - pubKey: '', - error: '', - }; - - useEffect(() => { - setPubKeyFields([ - { - name: 'current', - value: pubKey, - label: 'Public Key (Secp256k1)', - placeHolder: 'E. g. AtgCrYjD+21d1+og3inzVEOGbCf5uhXnVeltFIo7RcRp', - required: true, - disabled: true, - pubKey: pubKey, - address: '', - isPubKey: true, - error: '', - }, - { ...pubKeyObj }, - ]); - }, [pubKey]); - - const handleClose = () => { - onClose(); - }; - - const handleNameChange = (e: ChangeEvent) => { - setName(e.target.value); - }; - - const togglePubKey = (index: number) => { - const pubKeysList = [...pubKeyFields]; - pubKeysList[index].isPubKey = !pubKeysList[index].isPubKey; - pubKeysList[index].error = ''; - setPubKeyFields(pubKeysList); - }; - - const handleRemoveValue = (i: number) => { - if (pubKeyFields.length > 1) { - pubKeyFields.splice(i, 1); - setPubKeyFields([...pubKeyFields]); - } - }; - - const handleChangeValue = ( - index: number, - e: ChangeEvent - ) => { - const newInputFields = pubKeyFields.map((value, key) => { - if (e.target.name === 'address') { - if (index === key) { - value['address'] = e.target.value; - value['value'] = e.target.value; - } - return value; - } else { - if (index === key) { - value['pubKey'] = e.target.value; - value['value'] = e.target.value; - } - return value; - } - }); - - setPubKeyFields(newInputFields); - }; - - const handleAddPubKey = () => { - if (pubKeyFields?.length >= MAX_PUB_KEYS) { - dispatch( - setError({ - type: 'error', - message: MAX_PUBKEYS_ERROR, - }) - ); - return; - } else { - setPubKeyFields([...pubKeyFields, pubKeyObj]); - } - }; - - const handleChange = (e: ChangeEvent) => { - if (parseInt(e.target.value) > pubKeyFields?.length) { - alert(MAX_THRESHOLD_ERROR); - return; - } - setThreshold(parseInt(e.target.value)); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setFormError(''); - - if (Number(threshold) < 1) { - dispatch(setError({ type: 'error', message: MIN_THRESHOLD_ERROR })); - return; - } - - if (!pubKeyFields?.length) { - dispatch(setError({ type: 'error', message: MIN_PUBKEYS_ERROR })); - return; - } - - let isValid = true; - const pubKeyValidationPromises = pubKeyFields.map(async (field, index) => { - if (!field.isPubKey) { - const pubKey = await getPubkey(field.address, baseURL); - if (pubKey.length) { - return { index, pubKey, error: '' }; - } else { - isValid = false; - return { index, pubKey: '', error: ADDRESS_NOT_FOUND }; - } - } else if (field.pubKey.length) { - if (!isValidPubKey(field.pubKey)) { - isValid = false; - return { index, pubKey: field.pubKey, error: INVALID_PUBKEY }; - } else { - return { index, pubKey: field.pubKey, error: '' }; - } - } - return { index, pubKey: '', error: '' }; - }); - - const results = await Promise.all(pubKeyValidationPromises); - results.forEach((result) => { - const pubKeysList = [...pubKeyFields]; - pubKeysList[result.index].pubKey = result.pubKey; - pubKeysList[result.index].error = result.error; - setPubKeyFields(pubKeysList); - }); - - if (!isValid) { - return; - } - - const pubKeys = pubKeyFields.map((v) => v.pubKey); - - const uniquePubKeys = Array.from(new Set(pubKeys)); - if (uniquePubKeys?.length !== pubKeys?.length) { - dispatch( - setError({ - type: 'error', - message: DUPLICATE_PUBKEYS_ERROR, - }) - ); - return; - } - - for (let i = 0; i < uniquePubKeys.length; i++) { - if (!isValidPubKey(uniquePubKeys[i])) { - setFormError(`pubKey at ${i + 1} is invalid`); - return; - } - } - - try { - const res = generateMultisigAccount( - pubKeys, - Number(threshold), - addressPrefix - ); - const authToken = getAuthToken(chainID); - const queryParams = { - address: address, - signature: authToken?.signature || '', - }; - dispatch( - createAccount({ - queryParams: queryParams, - data: { - address: res.address, - chainId: chainID, - pubkeys: res.pubkeys, - createdBy: address, - name: name, - threshold: res.threshold, - }, - }) - ); - /* eslint-disable @typescript-eslint/no-explicit-any */ - } catch (error: any) { - dispatch( - setError({ - type: 'error', - message: error || FAILED_TO_GENERATE_MULTISIG, - }) - ); - } - }; - - useEffect(() => { - if (createMultiAccRes?.status === 'idle') { - dispatch(setError({ type: 'success', message: 'Successfully created' })); - } else if (createMultiAccRes?.status === 'rejected') { - dispatch(setError({ type: 'error', message: createMultiAccRes?.error })); - } - }, [createMultiAccRes]); - - return ( - - -
    -
    -
    { - handleClose(); - }} - > - Close -
    -
    -
    -
    -

    - Create Multisig -

    -
    handleSubmit(e)}> - - {pubKeyFields.map((field, index) => ( - <> - -
    - {index !== 0 ? ( - - ) : null} -
    - - ))} -
    - -
    -
    - -
    - of -
    - -
    - Threshold -
    -
    -
    {formError}
    - - -
    -
    -
    -
    -
    - ); -}; - -export default DialogCreateMultisig; diff --git a/frontend/src/app/(routes)/multisig/components/DialogCreateTxn.tsx b/frontend/src/app/(routes)/multisig/components/DialogCreateTxn.tsx deleted file mode 100644 index fdbd2b1ea..000000000 --- a/frontend/src/app/(routes)/multisig/components/DialogCreateTxn.tsx +++ /dev/null @@ -1,662 +0,0 @@ -import { - CLOSE_ICON_PATH, - DELEGATE_TYPE_URL, - MULTISIG_DELEGATE_TEMPLATE, - MULTISIG_REDELEGATE_TEMPLATE, - MULTISIG_SEND_TEMPLATE, - MULTISIG_TX_TYPES, - MULTISIG_UNDELEGATE_TEMPLATE, - NO_MESSAGES_ILLUSTRATION, - REDELEGATE_TYPE_URL, - SEND_TYPE_URL, - UNDELEGATE_TYPE_URL, -} from '@/utils/constants'; -import { - CircularProgress, - Dialog, - DialogContent, - FormControl, - IconButton, - InputLabel, - MenuItem, - Pagination, - Select, - SelectChangeEvent, - TextField, - Tooltip, -} from '@mui/material'; -import Image from 'next/image'; -import React, { useEffect, useState } from 'react'; -import Send from './txns/Send'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { parseBalance } from '@/utils/denom'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { paginationComponentStyles } from '../../staking/styles'; -import { Controller, useForm } from 'react-hook-form'; -import { getAuthToken } from '@/utils/localStorage'; -import { fee } from '@/txns/execute'; -import { - createTxn, - resetCreateTxnState, -} from '@/store/features/multisig/multisigSlice'; -import Delegate from './txns/Delegate'; -import { resetError, setError } from '@/store/features/common/commonSlice'; -import { createTxnTextFieldStyles, selectTxnStyles } from '../styles'; -import SendMessage from './msgs/SendMessage'; -import DelegateMessage from './msgs/DelegateMessage'; -import UndelegateMessage from './msgs/UndelegateMessage'; -import RedelegateMessage from './msgs/RedelegateMessage'; -import SelectTransactionType from './SelectTransactionType'; -import UnDelegate from './txns/UnDelegate'; -import ReDelegate from './txns/ReDelegate'; -import { - parseDelegateMsgsFromContent, - parseReDelegateMsgsFromContent, - parseSendMsgsFromContent, - parseUnDelegateMsgsFromContent, -} from '@/utils/parseMsgs'; -import ClearIcon from '@mui/icons-material/Clear'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -interface DialogCreateTxnProps { - open: boolean; - onClose: () => void; - chainID: string; - address: string; - walletAddress: string; -} - -const PER_PAGE = 6; - -interface FileUploadProps { - onFileContents: (content: string, type: string) => void; - txType: string; - resetMessages: () => void; -} - -const FileUpload = (props: FileUploadProps) => { - const { onFileContents, resetMessages, txType } = props; - const [uploadedFileName, setUploadedFileName] = useState(''); - - return ( -
    -
    { - document.getElementById('multisig_file')!.click(); - }} - > -
    - {uploadedFileName ? ( - <> -
    - {uploadedFileName}{' '} - - { - setUploadedFileName(''); - resetMessages(); - e.stopPropagation(); - }} - > - - - -
    - - ) : ( - <> - Upload file -
    Upload file here
    - - )} -
    - { - const files = e.target.files; - if (!files) { - return; - } - const file = files[0]; - if (!file) { - return; - } - - const reader = new FileReader(); - reader.onload = (e) => { - const contents = (e?.target?.result as string) || ''; - setUploadedFileName(file?.name); - onFileContents(contents, txType); - }; - reader.onerror = (e) => { - alert(e); - }; - reader.readAsText(file); - e.target.value = ''; - }} - /> -
    - -
    - ); -}; - -const DialogCreateTxn: React.FC = (props) => { - const { open, onClose, chainID, address, walletAddress } = props; - const [isFileUpload, setIsFileUpload] = useState(false); - const [txType, setTxType] = useState('Send'); - const [messages, setMessages] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - const [slicedMsgs, setSlicedMsgs] = useState([]); - const [availableBalance, setAvailableBalance] = useState(0); - - const dispatch = useAppDispatch(); - - const balance = useAppSelector( - (state: RootState) => state.multisig.balance.balance - ); - const createRes = useAppSelector( - (state: RootState) => state.multisig.createTxnRes - ); - - const handleClose = () => { - onClose(); - setMessages([]); - resetForm(); - }; - - const onSelect = (value: boolean) => { - setIsFileUpload(value); - }; - - const handleTypeChange = (event: SelectChangeEvent) => { - setTxType(event.target.value); - }; - - const { getDenomInfo, getChainInfo } = useGetChainInfo(); - const { feeAmount, baseURL } = getChainInfo(chainID); - const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); - const currency = { - coinDenom: displayDenom, - coinDecimals: decimals, - coinMinimalDenom: minimalDenom, - }; - - useEffect(() => { - if (balance) { - setAvailableBalance( - parseBalance( - [balance], - currency.coinDecimals, - currency.coinMinimalDenom - ) - ); - } - }, [balance]); - - useEffect(() => { - if (messages.length < PER_PAGE) { - setSlicedMsgs(messages); - } else { - setCurrentPage(1); - setSlicedMsgs(messages?.slice(0, 1 * PER_PAGE)); - } - }, [messages]); - - useEffect(() => { - if (createRes?.status === 'rejected') { - dispatch( - setError({ - type: 'error', - message: createRes?.error, - }) - ); - } else if (createRes?.status === 'idle') { - handleClose(); - dispatch( - setError({ - type: 'success', - message: 'Transaction created', - }) - ); - } - }, [createRes]); - - const resetMessages = () => { - setMessages([]); - }; - - useEffect(() => { - resetMessages(); - }, [isFileUpload]); - - useEffect(() => { - return () => { - dispatch(resetError()); - dispatch(resetCreateTxnState()); - }; - }, []); - - const renderMessage = ( - msg: Msg, - index: number, - currency: Currency, - onDelete: (index: number) => void - ) => { - switch (msg.typeUrl) { - case SEND_TYPE_URL: - return SendMessage({ msg, index, currency, onDelete }); - case DELEGATE_TYPE_URL: - return DelegateMessage({ msg, index, currency, onDelete }); - case UNDELEGATE_TYPE_URL: - return UndelegateMessage({ msg, index, currency, onDelete }); - case REDELEGATE_TYPE_URL: - return RedelegateMessage({ msg, index, currency, onDelete }); - default: - return ''; - } - }; - - const onDeleteMsg = (index: number) => { - const arr = messages.filter((_, i) => i !== index); - setMessages(arr); - }; - - const { - handleSubmit, - control, - reset: resetForm, - } = useForm({ - defaultValues: { - msgs: [], - gas: 900000, - memo: '', - fees: feeAmount * 10 ** currency.coinDecimals, - }, - }); - - const onFileContents = (content: string, type: string) => { - switch (type) { - case MULTISIG_TX_TYPES.send: { - const [parsedTxns, error] = parseSendMsgsFromContent(address, content); - if (error) { - dispatch( - setError({ - type: 'error', - message: error, - }) - ); - } else { - setMessages(parsedTxns); - } - break; - } - case MULTISIG_TX_TYPES.delegate: { - const [parsedTxns, error] = parseDelegateMsgsFromContent( - address, - content - ); - if (error) { - dispatch( - setError({ - type: 'error', - message: error, - }) - ); - } else { - setMessages(parsedTxns); - } - break; - } - case MULTISIG_TX_TYPES.redelegate: { - const [parsedTxns, error] = parseReDelegateMsgsFromContent( - address, - content - ); - if (error) { - dispatch( - setError({ - type: 'error', - message: error, - }) - ); - } else { - setMessages(parsedTxns); - } - break; - } - case MULTISIG_TX_TYPES.undelegate: { - const [parsedTxns, error] = parseUnDelegateMsgsFromContent( - address, - content - ); - if (error) { - dispatch( - setError({ - type: 'error', - message: error, - }) - ); - } else { - setMessages(parsedTxns); - } - break; - } - default: - setMessages([]); - } - }; - - const onSubmit = (data: { - msgs: never[]; - gas: number; - memo: string; - fees: number; - }) => { - const feeObj = fee( - currency.coinMinimalDenom, - data.fees.toString(), - data.gas - ); - const authToken = getAuthToken(chainID); - dispatch( - createTxn({ - data: { - address: address, - chain_id: chainID, - messages: messages, - fee: feeObj, - memo: data.memo, - gas: data.gas, - }, - queryParams: { - address: walletAddress, - signature: authToken?.signature || '', - }, - }) - ); - }; - - return ( - handleClose()} - maxWidth="lg" - PaperProps={{ - sx: dialogBoxPaperPropStyles, - }} - > - -
    -
    -
    { - handleClose(); - }} - > - Close -
    -
    -
    -
    -
    Create Transaction
    - - - - Select Transaction - - - - {isFileUpload ? ( - - onFileContents(content, type) - } - txType={txType} - resetMessages={resetMessages} - /> - ) : ( - <> - {txType === MULTISIG_TX_TYPES.send && ( - { - setMessages([...messages, payload]); - }} - currency={currency} - availableBalance={availableBalance} - /> - )} - {txType === MULTISIG_TX_TYPES.delegate && ( - { - setMessages([...messages, payload]); - }} - currency={currency} - availableBalance={availableBalance} - /> - )} - {txType === MULTISIG_TX_TYPES.undelegate && ( - { - setMessages([...messages, payload]); - }} - currency={currency} - availableBalance={availableBalance} - /> - )} - {txType === MULTISIG_TX_TYPES.redelegate && ( - { - setMessages([...messages, payload]); - }} - currency={currency} - availableBalance={availableBalance} - /> - )} - - )} -
    -
    -
    -
    Messages
    - {messages.length ? ( -
    -
    -
    - {slicedMsgs.map((msg, index) => { - return ( -
    - {renderMessage( - msg, - index + PER_PAGE * (currentPage - 1), - currency, - onDeleteMsg - )} -
    - ); - })} -
    - -
    PER_PAGE - ? 'mt-2 flex justify-end opacity-100' - : 'mt-2 flex justify-end opacity-0' - } - > - { - setCurrentPage(v); - setSlicedMsgs( - messages?.slice((v - 1) * PER_PAGE, v * PER_PAGE) - ); - }} - /> -
    -
    -
    -
    - ( - - )} - /> -
    - - -
    -
    - ) : ( -
    - No Messages -
    - )} -
    -
    -
    -
    -
    - ); -}; - -export default DialogCreateTxn; diff --git a/frontend/src/app/(routes)/multisig/components/DialogDeleteMultisig.tsx b/frontend/src/app/(routes)/multisig/components/DialogDeleteMultisig.tsx deleted file mode 100644 index 1b9a3f71e..000000000 --- a/frontend/src/app/(routes)/multisig/components/DialogDeleteMultisig.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { resetDeleteTxnState } from '@/store/features/multisig/multisigSlice'; -import { RootState } from '@/store/store'; -import { TxStatus } from '@/types/enums'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; -import { - CLOSE_ICON_PATH, - DELETE_TXN_DIALOG_IMAGE_PATH, -} from '@/utils/constants'; -import { Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React, { useEffect } from 'react'; - -interface DialogDeleteMultisigProps { - open: boolean; - onClose: () => void; - deleteTx: () => void; -} - -const DialogDeleteMultisig: React.FC = (props) => { - const { open, onClose, deleteTx } = props; - const dispatch = useAppDispatch(); - const deleteTxnStatus = useAppSelector( - (state: RootState) => state.multisig.deleteTxnRes.status - ); - - useEffect(() => { - if (deleteTxnStatus === TxStatus.IDLE) onClose(); - }, [deleteTxnStatus]); - - useEffect(() => { - dispatch(resetDeleteTxnState()); - }, []); - return ( - - -
    -
    -
    - Close -
    -
    -
    - Delete Txn -
    -
    -

    - Delete Multisig -

    -
    - Are you sure you want to delete this multisig ? This action - cannot be undone -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - ); -}; -export default DialogDeleteMultisig; diff --git a/frontend/src/app/(routes)/multisig/components/DialogDeleteTxn.tsx b/frontend/src/app/(routes)/multisig/components/DialogDeleteTxn.tsx deleted file mode 100644 index 86b664920..000000000 --- a/frontend/src/app/(routes)/multisig/components/DialogDeleteTxn.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { resetDeleteTxnState } from '@/store/features/multisig/multisigSlice'; -import { RootState } from '@/store/store'; -import { TxStatus } from '@/types/enums'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; -import { - CLOSE_ICON_PATH, - DELETE_TXN_DIALOG_IMAGE_PATH, -} from '@/utils/constants'; -import { CircularProgress, Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React, { useEffect } from 'react'; - -interface DialogDeleteTxnProps { - open: boolean; - onClose: () => void; - deleteTx: () => void; -} - -const DialogDeleteTxn: React.FC = (props) => { - const { open, onClose, deleteTx } = props; - const dispatch = useAppDispatch(); - const deleteTxnStatus = useAppSelector( - (state: RootState) => state.multisig.deleteTxnRes.status - ); - - useEffect(() => { - if (deleteTxnStatus === TxStatus.IDLE) onClose(); - }, [deleteTxnStatus]); - - useEffect(() => { - dispatch(resetDeleteTxnState()); - }, []); - return ( - - -
    -
    -
    - Close -
    -
    -
    - Delete Txn -
    -
    -

    - Delete Transaction -

    -
    - Are you sure you want to delete this transaction ? This action - cannot be undone -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - ); -}; -export default DialogDeleteTxn; diff --git a/frontend/src/app/(routes)/multisig/components/DialogRepeatTxn.tsx b/frontend/src/app/(routes)/multisig/components/DialogRepeatTxn.tsx new file mode 100644 index 000000000..a9806334c --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/DialogRepeatTxn.tsx @@ -0,0 +1,90 @@ +import CustomButton from '@/components/common/CustomButton'; +import { REPEAT_ICON } from '@/constants/image-names'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import { resetCreateTxnState } from '@/store/features/multisig/multisigSlice'; +import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React, { useEffect } from 'react'; + +const DialogRepeatTxn = ({ + confirmRepeat, + onClose, + open, +}: { + open: boolean; + onClose: () => void; + confirmRepeat: () => void; +}) => { + const dispatch = useAppDispatch(); + const createRes = useAppSelector((state) => state.multisig.createTxnRes); + + useEffect(() => { + if (createRes?.status === 'rejected') { + dispatch( + setError({ + type: 'error', + message: createRes?.error, + }) + ); + dispatch(resetCreateTxnState()); + onClose(); + } else if (createRes?.status === 'idle') { + dispatch( + setError({ + type: 'success', + message: 'Transaction created', + }) + ); + dispatch(resetCreateTxnState()); + onClose(); + } + }, [createRes]); + + return ( + + +
    +
    + +
    +
    + +
    +
    + Repeat Transaction +
    +
    + Are you sure you want to repeat this transaction ? +
    +
    +
    + +
    +
    +
    + ); +}; + +export default DialogRepeatTxn; diff --git a/frontend/src/app/(routes)/multisig/components/DialogTxnFailed.tsx b/frontend/src/app/(routes)/multisig/components/DialogTxnFailed.tsx deleted file mode 100644 index 4768acaa1..000000000 --- a/frontend/src/app/(routes)/multisig/components/DialogTxnFailed.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { - CLOSE_ICON_PATH, - DELETE_TXN_DIALOG_IMAGE_PATH, -} from '@/utils/constants'; -import { Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React from 'react'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -interface DialogTxnFailedProps { - open: boolean; - onClose: () => void; - errMsg: string; -} - -const DialogTxnFailed: React.FC = (props) => { - const { open, onClose, errMsg } = props; - return ( - - -
    -
    -
    - Close -
    -
    -
    - Delete Txn -
    -
    -

    - Status : Failed -

    -
    - {errMsg} -
    -
    - -
    -
    -
    -
    -
    -
    -
    - ); -}; - -export default DialogTxnFailed; diff --git a/frontend/src/app/(routes)/multisig/components/DialogViewRaw.tsx b/frontend/src/app/(routes)/multisig/components/DialogViewRaw.tsx deleted file mode 100644 index 9ee98ed9f..000000000 --- a/frontend/src/app/(routes)/multisig/components/DialogViewRaw.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Txn } from '@/types/multisig'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; -import { CLOSE_ICON_PATH } from '@/utils/constants'; -import { Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React from 'react'; - -interface DialogViewRawProps { - open: boolean; - onClose: () => void; - txn: Txn; -} - -const DialogViewRaw: React.FC = (props) => { - const { open, onClose, txn } = props; - const handleClose = () => { - onClose(); - }; - return ( - handleClose()} - maxWidth="lg" - PaperProps={{ - sx: dialogBoxPaperPropStyles, - }} - > - -
    -
    -
    { - handleClose(); - }} - > - Close -
    -
    -
    -
    Raw
    -
    - {txn ? ( -
    {JSON.stringify(txn, undefined, 2)}
    - ) : ( -
    - No Data -
    - )} -
    -
    -
    -
    -
    - ); -}; - -export default DialogViewRaw; diff --git a/frontend/src/app/(routes)/multisig/components/DialogViewTxnMessages.tsx b/frontend/src/app/(routes)/multisig/components/DialogViewTxnMessages.tsx deleted file mode 100644 index d39f7c25f..000000000 --- a/frontend/src/app/(routes)/multisig/components/DialogViewTxnMessages.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { TxStatus } from '@/types/enums'; -import { MultisigAddressPubkey, Txn } from '@/types/multisig'; -import { CLOSE_ICON_PATH } from '@/utils/constants'; -import { Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React, { useEffect } from 'react'; -import TransactionItem from './TransactionItem'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -interface DialogViewTxnMessagesProps { - open: boolean; - txn: Txn; - multisigAddress: string; - threshold: number; - pubKeys: MultisigAddressPubkey[]; - membersCount: number; - chainID: string; - toggleMsgDialogOpen: () => void; - onViewRawAction: (txn: Txn) => void; - isHistory: boolean; - currency: Currency; - explorerTxHashEndpoint: string; - onViewError: (errMsg: string) => void; - handleMsgDialogClose: () => void; -} - -const DialogViewTxnMessages: React.FC = (props) => { - const { - open, - txn, - multisigAddress, - threshold, - membersCount, - chainID, - pubKeys, - toggleMsgDialogOpen, - isHistory, - currency, - onViewRawAction, - explorerTxHashEndpoint, - onViewError, - handleMsgDialogClose, - } = props; - const deleteTxnRes = useAppSelector( - (state: RootState) => state.multisig.deleteTxnRes - ); - useEffect(() => { - if (deleteTxnRes.status === TxStatus.IDLE) { - handleMsgDialogClose(); - } - }, [deleteTxnRes]); - return ( - toggleMsgDialogOpen()} - maxWidth="lg" - PaperProps={{ - sx: dialogBoxPaperPropStyles, - }} - > - -
    -
    -
    { - toggleMsgDialogOpen(); - }} - > - Close -
    -
    -
    -
    -
    -
    Messages
    -
    Signed
    - {isHistory ?
    Status
    : null} -
    - Actions -
    -
    -
    -
    - -
    -
    -
    -
    - ); -}; - -export default DialogViewTxnMessages; diff --git a/frontend/src/app/(routes)/multisig/components/InternalLoading.tsx b/frontend/src/app/(routes)/multisig/components/InternalLoading.tsx new file mode 100644 index 000000000..962ba24aa --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/InternalLoading.tsx @@ -0,0 +1,57 @@ +import SectionHeader from '@/components/common/SectionHeader'; +import React from 'react'; + +const InternalLoading = () => { + return ( +
    +
    + Back to List +
    +
    +
    +

    +

    +

    +
    +
    +
    + +
    +
    + +
    + {[1, 2, 3, 4].map((_, index) => ( +
    + ))} +
    +
    + +
    +
    +
    + +
    + +
    +
    +

    +

    +

    +

    +
    +
    +

    +

    +
    +
    +
    + ); +}; + +export default InternalLoading; diff --git a/frontend/src/app/(routes)/multisig/components/MultisigLoading.tsx b/frontend/src/app/(routes)/multisig/components/MultisigLoading.tsx new file mode 100644 index 000000000..ebc898750 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/MultisigLoading.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import PageHeader from '@/components/common/PageHeader'; +import SectionHeader from '@/components/common/SectionHeader'; +import { MULTISIG_DESCRIPTION } from '@/utils/constants'; + +const MultisigLoading = () => { + return ( +
    +
    +
    + +
    + +
    +
    +
    + {[1, 2, 3].map((_, index) => ( +
    + ))} +
    +
    + +
    +
    +
    +
    +

    +

    +
    +

    +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default MultisigLoading; diff --git a/frontend/src/app/(routes)/multisig/components/MultisigSidebar.tsx b/frontend/src/app/(routes)/multisig/components/MultisigSidebar.tsx deleted file mode 100644 index 538bfbed0..000000000 --- a/frontend/src/app/(routes)/multisig/components/MultisigSidebar.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import TopNav from '@/components/TopNav'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { - getAccountAllMultisigTxns, - getTxns, - resetCreateTxnState, -} from '@/store/features/multisig/multisigSlice'; -import React, { useEffect, useState } from 'react'; -import TransactionsList from './TransactionsList'; -import { RootState } from '@/store/store'; -import DialogCreateTxn from './DialogCreateTxn'; -import { resetError, setError } from '@/store/features/common/commonSlice'; -import { TxStatus } from '@/types/enums'; -import AllTransactionsList from './AllTransactionsList'; - -interface MultisigSidebarProps { - chainID: string; - accountSpecific: boolean; - address?: string; - walletAddress: string; - verified: boolean; -} - -const MultisigSidebar: React.FC = (props) => { - const { chainID, accountSpecific, address, walletAddress, verified } = props; - const dispatch = useAppDispatch(); - const [isHistory, setIsHistory] = useState(false); - const [isMember, setIsMember] = useState(false); - const [createDialogOpen, setCreateDialogOpen] = useState(false); - const multisigAccount = useAppSelector( - (state: RootState) => state.multisig.multisigAccount - ); - const txnsState = useAppSelector((state: RootState) => state.multisig.txns); - - const updateTxnStatus = useAppSelector( - (state: RootState) => state.multisig.updateTxnRes - ); - const createSignRes = useAppSelector( - (state: RootState) => state.multisig.signTxRes - ); - const createTxnRes = useAppSelector( - (state: RootState) => state.multisig.createTxnRes - ); - const deleteTxnRes = useAppSelector( - (state: RootState) => state.multisig.deleteTxnRes - ); - - const { pubkeys } = multisigAccount; - - const getAllTxs = (status: string) => { - if (accountSpecific) { - if (address) { - dispatch( - getTxns({ - address: address, - status: status, - }) - ); - } - } else { - dispatch(getAccountAllMultisigTxns({ address: walletAddress, status })); - } - }; - - const handleCreateDialogClose = () => { - setCreateDialogOpen(false); - }; - - useEffect(() => { - getAllTxs(isHistory ? 'history' : 'current'); - }, [isHistory]); - - useEffect(() => { - const result = pubkeys?.filter((keys) => { - return keys.address === walletAddress; - }); - if (result?.length) { - setIsMember(true); - } - }, [pubkeys]); - - useEffect(() => { - if (updateTxnStatus.status === 'idle') { - getAllTxs(isHistory ? 'history' : 'current'); - } - }, [updateTxnStatus]); - - useEffect(() => { - if (createSignRes.status === TxStatus.IDLE) { - dispatch(setError({ type: 'success', message: 'Successfully signed' })); - getAllTxs(isHistory ? 'history' : 'current'); - } else if (createSignRes.status === TxStatus.REJECTED) { - dispatch( - setError({ - type: 'error', - message: 'Error while signing the transaction', - }) - ); - } - }, [createSignRes]); - - useEffect(() => { - if (createTxnRes.status === TxStatus.IDLE) { - dispatch( - setError({ - type: 'success', - message: 'Transaction created successfully', - }) - ); - setIsHistory(false); - getAllTxs('current'); - } else if (createTxnRes.status === TxStatus.REJECTED) { - dispatch( - setError({ - type: 'error', - message: 'Error while creating the transaction', - }) - ); - } - }, [createTxnRes]); - - useEffect(() => { - if (deleteTxnRes.status === TxStatus.IDLE) { - dispatch( - setError({ - type: 'success', - message: 'Transaction deleted successfully', - }) - ); - getAllTxs(isHistory ? 'history' : 'current'); - } else if (deleteTxnRes.status === TxStatus.REJECTED) { - dispatch( - setError({ - type: 'error', - message: 'Error while deleting the transaction', - }) - ); - } - }, [deleteTxnRes]); - - useEffect(() => { - dispatch(resetError()); - dispatch(resetCreateTxnState()); - }, []); - - return ( -
    - - {verified ? ( - <> -
    -
    -
    Transactions
    - {accountSpecific ? ( -
    - -
    - ) : null} -
    -
    -
    -
    setIsHistory(false)} - > -
    - {!isHistory ? ( -
    - ) : null} -
    -
    Active Transactions
    -
    -
    setIsHistory(true)} - > -
    - {isHistory ? ( -
    - ) : null} -
    -
    Completed Transactions
    -
    -
    -
    -
    - - {accountSpecific ? ( - <> - - - - ) : ( - <> - - - )} - - ) : null} -
    - ); -}; - -export default MultisigSidebar; diff --git a/frontend/src/app/(routes)/multisig/components/PageMultisig.tsx b/frontend/src/app/(routes)/multisig/components/PageMultisig.tsx index 2bd85ac96..e5a615406 100644 --- a/frontend/src/app/(routes)/multisig/components/PageMultisig.tsx +++ b/frontend/src/app/(routes)/multisig/components/PageMultisig.tsx @@ -1,88 +1,119 @@ import React, { useEffect, useState } from 'react'; -import AllMultisigs from './AllMultisigs'; -import MultisigSidebar from './MultisigSidebar'; import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; import { RootState } from '@/store/store'; import { getMultisigAccounts, + resetCreateMultisigRes, resetDeleteMultisigRes, - resetVerifyAccountRes, + setVerifyDialogOpen, } from '@/store/features/multisig/multisigSlice'; -import { setAuthToken } from '@/utils/localStorage'; -import { resetError, setError } from '@/store/features/common/commonSlice'; -import VerifyAccount from './VerifyAccount'; -import { isVerified } from '@/utils/util'; +import { resetError } from '@/store/features/common/commonSlice'; +import PageHeader from '@/components/common/PageHeader'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import MultisigDashboard from './multisig-dashboard/MultisigDashboard'; +import CustomButton from '@/components/common/CustomButton'; +import DialogCreateMultisig from './create-multisig/DialogCreateMultisig'; +import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; const PageMultisig = ({ chainName }: { chainName: string }) => { const dispatch = useAppDispatch(); - const [verified, setVerified] = useState(false); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + const nameToChainIDs = useAppSelector( (state: RootState) => state.wallet.nameToChainIDs ); - const verifyAccountRes = useAppSelector( - (state) => state.multisig.verifyAccountRes + const createMultiAccRes = useAppSelector( + (state: RootState) => state.multisig.createMultisigAccountRes + ); + const multisigAccounts = useAppSelector( + (state) => state.multisig.multisigAccounts ); + const accounts = multisigAccounts.accounts; const chainID = nameToChainIDs[chainName]; const { getChainInfo } = useGetChainInfo(); - const { address } = getChainInfo(chainID); + const { address: walletAddress } = getChainInfo(chainID); + const { isAccountVerified } = useVerifyAccount({ + address: walletAddress, + }); - useEffect(() => { - if (verifyAccountRes.status === 'idle') { - setAuthToken({ - chainID: chainID, - address: address, - signature: verifyAccountRes.token, - }); - setVerified(true); - dispatch(resetVerifyAccountRes()); - } else if (verifyAccountRes.status === 'rejected') { - dispatch( - setError({ - type: 'error', - message: verifyAccountRes.error, - }) - ); - } - }, [verifyAccountRes]); + const [createDialogOpen, setCreateDialogOpen] = useState(false); - useEffect(() => { - if (isVerified({ chainID, address })) { - setVerified(true); - } else { - setVerified(false); - } - }, [address, chainID]); + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; useEffect(() => { dispatch(resetError()); dispatch(resetDeleteMultisigRes()); }, []); + const openCreateDialog = () => { + if (isAccountVerified()) { + setCreateDialogOpen(true); + } else { + dispatch(setVerifyDialogOpen(true)); + } + }; + + const closeCreateDialog = () => { + setCreateDialogOpen(false); + }; + useEffect(() => { - if (address) dispatch(getMultisigAccounts(address)); - }, []); + if (createMultiAccRes.status === 'idle') { + setCreateDialogOpen(false); + dispatch(getMultisigAccounts(walletAddress)); + dispatch(resetCreateMultisigRes()); + } + }, [createMultiAccRes]); return ( -
    - {verified ? ( - <> - +
    +
    + - + {isWalletConnected && accounts?.length ? ( + + ) : null} +
    +
    + {!isWalletConnected ? ( +
    + +
    + ) : ( + - - ) : ( - - )} + )} +
    + {isWalletConnected ? ( + + ) : null}
    ); }; diff --git a/frontend/src/app/(routes)/multisig/components/PageMultisigInfo.tsx b/frontend/src/app/(routes)/multisig/components/PageMultisigInfo.tsx deleted file mode 100644 index 057b54434..000000000 --- a/frontend/src/app/(routes)/multisig/components/PageMultisigInfo.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { - getMultisigAccounts, - getMultisigBalance, - multisigByAddress, -} from '@/store/features/multisig/multisigSlice'; -import { setAuthToken } from '@/utils/localStorage'; -import { - resetError, - setError, - setSelectedNetwork, -} from '@/store/features/common/commonSlice'; -import { - getAllValidators, - getDelegations, -} from '@/store/features/staking/stakeSlice'; -import AccountInfo from './AccountInfo'; -import MultisigSidebar from './MultisigSidebar'; -import VerifyAccount from './VerifyAccount'; -import { isVerified } from '@/utils/util'; - -interface PageMultisigInfoProps { - chainName: string; - address: string; -} - -const PageMultisigInfo: React.FC = (props) => { - const { chainName, address } = props; - const dispatch = useAppDispatch(); - const [verified, setVerified] = useState(false); - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const verifyAccountRes = useAppSelector( - (state) => state.multisig.verifyAccountRes - ); - const chainID = nameToChainIDs[chainName]; - - const { getChainInfo, getDenomInfo } = useGetChainInfo(); - const { address: walletAddress, baseURL } = getChainInfo(chainID); - const { - minimalDenom: coinMinimalDenom, - decimals: coinDecimals, - displayDenom: coinDenom, - } = getDenomInfo(chainID); - - useEffect(() => { - if (verifyAccountRes.status === 'idle') { - setAuthToken({ - chainID: chainID, - address: walletAddress, - signature: verifyAccountRes.token, - }); - setVerified(true); - } else if (verifyAccountRes.status === 'rejected') { - dispatch( - setError({ - type: 'error', - message: verifyAccountRes.error, - }) - ); - } - }, [verifyAccountRes]); - - useEffect(() => { - if (isVerified({ chainID, address: walletAddress })) { - setVerified(true); - } else { - setVerified(false); - } - }, [address, chainID]); - - useEffect(() => { - if (chainID && isVerified({ chainID, address: walletAddress })) { - dispatch( - getMultisigBalance({ baseURL, address, denom: coinMinimalDenom }) - ); - dispatch(getDelegations({ baseURL, address, chainID })); - dispatch(getAllValidators({ baseURL, chainID })); - dispatch(multisigByAddress({ address })); - dispatch(getMultisigAccounts(walletAddress)); - } - }, [chainID, verifyAccountRes]); - - useEffect(() => { - dispatch(setSelectedNetwork({ chainName: chainName })); - }, [chainName]); - - useEffect(() => { - dispatch(resetError()); - }, []); - - return ( -
    - {verified ? ( - <> - - - - ) : ( - - )} -
    - ); -}; - -export default PageMultisigInfo; diff --git a/frontend/src/app/(routes)/multisig/components/SignTxn.tsx b/frontend/src/app/(routes)/multisig/components/SignTxn.tsx deleted file mode 100644 index f53bbc1bb..000000000 --- a/frontend/src/app/(routes)/multisig/components/SignTxn.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useAppDispatch } from '@/custom-hooks/StateHooks'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { setError } from '@/store/features/common/commonSlice'; -import { signTx } from '@/store/features/multisig/multisigSlice'; -import { getWalletAmino } from '@/txns/execute'; -import { Txn } from '@/types/multisig'; -import { getAuthToken } from '@/utils/localStorage'; -import { SigningStargateClient } from '@cosmjs/stargate'; -import { toBase64 } from '@cosmjs/encoding'; -import React, { useState } from 'react'; -import { CircularProgress } from '@mui/material'; -import { ERR_UNKNOWN } from '@/utils/errors'; - -interface SignTxnProps { - address: string; - txId: number; - unSignedTxn: Txn; - isMember: boolean; - chainID: string; -} - -declare let window: WalletWindow; - -const SignTxn: React.FC = (props) => { - const { address, isMember, txId, unSignedTxn, chainID } = props; - const dispatch = useAppDispatch(); - const [load, setLoad] = useState(false); - const { getChainInfo } = useGetChainInfo(); - const { rpc, address: walletAddress } = getChainInfo(chainID); - - const signTheTx = async () => { - setLoad(true); - window.wallet.defaultOptions = { - sign: { - preferNoSetMemo: true, - preferNoSetFee: true, - disableBalanceCheck: true, - }, - }; - try { - const client = await SigningStargateClient.connect(rpc); - - const result = await getWalletAmino(chainID); - const wallet = result[0]; - const signingClient = await SigningStargateClient.offline(wallet); - - const multisigAcc = await client.getAccount(address); - if (!multisigAcc) { - dispatch( - setError({ - type: 'error', - message: 'multisig account does not exist on chain', - }) - ); - setLoad(false); - return; - } - - const signerData = { - accountNumber: multisigAcc?.accountNumber, - sequence: multisigAcc?.sequence, - chainId: chainID, - }; - - const msgs = unSignedTxn?.messages || []; - - const { signatures } = await signingClient.sign( - walletAddress, - msgs, - unSignedTxn?.fee || { amount: [], gas: '' }, - unSignedTxn?.memo || '', - signerData - ); - - const payload = { - signer: walletAddress, - txId: txId || NaN, - address: address, - signature: toBase64(signatures[0]), - }; - - const authToken = getAuthToken(chainID); - dispatch( - signTx({ - data: payload, - // below object's data in passed as query params to api request - queryParams: { - address: walletAddress, - signature: authToken?.signature || '', - }, - }) - ); - setLoad(false); - } catch (error: unknown) { - if (error instanceof Error) { - setLoad(false); - dispatch(setError({ type: 'error', message: error.message })); - } else { - dispatch(setError({ type: 'error', message: ERR_UNKNOWN })); - console.log(ERR_UNKNOWN); - } - } - }; - - return ( - - ); -}; - -export default SignTxn; diff --git a/frontend/src/app/(routes)/multisig/components/TransactionCard.tsx b/frontend/src/app/(routes)/multisig/components/TransactionCard.tsx deleted file mode 100644 index 28040c4c0..000000000 --- a/frontend/src/app/(routes)/multisig/components/TransactionCard.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import Image from 'next/image'; -import React from 'react'; -import DeleteTxn from './DeleteTxn'; -import Link from 'next/link'; -import { cleanURL } from '@/utils/util'; -import BroadCastTxn from './BroadCastTxn'; -import SignTxn from './SignTxn'; -import { Tooltip } from '@mui/material'; -import TxnMsg from './msgs/TxnMsg'; -import { Txn } from '@/types/multisig'; - -interface TransactionCardProps { - isMember: boolean; - txn: Txn; - multisigAddress: string; - threshold: number; - membersCount: number; - chainID: string; - onViewMoreAction: (txn: Txn) => void; - onViewRawAction: (txn: Txn) => void; - isHistory: boolean; - currency: Currency; - onViewError: (errMsg: string) => void; - explorerTxHashEndpoint: string; -} - -const TransactionCard: React.FC = (props) => { - const { - isMember, - txn, - multisigAddress, - threshold, - membersCount, - chainID, - onViewMoreAction, - isHistory, - currency, - onViewRawAction, - onViewError, - explorerTxHashEndpoint, - } = props; - const isReadyToBroadcast = () => { - const signs = txn?.signatures || []; - if (signs?.length >= threshold) return true; - else return false; - }; - return ( -
    -
    -
    -
    - {txn?.messages?.length === 0 ? ( -
    -
    - ) : ( -
    - -
    - )} -
    - {txn?.messages.length ? ( - - View More onViewMoreAction(txn)} - draggable={false} - /> - - ) : null} -
    -
    - {' '} - - {txn?.signatures?.length || 0}/{membersCount} - {' '} - Signed -
    -
    -
    -
    - {!isHistory ? ( - <> - {isReadyToBroadcast() ? ( - - ) : ( - - )} - - ) : ( -
    - {txn?.status === 'SUCCESS' ? ( - - Transaction Successful - - ) : ( - onViewError(txn?.err_msg || '')} - > - Transaction Failed - - )} -
    - )} -
    -
    { - if (txn) { - onViewRawAction(txn); - } - }} - > - Raw-Icon -
    - RAW -
    -
    - -
    -
    -
    -
    - ); -}; - -export default TransactionCard; diff --git a/frontend/src/app/(routes)/multisig/components/TransactionItem.tsx b/frontend/src/app/(routes)/multisig/components/TransactionItem.tsx deleted file mode 100644 index 486f62680..000000000 --- a/frontend/src/app/(routes)/multisig/components/TransactionItem.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { MultisigAddressPubkey, Txn } from '@/types/multisig'; -import React, { useEffect, useState } from 'react'; -import TxnMsg from './msgs/TxnMsg'; -import Link from 'next/link'; -import { cleanURL, isMultisigMember } from '@/utils/util'; -import BroadCastTxn from './BroadCastTxn'; -import SignTxn from './SignTxn'; -import Image from 'next/image'; -import { Tooltip } from '@mui/material'; -import DeleteTxn from './DeleteTxn'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; - -interface TransactionItemProps { - txn: Txn; - multisigAddress: string; - threshold: number; - pubKeys: MultisigAddressPubkey[]; - membersCount: number; - chainID: string; - onViewRawAction: (txn: Txn) => void; - isHistory: boolean; - currency: Currency; - explorerTxHashEndpoint: string; - onViewError: (errMsg: string) => void; -} - -const TransactionItem: React.FC = (props) => { - const { - txn, - multisigAddress, - pubKeys, - threshold, - membersCount, - chainID, - isHistory, - currency, - onViewRawAction, - explorerTxHashEndpoint, - onViewError, - } = props; - const isReadyToBroadcast = () => { - const signs = txn?.signatures || []; - if (signs?.length >= threshold) return true; - else return false; - }; - - const [isMember, setIsMember] = useState(false); - const { getChainInfo } = useGetChainInfo(); - const { address: walletAddress } = getChainInfo(chainID); - - useEffect(() => { - const result = isMultisigMember(pubKeys, walletAddress); - setIsMember(result); - }, [pubKeys, walletAddress]); - - return ( -
    -
    -
    - {txn?.messages.map((msg, index) => { - return ( -
    -
    -
    {`#${index + 1}`}
    - -
    -
    - ); - })} -
    -
    -
    - {txn?.signatures?.length || 0}/{membersCount} -
    - {isHistory ? ( -
    - {txn?.status === 'SUCCESS' ? ( - - Success - - ) : ( - onViewError(txn?.err_msg || '')} - > - Failed - - )} -
    - ) : null} -
    - {!isHistory ? ( - <> - {isReadyToBroadcast() ? ( - - ) : ( - - )} - - ) : null} - -
    { - if (txn) { - onViewRawAction(txn); - } - }} - > - Raw-Icon -
    - RAW -
    -
    -
    - -
    -
    - ); -}; - -export default TransactionItem; diff --git a/frontend/src/app/(routes)/multisig/components/TransactionsList.tsx b/frontend/src/app/(routes)/multisig/components/TransactionsList.tsx deleted file mode 100644 index 35e3b2188..000000000 --- a/frontend/src/app/(routes)/multisig/components/TransactionsList.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { RootState } from '@/store/store'; -import { Txn, Txns } from '@/types/multisig'; -import { EMPTY_TXN } from '@/utils/constants'; -import React, { useEffect, useMemo, useState } from 'react'; -import DialogViewRaw from './DialogViewRaw'; -import DialogTxnFailed from './DialogTxnFailed'; -import DialogViewTxnMessages from './DialogViewTxnMessages'; -import TransactionCard from './TransactionCard'; -import Image from 'next/image'; -import { TxStatus } from '@/types/enums'; -import { CircularProgress } from '@mui/material'; - -interface TransactionsListProps { - chainID: string; - isMember: boolean; - txnsState: Txns; - isHistory: boolean; -} - -const TransactionsList: React.FC = (props) => { - const { chainID, isMember, txnsState, isHistory } = props; - const multisigAccount = useAppSelector( - (state: RootState) => state.multisig.multisigAccount - ); - const members = multisigAccount.pubkeys || []; - const [msgDialogOpen, setMsgDialogOpen] = useState(false); - const [viewRawOpen, setViewRawDialogOpen] = useState(false); - const [viewErrorOpen, setViewErrorDialogOpen] = useState(false); - - const toggleMsgDialogOpen = () => { - setMsgDialogOpen((prevState) => !prevState); - }; - - const toggleViewRawDialogOpen = () => { - setViewRawDialogOpen((prevState) => !prevState); - }; - - const handleMsgDialogClose = () => { - setMsgDialogOpen(false); - }; - - const [selectedTxn, setSelectedTxn] = useState(EMPTY_TXN); - const [errMsg, setErrMsg] = useState(''); - - const onViewMoreAction = (txn: Txn) => { - setSelectedTxn(txn); - setMsgDialogOpen(true); - }; - - const onViewRawAction = (txn: Txn) => { - setSelectedTxn(txn); - setViewRawDialogOpen(true); - }; - - const onViewError = (errMsg: string) => { - setErrMsg(errMsg); - setViewErrorDialogOpen(true); - }; - - const { getDenomInfo, getChainInfo } = useGetChainInfo(); - const { explorerTxHashEndpoint } = getChainInfo(chainID); - const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); - const createSignRes = useAppSelector( - (state: RootState) => state.multisig.signTxRes - ); - const updateTxnState = useAppSelector( - (state: RootState) => state.multisig.updateTxnRes - ); - const txnsLoading = useAppSelector( - (state: RootState) => state.multisig?.txns?.status - ); - const currency = useMemo( - () => ({ - coinMinimalDenom: minimalDenom, - coinDecimals: decimals, - coinDenom: displayDenom, - }), - [minimalDenom, decimals, displayDenom] - ); - - useEffect(() => { - if (createSignRes.status !== TxStatus.PENDING) { - setMsgDialogOpen(false); - } - }, [createSignRes.status]); - - useEffect(() => { - if (updateTxnState.status === TxStatus.IDLE) { - setMsgDialogOpen(false); - } - }, [updateTxnState.status]); - - return ( -
    - {txnsState.list.map((txn) => ( - - ))} -
    - {txnsLoading !== TxStatus.PENDING && !txnsState.list.length ? ( -
    - {'No -
    - No Transactions -
    -
    - ) : null} - {txnsLoading === TxStatus.PENDING ? ( - - ) : null} -
    - - - setViewErrorDialogOpen(false)} - errMsg={errMsg} - /> -
    - ); -}; - -export default TransactionsList; diff --git a/frontend/src/app/(routes)/multisig/components/VerifyAccount.tsx b/frontend/src/app/(routes)/multisig/components/VerifyAccount.tsx deleted file mode 100644 index 61186ad3a..000000000 --- a/frontend/src/app/(routes)/multisig/components/VerifyAccount.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import TopNav from '@/components/TopNav'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { verifyAccount } from '@/store/features/multisig/multisigSlice'; -import { RootState } from '@/store/store'; -import { TxStatus } from '@/types/enums'; -import { CircularProgress } from '@mui/material'; -import Image from 'next/image'; -import React from 'react'; - -interface VerifyAccountProps { - chainID: string; - walletAddress: string; -} - -const VerifyAccount: React.FC = (props) => { - const { chainID, walletAddress } = props; - const dispatch = useAppDispatch(); - const handleVerifyAccountEvent = () => { - dispatch(verifyAccount({ chainID, address: walletAddress })); - }; - const loading = useAppSelector( - (state: RootState) => state.multisig.verifyAccountRes.status - ); - return ( -
    -
    - -
    - Verify Ownership -
    - Please verify your account ownership to proceed. -
    - -
    - ); -}; - -export default VerifyAccount; diff --git a/frontend/src/app/(routes)/multisig/components/common/BroadCastTxn.tsx b/frontend/src/app/(routes)/multisig/components/common/BroadCastTxn.tsx new file mode 100644 index 000000000..a8b0f7d95 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/common/BroadCastTxn.tsx @@ -0,0 +1,89 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { setError } from '@/store/features/common/commonSlice'; +import { + broadcastTransaction, + resetUpdateTxnState, + setVerifyDialogOpen, +} from '@/store/features/multisig/multisigSlice'; +import { RootState } from '@/store/store'; +import { MultisigAddressPubkey, Txn } from '@/types/multisig'; +import React, { useEffect } from 'react'; +import { FAILED_TO_BROADCAST_ERROR } from '@/utils/errors'; +import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; +import CustomButton from '@/components/common/CustomButton'; + +interface BroadCastTxnProps { + txn: Txn; + multisigAddress: string; + threshold: number; + pubKeys: MultisigAddressPubkey[]; + chainID: string; + isMember: boolean; +} + +const BroadCastTxn: React.FC = (props) => { + const { txn, multisigAddress, pubKeys, threshold, chainID, isMember } = props; + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { + address: walletAddress, + restURLs: baseURLs, + rpcURLs, + } = getChainInfo(chainID); + const { isAccountVerified } = useVerifyAccount({ + address: walletAddress, + }); + + const updateTxnRes = useAppSelector( + (state: RootState) => state.multisig.updateTxnRes + ); + + useEffect(() => { + if (updateTxnRes.status === 'rejected') { + dispatch( + setError({ + type: 'error', + message: updateTxnRes?.error || FAILED_TO_BROADCAST_ERROR, + }) + ); + } + }, [updateTxnRes]); + + useEffect(() => { + return () => { + dispatch(resetUpdateTxnState()); + }; + }, []); + + const broadcastTxn = async () => { + if (!isAccountVerified()) { + dispatch(setVerifyDialogOpen(true)); + return; + } + dispatch( + broadcastTransaction({ + chainID, + multisigAddress, + signedTxn: txn, + walletAddress, + threshold, + pubKeys, + baseURLs, + rpcURLs, + }) + ); + }; + return ( + { + broadcastTxn(); + }} + btnDisabled={!isMember} + btnStyles="w-[115px]" + /> + ); +}; + +export default BroadCastTxn; diff --git a/frontend/src/app/(routes)/multisig/components/common/DialogVerifyAccount.tsx b/frontend/src/app/(routes)/multisig/components/common/DialogVerifyAccount.tsx new file mode 100644 index 000000000..ac6b1ac97 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/common/DialogVerifyAccount.tsx @@ -0,0 +1,85 @@ +import { Dialog, DialogContent } from '@mui/material'; +import React from 'react'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setVerifyDialogOpen } from '@/store/features/multisig/multisigSlice'; +import Image from 'next/image'; +import CustomButton from '@/components/common/CustomButton'; +import { VERIFY_ILLUSTRATION } from '@/constants/image-names'; +import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; +import { TxStatus } from '@/types/enums'; + +const DialogVerifyAccount = ({ walletAddress }: { walletAddress: string }) => { + const dispatch = useAppDispatch(); + const { verifyOwnership } = useVerifyAccount({ + address: walletAddress, + }); + + const open = useAppSelector((state) => state.multisig.verifyDialogOpen); + const loadingState = useAppSelector( + (state) => state.multisig.verifyAccountRes.status + ); + const isLoading = loadingState === TxStatus.PENDING; + + const handleClose = () => { + dispatch(setVerifyDialogOpen(false)); + }; + + const handleVerifyAccountEvent = () => { + verifyOwnership(); + }; + + return ( + + +
    + +
    +
    + Verify Ownership +
    +
    Ownership
    +
    + Verify your ownership to continue +
    +
    +
    + +
    +
    +
    +
    + ); +}; + +export default DialogVerifyAccount; diff --git a/frontend/src/app/(routes)/multisig/components/common/Loader.tsx b/frontend/src/app/(routes)/multisig/components/common/Loader.tsx new file mode 100644 index 000000000..1d93ebe1f --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/common/Loader.tsx @@ -0,0 +1,42 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { TxStatus } from '@/types/enums'; +import { CircularProgress, Dialog, DialogContent } from '@mui/material'; +import React from 'react'; + +const Loader = () => { + const signTxStatus = useAppSelector( + (state) => state.multisig.signTransactionRes + ); + const broadcastTxnStatus = useAppSelector( + (state) => state.multisig.broadcastTxnRes + ); + const signTxLoading = signTxStatus.status === TxStatus.PENDING; + const broadcastTxnLoading = broadcastTxnStatus.status === TxStatus.PENDING; + + return ( + + +
    + +
    + Loading + +
    +
    +
    +
    + ); +}; + +export default Loader; diff --git a/frontend/src/app/(routes)/multisig/components/common/MoreOptions.tsx b/frontend/src/app/(routes)/multisig/components/common/MoreOptions.tsx new file mode 100644 index 000000000..53c296d85 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/common/MoreOptions.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +const MoreOptions = ({ + setOptionsOpen, + hanldeDeleteTxn, + onViewRaw, + allowRepeat, + onRepeat, +}: { + setOptionsOpen: (value: boolean) => void; + hanldeDeleteTxn: () => void; + onViewRaw: () => void; + allowRepeat: boolean; + onRepeat: () => void; +}) => { + return ( +
    setOptionsOpen(true)} + onMouseLeave={() => setOptionsOpen(false)} + > + + + {allowRepeat ? ( + + ) : null} +
    + ); +}; + +export default MoreOptions; diff --git a/frontend/src/app/(routes)/multisig/components/common/SignTxn.tsx b/frontend/src/app/(routes)/multisig/components/common/SignTxn.tsx new file mode 100644 index 000000000..784c6f61b --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/common/SignTxn.tsx @@ -0,0 +1,57 @@ +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { + setVerifyDialogOpen, + signTransaction, +} from '@/store/features/multisig/multisigSlice'; +import { Txn } from '@/types/multisig'; +import React from 'react'; +import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; +import CustomButton from '@/components/common/CustomButton'; + +interface SignTxnProps { + address: string; + txId: number; + unSignedTxn: Txn; + isMember: boolean; + chainID: string; +} + +const SignTxn: React.FC = (props) => { + const { address, isMember, unSignedTxn, chainID } = props; + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { address: walletAddress, rpcURLs } = getChainInfo(chainID); + const { isAccountVerified } = useVerifyAccount({ + address: walletAddress, + }); + + const signTheTx = async () => { + if (!isAccountVerified()) { + dispatch(setVerifyDialogOpen(true)); + return; + } + dispatch( + signTransaction({ + chainID, + multisigAddress: address, + unSignedTxn, + walletAddress, + rpcURLs + }) + ); + }; + + return ( + { + signTheTx(); + }} + btnStyles="w-[115px]" + /> + ); +}; + +export default SignTxn; diff --git a/frontend/src/app/(routes)/multisig/components/common/TxnsCard.tsx b/frontend/src/app/(routes)/multisig/components/common/TxnsCard.tsx new file mode 100644 index 000000000..b10e1d786 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/common/TxnsCard.tsx @@ -0,0 +1,351 @@ +import { + DROP_DOWN_CLOSE, + DROP_DOWN_OPEN, + MENU_ICON, + REDIRECT_ICON_GREEN, + REDIRECT_ICON_RED, +} from '@/constants/image-names'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { + createTxn, + deleteTxn, + setVerifyDialogOpen, +} from '@/store/features/multisig/multisigSlice'; +import { TxStatus } from '@/types/enums'; +import { Txn } from '@/types/multisig'; +import { COSMOS_CHAIN_ID } from '@/utils/constants'; +import { getAuthToken } from '@/utils/localStorage'; +import { cleanURL, isMultisigMember } from '@/utils/util'; +import Image from 'next/image'; +import React, { useEffect, useRef, useState } from 'react'; +import TxnMsg from '../msgs/TxnMsg'; +import Link from 'next/link'; +import MoreOptions from './MoreOptions'; +import DialogConfirmDelete from '../multisig-account/DialogConfirmDelete'; +import CustomDialog from '@/components/common/CustomDialog'; +import BroadCastTxn from './BroadCastTxn'; +import SignTxn from './SignTxn'; +import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; +import { fee } from '@/txns/execute'; +import DialogRepeatTxn from '../DialogRepeatTxn'; +import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; + +export const TxnsCard = ({ + txn, + currency, + threshold, + multisigAddress, + chainID, + isHistory, + onViewError, + allowRepeat, +}: { + txn: Txn; + currency: Currency; + threshold: number; + multisigAddress: string; + chainID: string; + isHistory: boolean; + onViewError?: (errMsg: string) => void; + allowRepeat?: boolean; +}) => { + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { address: walletAddress, explorerTxHashEndpoint } = + getChainInfo(chainID); + const { isAccountVerified } = useVerifyAccount({ + address: walletAddress, + }); + + const [showAll, setShowAll] = useState(false); + const [viewRawOpen, setViewRawOpen] = useState(false); + const [repeatTxnOpen, setRepeatTxnOpen] = useState(false); + const { messages } = txn; + const pubKeys = txn.pubkeys || []; + const isMember = isMultisigMember(pubKeys, walletAddress); + const isReadyToBroadcast = () => { + const signs = txn?.signatures || []; + if (signs?.length >= threshold) return true; + else return false; + }; + + const [optionsOpen, setOptionsOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const menuRef = useRef(null); + const menuRef2 = useRef(null); + + const loading = useAppSelector((state) => state.multisig.deleteTxnRes.status); + + const hanldeDeleteTxn = () => { + if (isAccountVerified()) { + setDeleteDialogOpen(true); + } else { + dispatch(setVerifyDialogOpen(true)); + } + }; + + const onDeleteTxn = () => { + const authToken = getAuthToken(COSMOS_CHAIN_ID); + dispatch( + deleteTxn({ + data: { + address: multisigAddress, + id: txn.id, + }, + queryParams: { + address: walletAddress, + signature: authToken?.signature || '', + }, + }) + ); + }; + + const onRepeatTxn = (data: TxnBuilderForm) => { + const feeObj = fee( + currency.coinMinimalDenom, + data.fees.toString(), + data.gas + ); + const authToken = getAuthToken(COSMOS_CHAIN_ID); + dispatch( + createTxn({ + data: { + address: multisigAddress, + chain_id: chainID, + messages: data.msgs, + fee: feeObj, + memo: data.memo, + gas: data.gas, + }, + queryParams: { + address: walletAddress, + signature: authToken?.signature || '', + }, + }) + ); + }; + + const handleRepeatTx = () => { + if (isAccountVerified()) { + setRepeatTxnOpen(true); + } else { + dispatch(setVerifyDialogOpen(true)); + } + }; + + const confirmRepeatTxn = () => { + const gas = txn.fee.gas; + const msgs = txn.messages; + const memo = txn.memo; + const fees = txn.fee.amount; + onRepeatTxn({ + fees: Number(fees[0].amount), + memo, + gas: Number(gas), + msgs, + }); + }; + + useEffect(() => { + const handleOutsideClick = (event: MouseEvent) => { + if ( + menuRef.current && + !menuRef.current.contains(event.target as Node) && + menuRef2.current && + !menuRef2.current.contains(event.target as Node) + ) { + setOptionsOpen(false); + } + }; + + document.addEventListener('mousedown', handleOutsideClick); + + return () => { + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, []); + + useEffect(() => { + if (loading === TxStatus.IDLE || loading === TxStatus.REJECTED) { + setDeleteDialogOpen(false); + } + }, [loading]); + + return ( +
    +
    +
    Transaction Messages
    +
    +
    +
    +
    #1
    + +
    + {messages?.length > 1 ? ( + setShowAll((prev) => !prev)} + /> + ) : null} +
    + {showAll + ? messages.slice(1, messages?.length).map((msg, index) => ( +
    +
    +
    {`#${index + 2}`}
    + +
    +
    + )) + : null} +
    +
    +
    +
    + {isHistory || isReadyToBroadcast() ? 'Last Updated' : 'Created'} +
    +
    + + {isHistory || isReadyToBroadcast() + ? getTimeDifferenceToFutureDate(txn.last_updated, true) + : getTimeDifferenceToFutureDate(txn.created_at, true)} + +
    +
    + {isHistory ? ( +
    +
    Status
    +
    + {txn?.status === 'SUCCESS' ? ( + +
    + Success +
    + + + ) : ( +
    +
    onViewError?.(txn?.err_msg || '')} + className="text-[#F15757] underline underline-offset-[3px]" + > + Failed +
    + +
    + )} +
    +
    + ) : ( +
    +
    Signed
    +
    + {txn.signatures.length} + / + {threshold} +
    +
    + )} +
    +
    + {!isHistory ? ( + <> + {isReadyToBroadcast() ? ( + + ) : ( + + )} + + ) : null} +
    setOptionsOpen(true)} + onMouseLeave={() => setOptionsOpen(false)} + className="cursor-pointer" + > + Menu +
    +
    +
    + {optionsOpen ? ( + setOptionsOpen(value)} + hanldeDeleteTxn={hanldeDeleteTxn} + onViewRaw={() => setViewRawOpen(true)} + allowRepeat={!!allowRepeat} + onRepeat={handleRepeatTx} + /> + ) : null} + setDeleteDialogOpen(false)} + title="Delete Transaction" + description=" Are you sure you want to delete the transaction ?" + onDelete={onDeleteTxn} + loading={loading === TxStatus.PENDING} + /> + setViewRawOpen(false)} + title="Raw Transaction" + > +
    + {txn ? ( +
    {JSON.stringify(txn, undefined, 2)}
    + ) : ( +
    - No Data -
    + )} +
    +
    + setRepeatTxnOpen(false)} + confirmRepeat={confirmRepeatTxn} + /> +
    + ); +}; + +export default TxnsCard; + +const ExpandViewButton = ({ + showAll, + toggleView, +}: { + showAll: boolean; + toggleView: () => void; +}) => { + return ( + toggleView()} + src={showAll ? DROP_DOWN_CLOSE : DROP_DOWN_OPEN} + width={16} + height={16} + alt="" + /> + ); +}; diff --git a/frontend/src/app/(routes)/multisig/components/create-multisig/AddMember.tsx b/frontend/src/app/(routes)/multisig/components/create-multisig/AddMember.tsx new file mode 100644 index 000000000..034719597 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/create-multisig/AddMember.tsx @@ -0,0 +1,48 @@ +import { PubKeyFields } from '@/types/multisig'; +import React, { ChangeEvent } from 'react'; +import AddMemberButton from './AddMembersButton'; +import MultisigMemberTextField from './MultisigMemberTextField'; + +const AddMembers = ({ + handleChangeValue, + handleRemoveValue, + importMultisig, + pubKeyFields, + togglePubKey, + handleAddPubKey, +}: { + pubKeyFields: PubKeyFields[]; + handleRemoveValue: (i: number) => void; + handleChangeValue: ( + index: number, + e: ChangeEvent + ) => void; + togglePubKey: (index: number) => void; + importMultisig: boolean; + handleAddPubKey: () => void; +}) => { + return ( +
    +
    +
    Add Members
    +
    + {pubKeyFields.map((field, index) => ( +
    + +
    + ))} +
    +
    + {!importMultisig && } +
    + ); +}; + +export default AddMembers; diff --git a/frontend/src/app/(routes)/multisig/components/create-multisig/AddMembersButton.tsx b/frontend/src/app/(routes)/multisig/components/create-multisig/AddMembersButton.tsx new file mode 100644 index 000000000..f54a6414f --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/create-multisig/AddMembersButton.tsx @@ -0,0 +1,24 @@ +import { ADD_ICON } from '@/constants/image-names'; +import Image from 'next/image'; +import React from 'react'; + +const AddMemberButton = ({ + handleAddPubKey, +}: { + handleAddPubKey: () => void; +}) => { + return ( +
    + +
    + ); +}; + +export default AddMemberButton; diff --git a/frontend/src/app/(routes)/multisig/components/create-multisig/DialogCreateMultisig.tsx b/frontend/src/app/(routes)/multisig/components/create-multisig/DialogCreateMultisig.tsx new file mode 100644 index 000000000..7ca6fe95a --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/create-multisig/DialogCreateMultisig.tsx @@ -0,0 +1,509 @@ +import { TextField } from '@mui/material'; +import React, { ChangeEvent, useEffect, useState } from 'react'; +import { setError } from '@/store/features/common/commonSlice'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + generateMultisigAccount, + isValidPubKey, +} from '@/txns/multisig/multisig'; +import { getAuthToken } from '@/utils/localStorage'; +import { + createAccount, + importMultisigAccount, + resetMultisigAccountData, +} from '@/store/features/multisig/multisigSlice'; +import { RootState } from '@/store/store'; +import { createMultisigTextFieldStyles } from '../../styles'; +import { + ADDRESS_NOT_FOUND, + DUPLICATE_PUBKEYS_ERROR, + FAILED_TO_GENERATE_MULTISIG, + INVALID_PUBKEY, + MAX_PUBKEYS_ERROR, + MAX_THRESHOLD_ERROR, + MIN_PUBKEYS_ERROR, + MIN_THRESHOLD_ERROR, +} from '@/utils/errors'; +import { TxStatus } from '@/types/enums'; +import { fromBech32 } from '@cosmjs/encoding'; +import { DialogCreateMultisigProps, PubKeyFields } from '@/types/multisig'; +import { + COSMOS_CHAIN_ID, + DECREASE, + INCREASE, + MULTISIG_PUBKEY_OBJECT, +} from '@/utils/constants'; +import useGetPubkey from '@/custom-hooks/useGetPubkey'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import useGetAccountInfo from '@/custom-hooks/useGetAccountInfo'; +import CustomDialog from '@/components/common/CustomDialog'; +import CustomButton from '@/components/common/CustomButton'; +import ImportMultisig from './ImportMultisig'; +import AddMembers from './AddMember'; +import Threshold from './Threshold'; + +const MAX_PUB_KEYS = 7; +const MULTISIG_NAME_MAX_LENGTH = 100; + +const DialogCreateMultisig: React.FC = (props) => { + const { open, onClose, chainID } = props; + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { + address, + prefix: addressPrefix, + restURLs: baseURLs, + } = getChainInfo(chainID); + const [accountInfo] = useGetAccountInfo(chainID); + const { pubkey: pubKey } = accountInfo; + const [name, setName] = useState(''); + const [pubKeyFields, setPubKeyFields] = useState([]); + const [threshold, setThreshold] = useState(1); + const [formError, setFormError] = useState(''); + const [importMultisig, setImportMultisig] = useState(false); + const [page, setPage] = useState(1); + const [multisigAddress, setMultisigAddress] = useState(''); + const [addressValidationError, setAddressValidationError] = useState(''); + + const { getPubkey, pubkeyLoading } = useGetPubkey(); + + const createMultiAccRes = useAppSelector( + (state: RootState) => state.multisig.createMultisigAccountRes + ); + const importMultisigAccountRes = useAppSelector( + (state: RootState) => state.multisig.multisigAccountData + ); + + const pubKeyObj = { ...MULTISIG_PUBKEY_OBJECT }; + + useEffect(() => { + setDefaultFormValues(); + }, [pubKey]); + + const setDefaultFormValues = () => { + //By default current account is added + setPubKeyFields([ + { + name: 'current', + value: pubKey, + label: 'Public Key (Secp256k1)', + placeHolder: 'E. g. AtgCrYjD+21d1+og3inzVEOGbCf5uhXnVeltFIo7RcRp', + required: true, + disabled: true, + pubKey: pubKey, + address: address, + isPubKey: true, + error: '', + }, + { ...pubKeyObj }, + ]); + setName(''); + setThreshold(1); + setPage(1); + }; + + const handleClose = () => { + resetCreateMultisig(); + onClose(); + }; + + const resetCreateMultisig = () => { + setMultisigAddress(''); + setAddressValidationError(''); + setDefaultFormValues(); + setImportMultisig(false); + }; + + const handleNameChange = (e: ChangeEvent) => { + if (e.target.value.length > MULTISIG_NAME_MAX_LENGTH) { + return; + } + setName(e.target.value); + }; + + const handleMultisigAddressChange = (e: ChangeEvent) => { + validateMultisigAddress(e.target.value.trim()); + setMultisigAddress(e.target.value.trim()); + }; + + const togglePubKey = (index: number) => { + const pubKeysList = [...pubKeyFields]; + pubKeysList[index].isPubKey = !pubKeysList[index].isPubKey; + pubKeysList[index].error = ''; + setPubKeyFields(pubKeysList); + }; + + const handleRemoveValue = (i: number) => { + if (pubKeyFields.length > 1) { + pubKeyFields.splice(i, 1); + setPubKeyFields([...pubKeyFields]); + } + }; + + const handleChangeValue = ( + index: number, + e: ChangeEvent + ) => { + const newInputFields = pubKeyFields.map((value, key) => { + if (e.target.name === 'address') { + if (index === key) { + value['address'] = e.target.value; + value['value'] = e.target.value; + } + return value; + } else { + if (index === key) { + value['pubKey'] = e.target.value; + value['value'] = e.target.value; + } + return value; + } + }); + + setPubKeyFields(newInputFields); + }; + + const handleAddPubKey = () => { + if (pubKeyFields?.length >= MAX_PUB_KEYS) { + dispatch( + setError({ + type: 'error', + message: MAX_PUBKEYS_ERROR, + }) + ); + return; + } else { + setPubKeyFields([...pubKeyFields, pubKeyObj]); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setFormError(''); + + if (Number(threshold) < 1) { + dispatch(setError({ type: 'error', message: MIN_THRESHOLD_ERROR })); + return; + } + + if (!pubKeyFields?.length) { + dispatch(setError({ type: 'error', message: MIN_PUBKEYS_ERROR })); + return; + } + + let isValid = true; + const pubKeyValidationPromises = pubKeyFields.map(async (field, index) => { + if (!field.isPubKey) { + const pubKey = await getPubkey(field.address, baseURLs); + if (pubKey.length) { + return { index, pubKey, error: '' }; + } else { + isValid = false; + return { index, pubKey: '', error: ADDRESS_NOT_FOUND }; + } + } else if (field.pubKey.length) { + if (!isValidPubKey(field.pubKey)) { + isValid = false; + return { index, pubKey: field.pubKey, error: INVALID_PUBKEY }; + } else { + return { index, pubKey: field.pubKey, error: '' }; + } + } + return { index, pubKey: '', error: '' }; + }); + + const results = await Promise.all(pubKeyValidationPromises); + results.forEach((result) => { + const pubKeysList = [...pubKeyFields]; + pubKeysList[result.index].pubKey = result.pubKey; + pubKeysList[result.index].error = result.error; + setPubKeyFields(pubKeysList); + }); + + if (!isValid) { + return; + } + + const pubKeys = pubKeyFields.map((v) => v.pubKey); + + const uniquePubKeys = Array.from(new Set(pubKeys)); + if (uniquePubKeys?.length !== pubKeys?.length) { + dispatch( + setError({ + type: 'error', + message: DUPLICATE_PUBKEYS_ERROR, + }) + ); + return; + } + + for (let i = 0; i < uniquePubKeys.length; i++) { + if (!isValidPubKey(uniquePubKeys[i])) { + setFormError(`pubKey at ${i + 1} is invalid`); + return; + } + } + + try { + const res = generateMultisigAccount( + pubKeys, + Number(threshold), + addressPrefix + ); + const authToken = getAuthToken(COSMOS_CHAIN_ID); + const queryParams = { + address: address, + signature: authToken?.signature || '', + }; + dispatch( + createAccount({ + queryParams: queryParams, + data: { + address: res.address, + chainId: chainID, + pubkeys: res.pubkeys, + createdBy: address, + name: name, + threshold: res.threshold, + }, + }) + ); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + dispatch( + setError({ + type: 'error', + message: error?.message || FAILED_TO_GENERATE_MULTISIG, + }) + ); + } + }; + + useEffect(() => { + if (createMultiAccRes?.status === 'idle') { + const message = importMultisig + ? 'Successfully Imported' + : ' Successfully Created'; + resetCreateMultisig(); + dispatch(setError({ type: 'success', message: message })); + } else if (createMultiAccRes?.status === 'rejected') { + dispatch(setError({ type: 'error', message: createMultiAccRes?.error })); + } + }, [createMultiAccRes]); + + const fetchMultisigAccount = () => { + if (validateMultisigAddress(multisigAddress)) { + dispatch( + importMultisigAccount({ + accountAddress: address, + multisigAddress: multisigAddress, + baseURLs: baseURLs, + addressPrefix: addressPrefix, + chainID: chainID, + }) + ); + } + }; + + const validateMultisigAddress = (address: string): boolean => { + if (address.length) { + try { + fromBech32(address); + setAddressValidationError(''); + return true; + } catch (error) { + setAddressValidationError('Invalid address'); + return false; + } + } else { + setAddressValidationError('Please enter address'); + return false; + } + }; + + const setMultisigAccountData = () => { + const pubKeysList = + importMultisigAccountRes.account?.account?.pub_key?.public_keys || []; + const data: PubKeyFields[] = []; + pubKeysList.forEach((pubkey: PubKey) => { + data.push({ + name: 'pubKey', + value: pubkey.key, + label: 'Public Key (Secp256k1)', + placeHolder: 'E. g. AtgCrYjD+21d1+og3inzVEOGbCf5uhXnVeltFIo7RcRp', + required: true, + disabled: true, + isPubKey: true, + address: '', + pubKey: pubkey.key, + error: '', + }); + }); + setPubKeyFields(data); + setThreshold( + Number(importMultisigAccountRes.account?.account?.pub_key?.threshold || 0) + ); + }; + + const handleThresholdChange = (value: string) => { + if (value === INCREASE) { + if (threshold + 1 > pubKeyFields?.length) { + dispatch(setError({ type: 'error', message: MAX_THRESHOLD_ERROR })); + } else { + setThreshold(threshold + 1); + } + } else if (value === DECREASE) { + if (threshold - 1 < 1) { + dispatch(setError({ type: 'error', message: MIN_THRESHOLD_ERROR })); + } else { + setThreshold(threshold - 1); + } + } + }; + + const switchToCreateMultisig = () => { + setImportMultisig(false); + setDefaultFormValues(); + }; + + useEffect(() => { + if (importMultisigAccountRes.status === TxStatus.IDLE) { + setImportMultisig(true); + setPage(1); + setMultisigAccountData(); + } else if (importMultisigAccountRes.status === TxStatus.REJECTED) { + dispatch( + setError({ + type: 'error', + message: importMultisigAccountRes?.error, + }) + ); + } + }, [importMultisigAccountRes.status]); + + useEffect(() => { + dispatch(resetMultisigAccountData()); + }, []); + + return ( + +
    + {page === 1 ? ( +
    +
    +
    handleSubmit(e)} + > +
    +
    +
    Name
    + +
    +
    +
    Threshold
    + +
    +
    + +
    + {importMultisig ? ( + + ) : null} + + {formError ? ( +
    + {formError} +
    + ) : null} +
    + + {!importMultisig ? ( +
    +
    + Have an existing MultiSig account ? Import it +
    {' '} + +
    + ) : null} +
    +
    + ) : ( + + )} +
    +
    + ); +}; + +export default DialogCreateMultisig; diff --git a/frontend/src/app/(routes)/multisig/components/create-multisig/ImportMultisig.tsx b/frontend/src/app/(routes)/multisig/components/create-multisig/ImportMultisig.tsx new file mode 100644 index 000000000..6a0da16a7 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/create-multisig/ImportMultisig.tsx @@ -0,0 +1,85 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { TextField } from '@mui/material'; +import React, { ChangeEvent } from 'react'; +import { createMultisigTextFieldStyles } from '../../styles'; +import CustomButton from '@/components/common/CustomButton'; +import { TxStatus } from '@/types/enums'; + +const ImportMultisig = ({ + addressValidationError, + fetchMultisigAccount, + handleMultisigAddressChange, + multisigAddress, + switchToCreateMultisig, +}: { + multisigAddress: string; + handleMultisigAddressChange: (e: ChangeEvent) => void; + addressValidationError: string; + fetchMultisigAccount: () => void; + switchToCreateMultisig: () => void; +}) => { + const importMultisigAccountRes = useAppSelector( + (state) => state.multisig.multisigAccountData + ); + + return ( +
    +
    +
    +
    Import Multisig
    +
    +
    + + +
    +
    + {addressValidationError || ''} +
    +
    +
    +
    +
    +
    + Do not have an existing MultiSig account ? Create New +
    {' '} + +
    +
    + ); +}; + +export default ImportMultisig; diff --git a/frontend/src/app/(routes)/multisig/components/create-multisig/MultisigMemberTextField.tsx b/frontend/src/app/(routes)/multisig/components/create-multisig/MultisigMemberTextField.tsx new file mode 100644 index 000000000..e779efd1e --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/create-multisig/MultisigMemberTextField.tsx @@ -0,0 +1,119 @@ +import { InputTextComponentProps } from '@/types/multisig'; +import { InputAdornment, TextField } from '@mui/material'; +import React from 'react'; +import { createMultisigTextFieldStyles } from '../../styles'; +import Image from 'next/image'; +import { REMOVE_ICON, TOGGLE_OFF, TOGGLE_ON } from '@/constants/image-names'; + +const MultisigMemberTextField: React.FC = (props) => { + const { + field, + index, + handleChangeValue, + handleRemoveValue, + togglePubKey, + isImport, + } = props; + return ( +
    + handleChangeValue(index, e)} + name={field.isPubKey ? 'pubKey' : 'address'} + value={ + index === 0 + ? field.address + : field.isPubKey + ? field.pubKey + : field.address + } + required={field?.required} + placeholder={field.isPubKey ? 'Public Key (Secp256k1)' : 'Address'} + sx={createMultisigTextFieldStyles} + fullWidth + disabled={field.disabled} + InputProps={{ + endAdornment: + !field.disabled && !isImport ? ( + +
    + togglePubKey(index)} + isPubKey={field.isPubKey} + /> + + !field.disabled + ? handleRemoveValue(index) + : alert('Cannot self remove') + } + /> +
    +
    + ) : index === 0 ? ( + +
    (You)
    +
    + ) : null, + sx: { + input: { + color: 'white', + fontSize: '14px', + padding: 2, + }, + }, + }} + /> +
    + {field.error.length ? field.error : ''} +
    +
    + ); +}; + +export default MultisigMemberTextField; + +const TogglePubkey = ({ + toggle, + isPubKey, +}: { + toggle: () => void; + isPubKey: boolean; +}) => { + return ( + + ); +}; + +const RemoveButton = ({ onClick }: { onClick: () => void }) => { + return ( + + ); +}; diff --git a/frontend/src/app/(routes)/multisig/components/create-multisig/Threshold.tsx b/frontend/src/app/(routes)/multisig/components/create-multisig/Threshold.tsx new file mode 100644 index 000000000..50a2cb0f8 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/create-multisig/Threshold.tsx @@ -0,0 +1,57 @@ +import { + MINUS_ICON, + MINUS_ICON_DISABLED, + PLUS_ICON, + PLUS_ICON_DISABLED, +} from '@/constants/image-names'; +import { DECREASE, INCREASE } from '@/utils/constants'; +import Image from 'next/image'; +import React from 'react'; + +const Threshold = ({ + handleThresholdChange, + threshold, + membersCount, + isImportMultisig, +}: { + handleThresholdChange: (value: string) => void; + threshold: number; + membersCount: number; + isImportMultisig: boolean; +}) => { + const incDisabled = threshold >= membersCount; + const decDisabled = threshold <= 1; + return ( +
    + +
    {threshold}
    + +
    + ); +}; + +export default Threshold; diff --git a/frontend/src/app/(routes)/multisig/components/loaders/MultisigAccountsLoading.tsx b/frontend/src/app/(routes)/multisig/components/loaders/MultisigAccountsLoading.tsx new file mode 100644 index 000000000..f0a01a948 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/loaders/MultisigAccountsLoading.tsx @@ -0,0 +1,23 @@ +import SectionHeader from '@/components/common/SectionHeader'; +import React from 'react'; + +const MultisigAccountsLoading = () => { + return ( +
    + +
    + {[1, 2, 3].map((_, index) => ( +
    + ))} +
    +
    + ); +}; + +export default MultisigAccountsLoading; diff --git a/frontend/src/app/(routes)/multisig/components/loaders/MultisigInfoLoading.tsx b/frontend/src/app/(routes)/multisig/components/loaders/MultisigInfoLoading.tsx new file mode 100644 index 000000000..fb48245f9 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/loaders/MultisigInfoLoading.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +const MultisigInfoLoading = () => { + return ( +
    +
    + Back to List +
    +
    +
    +

    +

    +

    +
    +
    +
    + +
    +
    + +
    + {[1, 2, 3, 4].map((_, index) => ( +
    + ))} +
    + +
    +
    +
    +
    +
    +

    Transactions

    +

    Members

    +
    +
    +
    +
    + +
    +
    +

    +

    +

    +

    +

    + +
    +
    +
    + ); +}; + +export default MultisigInfoLoading; diff --git a/frontend/src/app/(routes)/multisig/components/loaders/TransactionsLoading.tsx b/frontend/src/app/(routes)/multisig/components/loaders/TransactionsLoading.tsx new file mode 100644 index 000000000..d12aa6f2f --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/loaders/TransactionsLoading.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +const TransactionsLoading = () => { + return ( +
    +

    +

    +
    + ); +}; + +export default TransactionsLoading; diff --git a/frontend/src/app/(routes)/multisig/components/msgs/TxnMsg.tsx b/frontend/src/app/(routes)/multisig/components/msgs/TxnMsg.tsx index dc27c3935..9d0aeabb1 100644 --- a/frontend/src/app/(routes)/multisig/components/msgs/TxnMsg.tsx +++ b/frontend/src/app/(routes)/multisig/components/msgs/TxnMsg.tsx @@ -1,9 +1,11 @@ +import useGetAllAssets from '@/custom-hooks/multisig/useGetAllAssets'; import { DELEGATE_TYPE_URL, MAP_TXNS, REDELEGATE_TYPE_URL, SEND_TYPE_URL, UNDELEGATE_TYPE_URL, + VOTE_TYPE_URL, } from '@/utils/constants'; import { parseTokens } from '@/utils/denom'; import { shortenAddress } from '@/utils/util'; @@ -12,10 +14,19 @@ import React from 'react'; interface TxnMsg { msg: Msg; currency: Currency; + chainID: string; } +const voteOptions: Record = { + '1': 'Yes', + '2': 'Abstain', + '3': 'No', + '4': 'No With Veto', +}; + const TxnMsg: React.FC = (props) => { - const { msg, currency } = props; + const { msg, currency, chainID } = props; + const { getParsedAsset } = useGetAllAssets(); const displayDenom = (amountObj: Coin[] | Coin) => { if (Array.isArray(amountObj)) { @@ -28,50 +39,76 @@ const TxnMsg: React.FC = (props) => { ); } }; - return ( -
    - {msg ? ( -
    - {msg.typeUrl === SEND_TYPE_URL ? ( -

    - {MAP_TXNS[msg?.typeUrl]}   - {displayDenom(msg?.value?.amount)} -  To {' '} - {shortenAddress(msg?.value?.toAddress, 20)} -

    - ) : null} - {msg.typeUrl === DELEGATE_TYPE_URL ? ( -

    - {MAP_TXNS[msg?.typeUrl]}{' '} - {displayDenom(msg?.value?.amount)} -   To   - {shortenAddress(msg?.value?.validatorAddress, 20)} -

    - ) : null} + const renderMessage = () => { + switch (msg?.typeUrl) { + case SEND_TYPE_URL: + const { assetInfo } = getParsedAsset({ + amount: msg.value?.amount?.[0]?.amount, + chainID, + denom: msg.value?.amount?.[0]?.denom, + }); + return ( +

    + {MAP_TXNS[msg?.typeUrl]}   + + {assetInfo?.amountInDenom} {assetInfo?.displayDenom} + +  To  + {shortenAddress(msg?.value?.toAddress, 20)} +

    + ); + + case DELEGATE_TYPE_URL: + return ( +

    + {MAP_TXNS[msg?.typeUrl]}{' '} + {displayDenom(msg?.value?.amount)} +   To   + {shortenAddress(msg?.value?.validatorAddress, 20)} +

    + ); + + case UNDELEGATE_TYPE_URL: + return ( +

    + {MAP_TXNS[msg?.typeUrl]}{' '} + {displayDenom(msg?.value?.amount)} +   From   + {shortenAddress(msg?.value?.validatorAddress, 20)} +

    + ); - {msg.typeUrl === UNDELEGATE_TYPE_URL ? ( -

    - {MAP_TXNS[msg?.typeUrl]}{' '} - {displayDenom(msg?.value?.amount)} -   From   - {shortenAddress(msg?.value?.validatorAddress, 20)} -

    - ) : null} + case REDELEGATE_TYPE_URL: + return ( +

    + {MAP_TXNS[msg?.typeUrl]}{' '} + {displayDenom(msg?.value?.amount)} +   From   + {shortenAddress(msg?.value?.validatorSrcAddress, 20)} +   To   + {shortenAddress(msg?.value?.validatorDstAddress, 20)} +

    + ); - {msg.typeUrl === REDELEGATE_TYPE_URL ? ( -

    - {MAP_TXNS[msg?.typeUrl]}{' '} - {displayDenom(msg?.value?.amount)} -   - From   - {shortenAddress(msg?.value?.validatorSrcAddress, 20)} -   To   - {shortenAddress(msg?.value?.validatorDstAddress, 20)} -

    - ) : null} -
    - ) : null} + case VOTE_TYPE_URL: + return ( +

    + {MAP_TXNS[msg?.typeUrl]}{' '} + {voteOptions?.[msg.value.option.toString()]} +  on proposal  + #{msg.value.proposalId} +

    + ); + + default: + return
    {msg?.typeUrl}
    ; + } + }; + + return ( +
    + {msg ?
    {renderMessage()}
    : null}
    ); }; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/AccountInfo.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/AccountInfo.tsx new file mode 100644 index 000000000..989d11c55 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/AccountInfo.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const AccountInfo = () => { + return
    AccountInfo
    ; +}; + +export default AccountInfo; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/DialogConfirmDelete.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/DialogConfirmDelete.tsx new file mode 100644 index 000000000..965458753 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/DialogConfirmDelete.tsx @@ -0,0 +1,74 @@ +import CustomButton from '@/components/common/CustomButton'; +import { DELETE_ILLUSTRATION } from '@/constants/image-names'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const DialogConfirmDelete = ({ + open, + onClose, + onDelete, + title, + description, + loading, +}: { + open: boolean; + onClose: () => void; + onDelete: () => void; + title: string; + description: string; + loading: boolean; +}) => { + return ( + + +
    + +
    +
    + Delete +
    +
    {title}
    +
    {description}
    +
    +
    + +
    +
    +
    +
    + ); +}; + +export default DialogConfirmDelete; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/DialogDeleteMultisig.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/DialogDeleteMultisig.tsx new file mode 100644 index 000000000..a19fa120b --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/DialogDeleteMultisig.tsx @@ -0,0 +1,72 @@ +import CustomButton from '@/components/common/CustomButton'; +import { DELETE_ILLUSTRATION } from '@/constants/image-names'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const DialogDeleteMultisig = ({ + open, + onClose, + onDelete, +}: { + open: boolean; + onClose: () => void; + onDelete: () => void; +}) => { + const loading = useAppSelector((state) => state.multisig.deleteMultisigRes); + return ( + + +
    + +
    +
    + Verify Ownership +
    +
    Delete Multisig
    +
    + Are you sure you want to delete this multisig ? +
    +
    +
    + +
    +
    +
    +
    + ); +}; + +export default DialogDeleteMultisig; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/DialogTxnFailed.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/DialogTxnFailed.tsx new file mode 100644 index 000000000..465db39b5 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/DialogTxnFailed.tsx @@ -0,0 +1,27 @@ +import CustomDialog from '@/components/common/CustomDialog'; +import React from 'react'; + +const DialogTxnFailed = ({ + onClose, + open, + errMsg, +}: { + open: boolean; + onClose: () => void; + errMsg: string; +}) => { + return ( + +
    + {errMsg} +
    +
    + ); +}; + +export default DialogTxnFailed; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccount.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccount.tsx new file mode 100644 index 000000000..471293d0b --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccount.tsx @@ -0,0 +1,483 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { + getMultisigAccounts, + getMultisigBalances, + multisigByAddress, + resetBroadcastTxnRes, + resetCreateTxnState, + resetsignTransactionRes, + resetUpdateTxnState, + setVerifyDialogOpen, +} from '@/store/features/multisig/multisigSlice'; +import { getDelegations } from '@/store/features/staking/stakeSlice'; +import { useEffect, useState } from 'react'; +import MultisigAccountHeader from './MultisigAccountHeader'; +import Copy from '@/components/common/Copy'; +import { checkForIBCTokens, parseBalance } from '@/utils/denom'; +import Transactions from './Transactions'; +import Loader from '../common/Loader'; +import Link from 'next/link'; +import { TxStatus } from '@/types/enums'; +import DialogVerifyAccount from '../common/DialogVerifyAccount'; +import MultisigInfoLoading from '../loaders/MultisigInfoLoading'; +import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; +import { useRouter } from 'next/navigation'; +import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; +import { Dialog, DialogContent } from '@mui/material'; +import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import SectionHeader from '@/components/common/SectionHeader'; +import useGetAllAssets from '@/custom-hooks/multisig/useGetAllAssets'; +import NumberFormat from '@/components/common/NumberFormat'; + +const MultisigAccount = ({ + chainName, + multisigAddress, +}: { + chainName: string; + multisigAddress: string; +}) => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + + const chainID = nameToChainIDs[chainName]; + + const multigAccountRes = useAppSelector( + (state) => state.multisig.multisigAccount.status + ); + + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { address: walletAddress, baseURL, restURLs } = getChainInfo(chainID); + const { + minimalDenom: coinMinimalDenom, + decimals: coinDecimals, + displayDenom: coinDenom, + } = getDenomInfo(chainID); + const currency = { + coinMinimalDenom, + coinDecimals, + coinDenom, + }; + const { isAccountVerified } = useVerifyAccount({ + address: walletAddress, + }); + + const [selectedTab, setSelectedTab] = useState('Transactions'); + + const multisigAccount = useAppSelector( + (state) => state.multisig.multisigAccount + ); + const members = multisigAccount.pubkeys.map((pubkey) => pubkey.address); + + const { name: multisigName, created_at: createdTime } = + multisigAccount.account; + + const isAdmin = + multisigAccount?.account?.created_by === (walletAddress || ''); + + const handleChange = (tab: string) => { + setSelectedTab(tab); + }; + + useEffect(() => { + if (chainID) { + dispatch( + getMultisigBalances({ + baseURL, + address: multisigAddress, + baseURLs: restURLs, + chainID, + }) + ); + dispatch( + getDelegations({ + baseURLs: restURLs, + address: multisigAddress, + chainID, + }) + ); + dispatch(multisigByAddress({ address: multisigAddress })); + dispatch(getMultisigAccounts(walletAddress)); + } + }, [chainID]); + + useEffect(() => { + dispatch(resetCreateTxnState()); + dispatch(resetUpdateTxnState()); + dispatch(resetBroadcastTxnRes()); + dispatch(resetsignTransactionRes()); + }, []); + + const createNewTxn = () => { + if (!isAccountVerified()) { + dispatch(setVerifyDialogOpen(true)); + return; + } + router.push(`/multisig/${chainName}/${multisigAddress}/create-txn`); + }; + + return ( +
    + {multigAccountRes === TxStatus.PENDING ? ( +
    + +
    + ) : ( +
    +
    + + Back to List + + +
    +
    + +
    + +
    + {selectedTab === 'Transactions' ? ( + + ) : ( + + )} +
    +
    +
    + + +
    + )} +
    + ); +}; + +export default MultisigAccount; + +const MultisigAccountInfo = ({ + chainID, + coinMinimalDenom, + currency, + createdTime, +}: { + chainID: string; + coinMinimalDenom: string; + currency: Currency; + createdTime: string; +}) => { + const [availableBalance, setAvailableBalance] = useState(0); + const [hasIBCTokens, setHasIBCTokens] = useState(false); + + const multisigAccount = useAppSelector( + (state) => state.multisig.multisigAccount + ); + const multisigAccounts = useAppSelector( + (state) => state.multisig.multisigAccounts + ); + const totalStaked = useAppSelector( + (state) => state.staking.chains?.[chainID]?.delegations.totalStaked + ); + const balance = useAppSelector((state) => state.multisig.balance.balance); + + const stakedTokens = [ + { + amount: totalStaked?.toString() || '', + denom: coinMinimalDenom, + }, + ]; + const stakedBalance = parseBalance( + stakedTokens, + currency.coinDecimals, + currency.coinMinimalDenom + ); + const { txnCounts = {} } = multisigAccounts; + const actionsRequired = txnCounts?.[multisigAccount?.account?.address] || 0; + + useEffect(() => { + setAvailableBalance( + parseBalance(balance, currency.coinDecimals, currency.coinMinimalDenom) + ); + setHasIBCTokens(checkForIBCTokens(balance, currency.coinMinimalDenom)); + }, [balance]); + + return ( +
    + +
    + ); +}; + +const MultisigAccountStats = ({ + actionsRequired, + created, + stakedBalance, + availableBalance, + hasIBCTokens, + chainID, + displayDenom, +}: { + actionsRequired: number; + created: string; + stakedBalance: number; + availableBalance: number; + hasIBCTokens: boolean; + chainID: string; + displayDenom: string; +}) => { + const [viewIBC, setViewIBC] = useState(false); + const stats = [ + { + name: 'Actions Required', + value: actionsRequired, + }, + { + name: 'Created', + value: created + ' ago', + }, + ]; + return ( +
    +
    +
    + Available Balance +
    +
    +
    + +
    + {hasIBCTokens ? ( + + ) : null} +
    +
    +
    +
    + Staked Balance +
    +
    +
    + +
    +
    +
    + {stats.map((stat) => ( + + ))} + setViewIBC(false)} + open={viewIBC} + chainID={chainID} + /> +
    + ); +}; + +const MultisigAccountStatsCard = ({ + name, + value, + action, + actionName, +}: { + name: string; + value: string | number; + action?: () => void; + actionName?: string; +}) => { + return ( +
    +
    {name}
    + {actionName ? ( +
    +
    {value}
    + +
    + ) : ( +
    {value}
    + )} +
    + ); +}; + +const MultisigMembersList = ({ members }: { members: string[] }) => { + return ( +
    + {members.map((address, index) => ( + + ))} +
    + ); +}; + +const MultisigMember = ({ + address, + index, +}: { + address: string; + index: number; +}) => { + return ( +
    +
    Member #{index}
    +
    +
    {address}
    + +
    +
    + ); +}; + +const TabsGroup = ({ + handleChange, + selectedTab, + tabs, + createNewTxn, +}: { + tabs: string[]; + handleChange: (tab: string) => void; + selectedTab: string; + createNewTxn: () => void; +}) => { + return ( +
    +
    + {tabs.map((tab) => ( +
    + +
    +
    + ))} +
    + +
    + ); +}; + +const DialogMultisigAssets = ({ + onClose, + open, + chainID, +}: { + open: boolean; + onClose: () => void; + chainID: string; +}) => { + const handleClose = () => { + onClose(); + }; + const { getAllAssets } = useGetAllAssets(); + const { allAssets } = getAllAssets(chainID, false, true); + return ( + + +
    +
    + +
    + +
    + {allAssets.length === 0 ? ( +
    No IBC assets found
    + ) : ( + allAssets.map((asset) => ( +
    +
    + +
    +
    + )) + )} +
    +
    +
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccountHeader.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccountHeader.tsx new file mode 100644 index 000000000..cf0bbf4eb --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccountHeader.tsx @@ -0,0 +1,127 @@ +import CustomButton from '@/components/common/CustomButton'; +import LetterAvatar from '@/components/common/LetterAvatar'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; +import { + deleteMultisig, + setVerifyDialogOpen, +} from '@/store/features/multisig/multisigSlice'; +import { COSMOS_CHAIN_ID } from '@/utils/constants'; +import { getAuthToken } from '@/utils/localStorage'; +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { setError } from '@/store/features/common/commonSlice'; +import DialogConfirmDelete from './DialogConfirmDelete'; +import Copy from '@/components/common/Copy'; + +const MultisigAccountHeader = ({ + isAdmin, + multisigName, + multisigAddress, + walletAddress, + chainName, + threshold, + membersCount, +}: { + isAdmin: boolean; + multisigName: string; + multisigAddress: string; + walletAddress: string; + chainName: string; + threshold: number; + membersCount: number; +}) => { + const dispatch = useAppDispatch(); + const router = useRouter(); + const { isAccountVerified } = useVerifyAccount({ + address: walletAddress, + }); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const deleteMultisigRes = useAppSelector( + (state) => state.multisig.deleteMultisigRes + ); + const loading = useAppSelector((state) => state.multisig.deleteMultisigRes); + + const handleDeleteMultisig = () => { + const authToken = getAuthToken(COSMOS_CHAIN_ID); + if (isAdmin) { + dispatch( + deleteMultisig({ + data: { address: multisigAddress }, + queryParams: { + address: walletAddress, + signature: authToken?.signature || '', + }, + }) + ); + } + }; + + const onDeleteMultisig = () => { + if (!isAccountVerified()) { + dispatch(setVerifyDialogOpen(true)); + return; + } + setDeleteDialogOpen(true); + }; + + useEffect(() => { + if (deleteMultisigRes.status === 'idle') { + dispatch( + setError({ message: 'Account Deleted Successfully', type: 'success' }) + ); + setTimeout(() => { + router.push(`/multisig/${chainName}`); + }, 500); + } + }, [deleteMultisigRes]); + + return ( +
    +
    +
    +
    +
    + +
    +
    + {multisigName} +
    +
    +
    + {threshold} +
    +
    {`/${membersCount} Threshold`}
    +
    +
    +
    +
    +
    +
    + {multisigAddress} +
    + +
    +
    +
    +
    + {isAdmin ? ( + + ) : null} + setDeleteDialogOpen(false)} + onDelete={handleDeleteMultisig} + title="Delete Multisig" + description=" Are you sure you want to delete this multisig ?" + loading={loading.status === 'pending'} + /> +
    + ); +}; + +export default MultisigAccountHeader; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/PageMultisigInfo.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/PageMultisigInfo.tsx new file mode 100644 index 000000000..1bb6eb62e --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/PageMultisigInfo.tsx @@ -0,0 +1,59 @@ +'use client'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import React from 'react'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import MultisigAccount from './MultisigAccount'; + +const PageMultisigInfo = ({ + paramChain, + paramAddress, +}: { + paramChain: string; + paramAddress: string; +}) => { + const dispatch = useAppDispatch(); + const nameToChainIDs = useAppSelector( + (state: RootState) => state.common.nameToChainIDs + ); + const chainName = paramChain.toLowerCase(); + const validChain = chainName in nameToChainIDs; + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + + return ( +
    + {validChain ? ( + <> + {isWalletConnected ? ( + + ) : ( +
    + +
    + )} + + ) : ( + <> +
    + - The {chainName} is not supported - +
    + + )} +
    + ); +}; + +export default PageMultisigInfo; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/Transactions.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/Transactions.tsx new file mode 100644 index 000000000..78c3427be --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/Transactions.tsx @@ -0,0 +1,269 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getTxns } from '@/store/features/multisig/multisigSlice'; +import { Txn } from '@/types/multisig'; +import React, { useEffect, useState } from 'react'; +import { TxStatus } from '@/types/enums'; +import TxnsCard from '../common/TxnsCard'; +import useFetchTxns from '@/custom-hooks/multisig/useFetchTxns'; +import NoData from '@/components/common/NoData'; +import TransactionsLoading from '../loaders/TransactionsLoading'; +import DialogTxnFailed from './DialogTxnFailed'; + +const TXNS_TYPES = [ + { option: 'to-sign', value: 'To be Signed' }, + { option: 'to-broadcast', value: 'To be Broadcasted' }, + { option: 'completed', value: 'Completed' }, + { option: 'failed', value: 'Failed' }, +]; + +const Transactions = ({ + chainID, + multisigAddress, + currency, + threshold, +}: { + chainID: string; + multisigAddress: string; + currency: Currency; + threshold: number; +}) => { + const dispatch = useAppDispatch(); + const txnsState = useAppSelector((state) => state.multisig.txns.list); + + const [txnsList, setTxnsList] = useState([]); + const [txnsType, setTxnsType] = useState('to-sign'); + + const fetchTxns = (status: string) => { + dispatch(getTxns({ address: multisigAddress, status: status })); + }; + + const txnsCount = useAppSelector((state) => state.multisig.txns.Count) + const txnsStatus = useAppSelector((state) => state.multisig.txns.status); + const deleteTxnRes = useAppSelector((state) => state.multisig.deleteTxnRes); + const signTxStatus = useAppSelector( + (state) => state.multisig.signTransactionRes + ); + const updateTxStatus = useAppSelector((state) => state.multisig.updateTxnRes); + + const handleTxnsTypeChange = (type: string) => { + setTxnsType(type); + if (['failed', 'history', 'completed'].includes(type)) { + fetchTxns('history'); + } else { + fetchTxns('current'); + } + }; + + const isReadyToBroadcast = (txn: Txn) => { + const signs = txn?.signatures || []; + if (signs?.length >= threshold) return true; + else return false; + }; + + const isTxnCompleted = (txn: Txn) => { + if (txn.status === 'SUCCESS') { + return true; + } + return false; + }; + + useEffect(() => { + if (chainID && multisigAddress) { + fetchTxns('current'); + } + }, [chainID, multisigAddress]); + + useEffect(() => { + let filteredTxns: Txn[] = []; + if (txnsType === 'to-broadcast') { + filteredTxns = txnsState.filter((txn) => { + return isReadyToBroadcast(txn); + }); + } else if (txnsType === 'to-sign') { + filteredTxns = txnsState.filter((txn) => { + return !isReadyToBroadcast(txn); + }); + } else if (txnsType === 'completed') { + filteredTxns = txnsState.filter((txn) => { + return isTxnCompleted(txn); + }); + } else if (txnsType === 'failed') { + filteredTxns = txnsState.filter((txn) => { + return !isTxnCompleted(txn); + }); + } + + setTxnsList(filteredTxns); + }, [txnsState]); + + useEffect(() => { + if (deleteTxnRes.status === TxStatus.IDLE) { + if (['failed', 'history', 'completed'].includes(txnsType)) { + fetchTxns('history'); + } else { + fetchTxns('current'); + } + } + }, [deleteTxnRes]); + + useEffect(() => { + if ( + signTxStatus.status === TxStatus.IDLE || + updateTxStatus.status === TxStatus.IDLE || + updateTxStatus.status === TxStatus.REJECTED + ) { + if (['failed', 'history', 'completed'].includes(txnsType)) { + fetchTxns('history'); + } else { + fetchTxns('current'); + } + } + }, [signTxStatus.status, updateTxStatus.status]); + + // To reset state after singing or broadcasting txn + useFetchTxns(); + + const createRes = useAppSelector((state) => state.multisig.createTxnRes); + + useEffect(() => { + if (createRes?.status === 'idle') { + setTxnsType('to-sign'); + fetchTxns('current'); + } + }, [createRes]); + + return ( +
    +
    + + {txnsStatus === TxStatus.PENDING ? ( + + ) : ( +
    + {txnsList?.length ? ( + + ) : ( + + )} +
    + )} +
    +
    + ); +}; + +export default Transactions; + +const TransactionsFilters = ({ + handleTxnsTypeChange, + txnsType, + txnsCount, +}: { + txnsType: string; + handleTxnsTypeChange: (type: string) => void; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + txnsCount: any; +}) => { + + const getCount = (option: string) => { + let count = 0; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + txnsCount && txnsCount.forEach((t: any) => { + if (t?.computed_status?.toLowerCase() === option.toLowerCase()) { + count = t?.count + } + }) + + return count + } + + return ( +
    + {TXNS_TYPES.map((type) => ( + handleTxnsTypeChange(type.option)} + /> + ))} +
    + ); +}; + +const TransactionFilterItem = ({ + name, + isSelected, + onClick, +}: { + name: string; + isSelected: boolean; + onClick: () => void; +}) => { + return ( + + ); +}; + +const TransactionsList = ({ + txns, + currency, + threshold, + multisigAddress, + chainID, + txnsType, +}: { + txns: Txn[]; + currency: Currency; + threshold: number; + multisigAddress: string; + chainID: string; + txnsType: string; +}) => { + const isHistory = ['completed', 'failed'].includes(txnsType); + const [viewErrorDialogOpen, setViewErrorDialogOpen] = + useState(false); + const [errMsg, setErrMsg] = useState(''); + const onViewError = (errMsg: string) => { + setErrMsg(errMsg); + setViewErrorDialogOpen(true); + }; + return ( +
    + {txns.map((txn, index) => ( + + ))} + setViewErrorDialogOpen(false)} + open={viewErrorDialogOpen} + /> +
    + ); +}; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-dashboard/AllMultisigAccounts.tsx b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/AllMultisigAccounts.tsx new file mode 100644 index 000000000..0c3816f2e --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/AllMultisigAccounts.tsx @@ -0,0 +1,70 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React from 'react'; +import MultisigAccountCard from './MultisigAccountCard'; +import { TxStatus } from '@/types/enums'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { NO_DATA_ILLUSTRATION } from '@/constants/image-names'; +import MultisigAccountsLoading from '../loaders/MultisigAccountsLoading'; +import SectionHeader from '@/components/common/SectionHeader'; + +const AllMultisigAccounts = ({ + chainName, + setCreateDialogOpen, +}: { + chainName: string; + setCreateDialogOpen: () => void; +}) => { + const multisigAccounts = useAppSelector( + (state) => state.multisig.multisigAccounts + ); + const txnsState = useAppSelector((state) => state.multisig.txns.list); + const accounts = multisigAccounts.accounts; + const pendingTxns = multisigAccounts.txnCounts; + const status = multisigAccounts.status; + + return ( + <> + {status === TxStatus.PENDING ? ( + + ) : ( +
    + {txnsState?.length ? ( + + ) : null} + {accounts?.length ? ( +
    + {accounts.map((account) => ( + + ))} +
    + ) : ( +
    + +
    + )} +
    + )} + + ); +}; + +export default AllMultisigAccounts; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigAccountCard.tsx b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigAccountCard.tsx new file mode 100644 index 000000000..ca01a9335 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigAccountCard.tsx @@ -0,0 +1,77 @@ +import Copy from '@/components/common/Copy'; +import LetterAvatar from '@/components/common/LetterAvatar'; +import { shortenAddress } from '@/utils/util'; +import Link from 'next/link'; +import React from 'react'; + +interface MultisigAccountCardProps { + multisigAddress: string; + threshold: number; + name: string; + actionsRequired: number; + chainName: string; +} + +const MultisigAccountCard = (props: MultisigAccountCardProps) => { + const { actionsRequired, multisigAddress, chainName, name, threshold } = + props; + return ( + + +
    + + + +
    + + ); +}; + +export default MultisigAccountCard; + +const MultisigName = ({ name }: { name: string }) => { + return ( +
    +
    {name ? : null}
    +
    {name}
    +
    + ); +}; + +const MultisigAddress = ({ address }: { address: string }) => { + return ( +
    +
    Address
    +
    +
    {shortenAddress(address, 12)}
    + +
    +
    + ); +}; + +const MultisigDetail = ({ + title, + value, + isAddress = false, +}: { + title: string; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + value: any; + isAddress?: boolean; +}) => { + return ( +
    +
    {title}
    +
    +
    + {isAddress ? shortenAddress(value, 15) : value} +
    + {isAddress ? : null} +
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigDashboard.tsx b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigDashboard.tsx new file mode 100644 index 000000000..6f24b2778 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigDashboard.tsx @@ -0,0 +1,147 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + getAccountAllMultisigTxns, + getMultisigAccounts, + resetBroadcastTxnRes, + resetCreateMultisigRes, + resetCreateTxnState, + resetsignTransactionRes, + resetSignTxnState, + resetUpdateTxnState, +} from '@/store/features/multisig/multisigSlice'; +import React, { useEffect } from 'react'; +import AllMultisigAccounts from './AllMultisigAccounts'; +import RecentTransactions from './RecentTransactions'; +import { setError } from '@/store/features/common/commonSlice'; +import { TxStatus } from '@/types/enums'; +import Loader from '../common/Loader'; +import DialogVerifyAccount from '../common/DialogVerifyAccount'; + +interface MultisigDashboardI { + walletAddress: string; + chainName: string; + chainID: string; + setCreateDialogOpen: () => void; +} + +const MultisigDashboard: React.FC = (props) => { + const { walletAddress, chainName, chainID, setCreateDialogOpen } = props; + const dispatch = useAppDispatch(); + + const createMultiAccRes = useAppSelector( + (state) => state.multisig.createMultisigAccountRes + ); + + const signTxStatus = useAppSelector( + (state) => state.multisig.signTransactionRes + ); + const broadcastTxnStatus = useAppSelector( + (state) => state.multisig.broadcastTxnRes + ); + const updateTxStatus = useAppSelector((state) => state.multisig.updateTxnRes); + const deleteTxnRes = useAppSelector((state) => state.multisig.deleteTxnRes); + + useEffect(() => { + if (walletAddress) { + dispatch(getMultisigAccounts(walletAddress)); + } + }, [walletAddress]); + + useEffect(() => { + if (createMultiAccRes.status === 'idle') { + dispatch(getMultisigAccounts(walletAddress)); + dispatch(resetCreateMultisigRes()); + } + }, [createMultiAccRes]); + + const fetchAllTransactions = () => { + dispatch( + getAccountAllMultisigTxns({ address: walletAddress, status: 'current' }) + ); + }; + + const resetSignTxn = () => { + dispatch(resetSignTxnState()); + dispatch(resetsignTransactionRes()); + }; + + const resetBroadcastTxn = () => { + dispatch(resetUpdateTxnState()); + dispatch(resetBroadcastTxnRes()); + }; + + useEffect(() => { + if (walletAddress) { + fetchAllTransactions(); + } + }, [walletAddress]); + + useEffect(() => { + if (signTxStatus.status === TxStatus.IDLE) { + dispatch(setError({ type: 'success', message: 'Successfully signed' })); + resetSignTxn(); + fetchAllTransactions(); + } else if (signTxStatus.status === TxStatus.REJECTED) { + dispatch( + setError({ + type: 'error', + message: signTxStatus.error || 'Error while signing the transaction', + }) + ); + resetSignTxn(); + fetchAllTransactions(); + } + }, [signTxStatus]); + + useEffect(() => { + if (broadcastTxnStatus.status === TxStatus.IDLE) { + dispatch( + setError({ type: 'success', message: 'Broadcasted successfully' }) + ); + resetBroadcastTxn(); + } else if (broadcastTxnStatus.status === TxStatus.REJECTED) { + dispatch( + setError({ + type: 'error', + message: broadcastTxnStatus.error || 'Failed to broadcasted', + }) + ); + resetBroadcastTxn(); + } + }, [broadcastTxnStatus]); + + useEffect(() => { + if (updateTxStatus.status === TxStatus.IDLE) { + fetchAllTransactions(); + } else if (updateTxStatus.status === TxStatus.REJECTED) { + fetchAllTransactions(); + } + }, [updateTxStatus]); + + useEffect(() => { + if (deleteTxnRes.status === TxStatus.IDLE) { + fetchAllTransactions(); + } + }, [deleteTxnRes]); + + useEffect(() => { + dispatch(resetCreateTxnState()); + dispatch(resetUpdateTxnState()); + dispatch(resetBroadcastTxnRes()); + dispatch(resetsignTransactionRes()); + }, []); + + return ( +
    + + + + +
    + ); +}; + +export default MultisigDashboard; diff --git a/frontend/src/app/(routes)/multisig/components/multisig-dashboard/RecentTransactions.tsx b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/RecentTransactions.tsx new file mode 100644 index 000000000..a2f6cbacb --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/RecentTransactions.tsx @@ -0,0 +1,124 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React, { useMemo, useState } from 'react'; +import { Txn } from '@/types/multisig'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import LetterAvatar from '@/components/common/LetterAvatar'; +import TxnsCard from '../common/TxnsCard'; +import { TxStatus } from '@/types/enums'; +import TransactionsLoading from '../loaders/TransactionsLoading'; + +const RecentTransactions = ({ chainID }: { chainID: string }) => { + const { getDenomInfo } = useGetChainInfo(); + const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); + const multisigAccounts = useAppSelector( + (state) => state.multisig.multisigAccounts + ); + const accounts = multisigAccounts.accounts; + const txnsState = useAppSelector((state) => state.multisig.txns.list); + const txnsLoading = useAppSelector((state) => state.multisig.txns.status); + + const currency = useMemo( + () => ({ + coinMinimalDenom: minimalDenom, + coinDecimals: decimals, + coinDenom: displayDenom, + }), + [minimalDenom, decimals, displayDenom] + ); + + return ( + <> + {txnsState?.length ? ( +
    +
    + {accounts.map((account) => { + const txns = txnsState.filter( + (txn) => txn.multisig_address === account.address + ); + return ( + <> + {txns?.length ? ( + + ) : null} + + ); + })} +
    +
    + ) : ( +
    + {txnsLoading === TxStatus.PENDING ? : null} +
    + )} + + ); +}; + +export default RecentTransactions; + +const MultisigAccountRecentTxns = ({ + actionsRequired, + multisigName, + txns, + currency, + threshold, + multisigAddress, + chainID, +}: { + multisigName: string; + actionsRequired: number; + txns: Txn[]; + currency: Currency; + threshold: number; + multisigAddress: string; + chainID: string; +}) => { + const [showAllTxns, setShowAllTxns] = useState(false); + + const handleToggleView = () => { + setShowAllTxns(!showAllTxns); + }; + + const displayedTxns = showAllTxns ? txns : txns.slice(0, 1); + + return ( +
    +
    +
    + +
    {multisigName}
    +
    +
    + {actionsRequired} Actions Required +
    +
    +
    + {displayedTxns.map((txn, index) => ( + + ))} +
    +
    + +
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/multisig/components/txns/ReDelegate.tsx b/frontend/src/app/(routes)/multisig/components/txns/ReDelegate.tsx index 54ca52810..00b9b3697 100644 --- a/frontend/src/app/(routes)/multisig/components/txns/ReDelegate.tsx +++ b/frontend/src/app/(routes)/multisig/components/txns/ReDelegate.tsx @@ -12,6 +12,7 @@ import { } from '../../styles'; import { getDelegations } from '@/store/features/staking/stakeSlice'; import { INSUFFICIENT_BALANCE } from '@/utils/errors'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; interface ReDelegateProps { chainID: string; @@ -23,8 +24,10 @@ interface ReDelegateProps { } const ReDelegate: React.FC = (props) => { - const { chainID, address, onDelegate, currency, baseURL } = props; - const dispatch = useAppDispatch(); + const { chainID, address, onDelegate, currency } = props; + const {getChainInfo} = useGetChainInfo() + const {restURLs} = getChainInfo(chainID) + const dispatch = useAppDispatch(); const { handleSubmit, @@ -49,7 +52,7 @@ const ReDelegate: React.FC = (props) => { ) useEffect(() => { - dispatch(getDelegations({ address, chainID, baseURL })) + dispatch(getDelegations({ address, chainID, baseURLs: restURLs })) }, []) interface stakeBal { diff --git a/frontend/src/app/(routes)/multisig/components/txns/UnDelegate.tsx b/frontend/src/app/(routes)/multisig/components/txns/UnDelegate.tsx index 57ed3f5f0..85dea220a 100644 --- a/frontend/src/app/(routes)/multisig/components/txns/UnDelegate.tsx +++ b/frontend/src/app/(routes)/multisig/components/txns/UnDelegate.tsx @@ -12,6 +12,7 @@ import { } from '../../styles'; import { getDelegations } from '@/store/features/staking/stakeSlice'; import { INSUFFICIENT_BALANCE } from '@/utils/errors'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; interface UnDelegateProps { chainID: string; @@ -23,9 +24,10 @@ interface UnDelegateProps { } const UnDelegate: React.FC = (props) => { - const { chainID, address, onDelegate, currency, baseURL } = props; + const { chainID, address, onDelegate, currency } = props; const dispatch = useAppDispatch(); - + const {getChainInfo} = useGetChainInfo(); + const {restURLs} = getChainInfo(chainID) const { handleSubmit, control, @@ -48,7 +50,7 @@ const UnDelegate: React.FC = (props) => { ); useEffect(() => { - dispatch(getDelegations({ address, chainID, baseURL })); + dispatch(getDelegations({ address, chainID, baseURLs: restURLs })); }, []); interface stakeBal { diff --git a/frontend/src/app/(routes)/multisig/error.tsx b/frontend/src/app/(routes)/multisig/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/multisig/loading.tsx b/frontend/src/app/(routes)/multisig/loading.tsx new file mode 100644 index 000000000..0b08e2215 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/multisig/multisig.css b/frontend/src/app/(routes)/multisig/multisig.css index c49d87f83..5e7a6caaf 100644 --- a/frontend/src/app/(routes)/multisig/multisig.css +++ b/frontend/src/app/(routes)/multisig/multisig.css @@ -1,160 +1,100 @@ -.create-multisig-btn { - @apply rounded-lg px-3 py-[6px] text-[12px] font-medium leading-[20px] tracking-[0.48px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); -} - -.verify-btn { - @apply min-w-[220px] min-h-[40px]; -} - -.multisig-account-card { - @apply p-4 rounded-2xl bg-[#0E0B26] cursor-pointer hover:bg-[#FFFFFF0D] truncate; - backdrop-filter: blur(2px); -} - -.multisig-info-title { - @apply relative rounded-t-2xl flex px-6 py-4 h-[78px] justify-between items-center; - background: linear-gradient(90deg, #704290 0.19%, #241b61 79.25%); +.multisig-card { + @apply px-4 py-6 rounded-2xl space-y-6 cursor-pointer; + background: linear-gradient( + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% + ); } -.account-info-item { - @apply text-[14px] space-y-2 justify-center p-2 rounded-2xl bg-[#FFFFFF0D] overflow-hidden; +.actions-required-badge { + @apply rounded-full text-[12px] font-extralight h-6 flex items-center px-3 border-[1px] border-[#F15757] bg-[#F1575780] leading-[18px]; } -.account-members { - @apply flex-1 p-2 pb-4 flex flex-col gap-2 rounded-2xl bg-[#FFFFFF0D]; +.txn-card { + @apply rounded-2xl p-6 flex justify-between relative items-center; + background: linear-gradient( + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% + ); } -.member-address { - @apply text-[12px] font-medium h-[36px] flex gap-[10px] justify-center items-center p-2 rounded-lg; +.stats-card { + @apply flex-1 rounded-2xl p-4 flex flex-col justify-center items-center gap-2 h-[92px]; background: linear-gradient( - 180deg, - rgba(74, 162, 156, 0.1) 0%, - rgba(139, 61, 167, 0.1) 100% + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% ); } .members-list { - @apply grid grid-cols-3 gap-6 px-2; -} - -.delete-multisig-btn { - @apply px-10 py-[10px] rounded-2xl font-medium leading-[20px] tracking-[0.64px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); -} - -.multisig-sidebar { - @apply space-y-6 p-6 w-[500px] bg-[#0E0B26] h-screen min-h-[800px] overflow-y-scroll; -} - -.multisig-account-info { - @apply flex-1 flex flex-col pl-10 py-6 text-white space-y-6 h-screen min-h-[800px] overflow-y-scroll; -} - -.multisig-account-address { - @apply max-w-fit leading-[20px]; -} - -.custom-radio-button-label { - @apply flex items-center cursor-pointer gap-2; -} - -.custom-radio-button { - @apply border-2 w-4 h-4 border-[#FFFFFF80] rounded-full flex justify-center items-center; -} - -.custom-radio-button-checked { - @apply h-[6px] w-[6px] bg-white rounded-full; -} - -.verify-account { - @apply flex-1 h-screen flex flex-col justify-center items-center; -} - -.create-txn-btn { - @apply rounded-lg font-medium leading-[20px] tracking-[0.64px] px-3 py-[6px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); -} - -.line { - @apply h-[1px] mt-4 bg-[#ffffff35] opacity-50; -} - -.action-image { - @apply rounded-lg w-8 h-8 cursor-pointer justify-center items-center flex; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); -} - -.sign-broadcast-btn { - @apply min-w-[87px] text-[12px] font-medium leading-[20px] px-3 py-[6px] rounded-lg cursor-pointer justify-center flex; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); -} - -.raw-content { - @apply p-2 bg-black max-h-[600px] overflow-y-scroll; -} - -.account-address { - @apply w-fit flex gap-2 items-center p-2 text-white text-[14px] font-normal leading-normal rounded-lg opacity-80 bg-[#FFFFFF1A] h-[36px]; -} - -.txn-card { - @apply bg-[#FFFFFF0D] p-4 rounded-2xl space-y-6; - backdrop-filter: blur(2px); + @apply rounded-2xl p-6 space-y-2; + background: linear-gradient( + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% + ); } -.create-account-btn { - @apply rounded-2xl px-10 py-[10px] text-[16px] font-medium leading-[20px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.txns-filter { + @apply !h-8 flex justify-center items-center text-[14px] px-4 py-[10.5px] rounded-lg; } -.fee-selected { - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.txns-filter-unselected { + @apply border-[0.25px] bg-transparent border-[#FFFFFF10] hover:bg-[#ffffff08] hover:border-transparent; } -.fee-component-btn { - @apply rounded-2xl p-4 space-y-4 bg-[#FFFFFF0D] cursor-pointer hover:bg-[#ffffff17]; +.txns-filter-selected { + @apply bg-[#FFFFFF14] border-[0.25px] border-transparent; } -.create-txn-form-btn { - @apply px-10 py-[10px] rounded-2xl; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.more-options { + @apply bg-[#FFFFFF14] rounded-2xl flex flex-col absolute right-[24px] top-12 overflow-hidden w-[180px] z-[999]; + backdrop-filter: blur(15px); } -.cancel-button { - @apply underline underline-offset-[3px] font-medium; +.delete-multisig-btn { + border-image: none !important; + background: #d921011a !important; } -.circular-progress-custom { - color: purple; +.threshold { + @apply text-[14px] rounded-full !h-8 border-[#ffffff10] border-[0.25px] px-4 py-[10.5px] w-[125px] flex justify-center items-center gap-2; } -.address-pubkey-field-error { - @apply text-[14px] text-[#E57575]; +.address-pubkey-field-error, +.address-error { + @apply h-[18px] text-[#D92101] text-right text-[12px] font-light; } -.btn-disabled { - @apply text-[#FFFFFF1A] cursor-not-allowed; - background: #ffffff1a; +.add-members { + @apply space-y-2; } -.file-upload-box { - @apply flex justify-center items-center px-4 py-10 bg-[#FFFFFF1A] min-h-[304px] rounded-3xl w-full cursor-pointer; +/* .add-members { + @apply max-h-[180px] overflow-y-scroll; } -.error-box { - @apply flex justify-end mt-2 h-[26px]; +.add-members::-webkit-scrollbar { + width: 5px; } -.error-chip { - @apply text-[12px] rounded-lg bg-[#ff00005b] text-white text-center leading-normal max-w-fit py-1 px-2 truncate; +.add-members::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + -webkit-border-radius: 10px; + border-radius: 10px; } -.create-multisig-btn-2 { - @apply rounded-2xl px-10 py-[10px] font-medium text-[16px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); -} +.add-members::-webkit-scrollbar-thumb { + -webkit-border-radius: 10px; + border-radius: 10px; + background: #ffffff40; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); +} */ -.empty-screen-text { - @apply text-[16px] my-6 leading-normal italic font-extralight text-center opacity-50; +.selected-tab { + background: linear-gradient(90deg, #4453df 0%, #7f5ced 100%); + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); } diff --git a/frontend/src/app/(routes)/multisig/styles.ts b/frontend/src/app/(routes)/multisig/styles.ts index c0dd9c331..e4ca02e05 100644 --- a/frontend/src/app/(routes)/multisig/styles.ts +++ b/frontend/src/app/(routes)/multisig/styles.ts @@ -78,25 +78,27 @@ export const sendTxnTextFieldStyles = { }; export const createMultisigTextFieldStyles = { - mt: 3, - '& .MuiTypography-body1': { + '& .MuiInputBase-input': { color: 'white', - fontSize: '12px', + fontSize: '14px', fontWeight: 200, + fontFamily: 'Libre Franklin', + lineHeight: '19px', }, '& .MuiOutlinedInput-notchedOutline': { border: 'none', }, - '& .Mui-disabled': { - WebkitTextFillColor: '#ffffff6b !important', - }, '& .MuiOutlinedInput-root': { - border: '1px solid transparent', - borderRadius: '16px', + border: '0.25px solid #ffffff10', + borderRadius: '100px', + height: '40px', }, '& .Mui-focused': { - border: '1px solid #ffffff4a', - borderRadius: '16px', + border: '0.25px solid #ffffff4a', + borderRadius: '100px', + }, + '& .Mui-disabled': { + WebkitTextFillColor: '#ffffff !important', }, }; diff --git a/frontend/src/app/(routes)/multisig/utils/multisigSigning.ts b/frontend/src/app/(routes)/multisig/utils/multisigSigning.ts new file mode 100644 index 000000000..9c7d45b4a --- /dev/null +++ b/frontend/src/app/(routes)/multisig/utils/multisigSigning.ts @@ -0,0 +1,172 @@ +import { getWalletAmino } from '@/txns/execute'; +import { MultisigAddressPubkey, Pubkey, Txn } from '@/types/multisig'; +import { makeMultisignedTx, SigningStargateClient } from '@cosmjs/stargate'; +import { toBase64 } from '@cosmjs/encoding'; +import { getAuthToken } from '@/utils/localStorage'; +import { COSMOS_CHAIN_ID } from '@/utils/constants'; +import { + addChainIDParam, + cleanURL, + isNetworkError, + NewMultisigThresholdPubkey, +} from '@/utils/util'; +import { fromBase64 } from '@cosmjs/encoding'; +import axios from 'axios'; +import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import { parseTxResult } from '@/utils/signing'; +import { NETWORK_ERROR } from '@/utils/errors'; +import multisigService from '@/store/features/multisig/multisigService'; + +declare let window: WalletWindow; + +const signTransaction = async ( + chainID: string, + multisigAddress: string, + unSignedTxn: Txn, + walletAddress: string, + rpcURLs: string[] +) => { + try { + window.wallet.defaultOptions = { + sign: { + preferNoSetMemo: true, + preferNoSetFee: true, + disableBalanceCheck: true, + }, + }; + const client = await multisigService.getStargateClient(rpcURLs); + + const result = await getWalletAmino(chainID); + const wallet = result[0]; + const signingClient = await SigningStargateClient.offline(wallet); + + const multisigAcc = await client.getAccount(multisigAddress); + if (!multisigAcc) { + throw new Error('Multisig account does not exist on chain'); + } + + const signerData = { + accountNumber: multisigAcc?.accountNumber, + sequence: multisigAcc?.sequence, + chainId: chainID, + }; + + const msgs = unSignedTxn?.messages || []; + + const { signatures } = await signingClient.sign( + walletAddress, + msgs, + unSignedTxn?.fee || { amount: [], gas: '' }, + unSignedTxn?.memo || '', + signerData + ); + + const payload = { + signer: walletAddress, + txId: unSignedTxn.id || NaN, + address: multisigAddress, + signature: toBase64(signatures[0]), + }; + + return payload; + } catch (error) { + throw error; + } +}; + +export async function broadcastTransaction(data: { + chainID: string; + multisigAddress: string; + signedTxn: Txn; + walletAddress: string; + pubKeys: MultisigAddressPubkey[]; + threshold: number; + baseURLs: string[]; + rpcURLs: string[]; +}) { + const authToken = getAuthToken(COSMOS_CHAIN_ID); + const queryParams = { + address: data.walletAddress, + signature: authToken?.signature || '', + }; + + try { + const client = await multisigService.getStargateClient(data.rpcURLs); + const multisigAcc = await client.getAccount(data.multisigAddress); + if (!multisigAcc) { + throw new Error('Multisig account does not exist on chain'); + } + + const mapData = data.pubKeys || []; + let pubkeys_list: Pubkey[] = []; + + pubkeys_list = mapData.map((p) => { + const parsed = p?.pubkey; + const obj = { + type: parsed?.type, + value: parsed?.value, + }; + return obj; + }); + + const multisigThresholdPK = NewMultisigThresholdPubkey( + pubkeys_list, + `${data.threshold}` + ); + + const txBody = { + typeUrl: '/cosmos.tx.v1beta1.TxBody', + value: { + messages: data.signedTxn.messages, + memo: data.signedTxn.memo, + }, + }; + + const walletAmino = await getWalletAmino(data.chainID); + const offlineClient = await SigningStargateClient.offline(walletAmino[0]); + const txBodyBytes = offlineClient.registry.encode(txBody); + + const signedTx = makeMultisignedTx( + multisigThresholdPK, + multisigAcc.sequence, + data.signedTxn?.fee, + txBodyBytes, + new Map( + data.signedTxn?.signatures.map((s) => [ + s.address, + fromBase64(s.signature), + ]) + ) + ); + + const result = await client.broadcastTx( + Uint8Array.from(TxRaw.encode(signedTx).finish()) + ); + let txnUrl = `${cleanURL(data.baseURLs[0])}/cosmos/tx/v1beta1/txs/${result.transactionHash}`; + txnUrl = addChainIDParam(txnUrl, data.chainID); + const txn = await axios.get(txnUrl); + + const { + code, + transactionHash, + fee = [], + memo = '', + rawLog = '', + } = parseTxResult(txn?.data?.tx_response); + + return { result, code, transactionHash, fee, memo, rawLog, queryParams }; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + let errMsg = + error?.message || 'Error while signing the transaction, Try again.'; + if (isNetworkError(errMsg)) { + errMsg = `${NETWORK_ERROR}: ${errMsg}`; + } + throw new Error(errMsg); + } +} + +export default { + signTransaction, + broadcastTransaction, +}; diff --git a/frontend/src/app/(routes)/settings/(general)/[network]/error.tsx b/frontend/src/app/(routes)/settings/(general)/[network]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/[network]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/settings/(general)/[network]/loading.tsx b/frontend/src/app/(routes)/settings/(general)/[network]/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/[network]/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/settings/(general)/[network]/page.tsx b/frontend/src/app/(routes)/settings/(general)/[network]/page.tsx new file mode 100644 index 000000000..7eb68b810 --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/[network]/page.tsx @@ -0,0 +1,51 @@ +'use client'; + +import React from 'react'; +import '../../settings.css'; +import PageHeader from '@/components/common/PageHeader'; +import { GENERAL_SETTINGS_DESCRIPTION } from '@/utils/constants'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setChangeNetworkDialogOpen } from '@/store/features/common/commonSlice'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; + +const Page = () => { + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const openChangeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: true })); + }; + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + + return ( +
    + +
    +
    + {isWalletConnected ? ( + + ) : ( + + )} +
    +
    +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/settings/(general)/components/CustomNetworkCard.tsx b/frontend/src/app/(routes)/settings/(general)/components/CustomNetworkCard.tsx new file mode 100644 index 000000000..d1c7819e2 --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/components/CustomNetworkCard.tsx @@ -0,0 +1,75 @@ +import DialogConfirmDeleteNetwork from '@/components/select-network/DialogConfirmDeleteNetwork'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import { establishWalletConnection } from '@/store/features/wallet/walletSlice'; +import { networks } from '@/utils/chainsInfo'; +import { getLocalNetworks, removeLocalNetwork } from '@/utils/localStorage'; +import { shortenName } from '@/utils/util'; +import Image from 'next/image'; +import React, { useState } from 'react'; + +const CustomNetworkCard = ({ + chainID, + chainLogo, + chainName, +}: { + chainID: string; + chainName: string; + chainLogo: string; +}) => { + const dispatch = useAppDispatch(); + const [removeNetworkDialogOpen, setRemoveNetworkDialogOpen] = useState(false); + + const handleRemoveNetwork = async () => { + await removeLocalNetwork(chainID); + dispatch( + establishWalletConnection({ + walletName: 'keplr', + networks: [...networks, ...getLocalNetworks()], + }) + ); + setRemoveNetworkDialogOpen(false); + dispatch(setError({ type: 'success', message: 'Network Removed' })); + }; + + return ( +
    +
    +
    + {`${chainName} +

    {chainName}

    +
    + + +
    +
    +
    +

    Network

    +

    {shortenName(chainName, 15)}

    +
    +
    +

    Chain ID

    +

    {chainID}

    +
    +
    + setRemoveNetworkDialogOpen(false)} + onConfirm={handleRemoveNetwork} + /> +
    + ); +}; + +export default CustomNetworkCard; diff --git a/frontend/src/app/(routes)/settings/(general)/components/CustomNetworks.tsx b/frontend/src/app/(routes)/settings/(general)/components/CustomNetworks.tsx new file mode 100644 index 000000000..2365fefc3 --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/components/CustomNetworks.tsx @@ -0,0 +1,51 @@ +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import React from 'react'; +import CustomNetworkCard from './CustomNetworkCard'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { setAddNetworkDialogOpen } from '@/store/features/common/commonSlice'; +import { NO_DATA_ILLUSTRATION } from '@/constants/image-names'; + +const CustomNetworks = () => { + const dispatch = useAppDispatch(); + const { getCustomNetworks, getChainInfo } = useGetChainInfo(); + const customNetworks = getCustomNetworks(); + + return ( +
    +
    + {customNetworks.map((chainID) => { + const { chainLogo, chainName } = getChainInfo(chainID); + return ( + + ); + })} +
    +
    + {!customNetworks?.length && ( +
    + { + dispatch(setAddNetworkDialogOpen(true)); + }} + hasActionBtn + /> +
    + )} +
    +
    + ); +}; + +export default CustomNetworks; diff --git a/frontend/src/app/(routes)/settings/(general)/components/MsgItem.tsx b/frontend/src/app/(routes)/settings/(general)/components/MsgItem.tsx new file mode 100644 index 000000000..2f750ddb1 --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/components/MsgItem.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const MsgItem = ({ + msg, + onSelect, + selected, +}: { + msg: string; + onSelect: (chainID: string) => void; + selected: boolean; +}) => { + return ( + + ); +}; + +export default MsgItem; diff --git a/frontend/src/app/(routes)/settings/(general)/components/SelectedChains.tsx b/frontend/src/app/(routes)/settings/(general)/components/SelectedChains.tsx new file mode 100644 index 000000000..d54ec1bd9 --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/components/SelectedChains.tsx @@ -0,0 +1,31 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import Image from 'next/image'; +import React from 'react'; + +const SelectedChains = ({ selectedChains }: { selectedChains: string[] }) => { + const { getChainInfo } = useGetChainInfo(); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + return ( +
    +
    + Networks Selected +
    +
    + {selectedChains.map((chainName) => { + const chainID = nameToChainIDs?.[chainName.toLowerCase()]; + const { chainLogo } = getChainInfo(chainID); + return ; + })} +
    +
    + ); +}; + +export default SelectedChains; + +const NetworkLogo = ({ logo }: { logo: string }) => { + return ( + + ); +}; diff --git a/frontend/src/app/(routes)/settings/(general)/error.tsx b/frontend/src/app/(routes)/settings/(general)/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/settings/(general)/loading.tsx b/frontend/src/app/(routes)/settings/(general)/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/settings/(general)/page.tsx b/frontend/src/app/(routes)/settings/(general)/page.tsx new file mode 100644 index 000000000..de09aa1a8 --- /dev/null +++ b/frontend/src/app/(routes)/settings/(general)/page.tsx @@ -0,0 +1,32 @@ +'use client'; + +import React from 'react'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import './../settings.css'; +import SettingsLayout from '../SettingsLayout'; +import { setAddNetworkDialogOpen } from '@/store/features/common/commonSlice'; +import CustomNetworks from './components/CustomNetworks'; + +const Page = () => { + const dispatch = useAppDispatch(); + + const addNetwork = () => { + dispatch(setAddNetworkDialogOpen(true)); + }; + + return ( + +
    + + {/* TODO: Implement address book functionality and integrate at all address fields */} + {/* TODO: Empty screen when no custom networks or no addresses */} +
    +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/settings/SettingsLayout.tsx b/frontend/src/app/(routes)/settings/SettingsLayout.tsx new file mode 100644 index 000000000..1edf9711c --- /dev/null +++ b/frontend/src/app/(routes)/settings/SettingsLayout.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import SettingsHeader from './components/SettingsHeader'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import EmptyScreen from '@/components/common/EmptyScreen'; + +interface SetttingLayoutProps { + action: () => void; + actionName: string; + tabName: string; + children: React.ReactNode; +} + +const SettingsLayout = (props: SetttingLayoutProps) => { + const { action, actionName, tabName, children } = props; + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const connectWallet = () => { + dispatch(setConnectWalletOpen(true)); + }; + + return ( +
    + + {isWalletConnected ? ( +
    {children}
    + ) : ( +
    + +
    + )} +
    + ); +}; + +export default SettingsLayout; diff --git a/frontend/src/app/(routes)/settings/authz/AuthzPage.tsx b/frontend/src/app/(routes)/settings/authz/AuthzPage.tsx new file mode 100644 index 000000000..13712fb5c --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/AuthzPage.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import AuthzFilters from './components/AuthzFilters'; +import useInitAuthz from '@/custom-hooks/useInitAuthz'; + +const AuthzPage = ({ chainIDs }: { chainIDs: string[] }) => { + useInitAuthz({ chainIDs, shouldFetch: true }); + + return
    + +
    ; +}; + +export default AuthzPage; diff --git a/frontend/src/app/(routes)/settings/authz/[network]/error.tsx b/frontend/src/app/(routes)/settings/authz/[network]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/[network]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/settings/authz/[network]/loading.tsx b/frontend/src/app/(routes)/settings/authz/[network]/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/[network]/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/settings/authz/[network]/page.tsx b/frontend/src/app/(routes)/settings/authz/[network]/page.tsx new file mode 100644 index 000000000..a4d441781 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/[network]/page.tsx @@ -0,0 +1,46 @@ +'use client'; + +import React from 'react'; +import '../../settings.css'; +import SettingsLayout from '../../SettingsLayout'; +import { useParams, useRouter } from 'next/navigation'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import AuthzPage from '../AuthzPage'; + +const Page = () => { + const params = useParams(); + const paramChains = params.network; + const chainNames = + typeof paramChains === 'string' ? [paramChains.toLowerCase()] : paramChains; + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const chainIDs: string[] = []; + Object.keys(nameToChainIDs).forEach((chain) => { + chainNames.forEach((paramChain) => { + if (chain === paramChain.toLowerCase()) + chainIDs.push(nameToChainIDs[chain]); + }); + }); + + const router = useRouter(); + const createNewAuthz = () => { + router.push('/settings/authz/new-authz'); + }; + + return ( + + {chainIDs.length ? ( + + ) : ( +
    + - Chain Not found - +
    + )} +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/settings/authz/components/AuthzFilters.tsx b/frontend/src/app/(routes)/settings/authz/components/AuthzFilters.tsx new file mode 100644 index 000000000..d2978d109 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/components/AuthzFilters.tsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import GrantedByMe from './GrantedByMe'; +import GrantedToMe from './GrantedToMe'; + + +const FeegrantFilters = ({ chainIDs }: { chainIDs: string[] }) => { + const [filter, setFilter] = useState('toMe'); + + const handleFiltersChange = (newFilter: string) => { + setFilter(newFilter); + }; + + return ( +
    +
    + + +
    + {filter === 'byMe' ? ( +
    + +
    + ) : null} + {filter === 'toMe' ? ( +
    + +
    + ) : null} +
    + ); +}; + +export default FeegrantFilters; diff --git a/frontend/src/app/(routes)/settings/authz/components/DialogAuthzDetails.tsx b/frontend/src/app/(routes)/settings/authz/components/DialogAuthzDetails.tsx new file mode 100644 index 000000000..e613df403 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/components/DialogAuthzDetails.tsx @@ -0,0 +1,250 @@ +import CustomDialog from '@/components/common/CustomDialog'; +import React, { useEffect } from 'react'; +import Image from 'next/image'; +import { getMsgNameFromAuthz, getTypeURLFromAuthorization } from '@/utils/authorizations'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetAuthzRevokeMsgs from '@/custom-hooks/useGetAuthzRevokeMsgs'; +import { RootState } from '@/store/store'; +import { TxStatus } from '@/types/enums'; +import { txAuthzRevoke } from '@/store/features/authz/authzSlice'; +import { get } from 'lodash'; +import { shortenAddress } from '@/utils/util'; +import Copy from '@/components/common/Copy'; + +const DialogViewDetails = ({ + onClose, + open, + AddressGrants, + chainID, + address, + revoke, +}: { + open: boolean; + onClose: () => void; + AddressGrants: Authorization[]; + chainID: string; + address: string; + revoke: boolean; +}) => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { chainLogo } = getChainInfo(chainID); + + const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); + + const getParseAmounts = (amount: Coin[]) => { + let total = 0; + amount && + amount.map((c) => { + if (c?.denom === minimalDenom) total += Number(c.amount); + }); + + return total / 10 ** decimals; + }; + + const getParseAmount = (amount?: Coin | null) => { + return amount?.amount ? Number(amount?.amount) / 10 ** decimals : 0; + }; + + const dispatch = useAppDispatch(); + + let allTypeUrls: string[] = [] + + let granter = '', grantee = ''; + + AddressGrants.forEach(a => { + allTypeUrls = [...allTypeUrls, getTypeURLFromAuthorization(a)] + grantee = a.grantee + granter = a.granter + }) + + const { txRevokeAuthzInputs } = useGetAuthzRevokeMsgs({ + granter: granter, + grantee: grantee, + chainID, + typeURLs: allTypeUrls, + }); + + const { basicChainInfo, denom, feeAmount, feegranter, msgs } = + txRevokeAuthzInputs; + + const loading = useAppSelector( + (state: RootState) => state.authz.chains?.[chainID].tx.status + ); + + useEffect(() => { + if (loading === TxStatus.IDLE) { + onClose(); + } + }, [loading]); + + const handleDelete = () => { + dispatch( + txAuthzRevoke({ + basicChainInfo, + denom, + feeAmount, + feegranter, + msgs, + }) + ); + + }; + + + return ( + +
    +
    + {AddressGrants?.map((g, index) => { + //TODO: check is the grant is stake authorization or not + const isStakeAuthz = false; + return ( +
    +
    + network-logo + {getMsgNameFromAuthz(g)} +
    + {(g?.authorization?.['@type'] === + '/cosmos.bank.v1beta1.SendAuthorization' && ( + <> +
    +

    Spend Limit

    +

    + {getParseAmounts(g?.authorization?.spend_limit)}{' '} + {displayDenom} +

    +
    + + )) || + (g?.authorization?.['@type'] === + '/cosmos.staking.v1beta1.StakeAuthorization' && ( + <> +
    +

    Max Tokens

    +

    + {getParseAmount(g?.authorization?.max_tokens)}{' '} + {displayDenom} +

    +
    + { + (get(g, 'authorization.allow_list.address')) && +
    +

    Allow Addresses

    +
    + { + get(g, 'authorization.allow_list.address', []).map((a, ai) => ( +

    {shortenAddress(a, 20)}


    + )) + } +
    + + +
    || null + } + + { + (get(g, 'authorization.deny_list.address')) && +
    +

    Validator List (Deny)

    +
    + { + get(g, 'authorization.deny_list.address', []).map((a, ai) => ( +

    {shortenAddress(a, 20)}


    + )) + } +
    + + +
    || null + } + + + ))} + +
    +

    Expires in

    +

    + {getTimeDifferenceToFutureDate(g?.expiration || '')} +

    +
    +
    + ); + })} + + {/*
    +
    + network-logo +

    Grant Authz

    +
    +
    +

    Expiry

    +

    23rd March 2024, 11:23 pm

    +
    +
    */} +
    + {/*
    +
    + network-logo +

    Send

    +
    +
    +
    +
    +

    Spend Limit

    +

    120 AKT

    +
    +
    +

    Expiry

    +

    23rd March 2024, 11:23 pm

    +
    +
    +
    +

    Validator List (Deny)

    +
    + {['Vitwit', 'Stakefish', 'Polkachu'].map((validator, index) => ( +
    + validator-logo +

    {validator}

    +
    + ))} +
    +
    +
    +
    */} + + { + revoke && (loading === TxStatus.PENDING ? + : + ) || null + } +
    +
    + ); +}; + +export default DialogViewDetails; \ No newline at end of file diff --git a/frontend/src/app/(routes)/settings/authz/components/GrantByMeLoading.tsx b/frontend/src/app/(routes)/settings/authz/components/GrantByMeLoading.tsx new file mode 100644 index 000000000..083dbebd7 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/components/GrantByMeLoading.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +const GrantByMeLoading = () => { + return ( +
    + {[1, 2].map((_, index) => ( +
    +
    +
    +

    +

    +
    +
    +

    +

    +
    +
    +
    +

    Permissions

    +
    +

    +

    +

    +

    +
    +
    +
    +
    +
    +
    +
    + ))} +
    + ); +}; + +export default GrantByMeLoading; diff --git a/frontend/src/app/(routes)/settings/authz/components/GrantToMeLoading.tsx b/frontend/src/app/(routes)/settings/authz/components/GrantToMeLoading.tsx new file mode 100644 index 000000000..e8035a562 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/components/GrantToMeLoading.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const GrantToMeLoading = () => { + return ( +
    + {[1, 2].map((_, index) => ( +
    +
    +
    Address
    +
    +

    +

    +
    +
    +
    +

    Permissions

    +
    +

    +

    +

    +

    +
    +
    +
    +
    +
    +
    +
    + ))} +
    + ); +}; + +export default GrantToMeLoading; diff --git a/frontend/src/app/(routes)/settings/authz/components/GrantedByMe.tsx b/frontend/src/app/(routes)/settings/authz/components/GrantedByMe.tsx new file mode 100644 index 000000000..2a98eba9a --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/components/GrantedByMe.tsx @@ -0,0 +1,280 @@ +import React, { useEffect, useState } from 'react'; +import Image from 'next/image'; +import Copy from '@/components/common/Copy'; + +import DialogRevokeAll from '../../components/DialogRevokeAll'; +import DialogConfirmDelete from '@/components/common/DialogConfirmDelete'; +import DialogAuthzDetails from './DialogAuthzDetails'; +import useAuthzGrants from '@/custom-hooks/useAuthzGrants'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { + getMsgNameFromAuthz, + getTypeURLFromAuthorization, +} from '@/utils/authorizations'; +import GrantByMeLoading from './GrantByMeLoading'; +import useGetAuthzRevokeMsgs from '@/custom-hooks/useGetAuthzRevokeMsgs'; +import { txAuthzRevoke } from '@/store/features/authz/authzSlice'; +import { TxStatus } from '@/types/enums'; +import { RootState } from '@/store/store'; +import { groupBy } from 'lodash'; +import { shortenAddress } from '@/utils/util'; +import WithConnectionIllustration from '@/components/illustrations/withConnectionIllustration'; + +const GrantedByMe = ({ chainIDs }: { chainIDs: string[] }) => { + const [revokeDialogOpen, setRevokeDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [selectedPermission, setSelectedPermission] = useState( + null + ); + + const { convertToCosmosAddress } = useGetChainInfo(); + const { getGrantsByMe } = useAuthzGrants(); + + const authzGrants = getGrantsByMe(chainIDs); + /* eslint-disable @typescript-eslint/no-explicit-any */ + let grantsList: any[] = []; + authzGrants.forEach((grant) => { + const data = { + ...grant, + cosmosAddress: convertToCosmosAddress(grant.address), + }; + grantsList = [...grantsList, data]; + }); + const groupedGrants = groupBy(grantsList, 'cosmosAddress'); + + const loading = useAppSelector((state) => state.authz.getGrantsByMeLoading); + + const handleCloseRevokeDialog = () => { + setRevokeDialogOpen(false); + }; + + const handleCloseDeleteDialog = () => { + setDeleteDialogOpen(false); + setSelectedPermission(null); + }; + + const handleDelete = () => { + console.log('Delete permission:', selectedPermission); + setDeleteDialogOpen(false); + }; + + return ( +
    + {Object.entries(groupedGrants).map(([granterKey, grants]) => ( +
    + {grants.map((g, index) => ( + + ))} +
    + ))} + {!!loading ? ( + + ) : ( + <> + {!authzGrants?.length && ( + <> + + + )} + + )} + + + + +
    + ); +}; + +interface SelectPermission { + granter: string; + grantee: string; + typeUrl: string; +} + +const AuthzGrant: React.FC = ({ chainID, address, grants }) => { + const [dialogOpen, setDialogOpen] = useState(false); + const [isRevoke, setIsRevoke] = useState(false); + + const { getChainInfo } = useGetChainInfo(); + const { chainLogo, chainName } = getChainInfo(chainID); + + const handleCloseDialog = () => { + setDialogOpen(false); + setIsRevoke(false); + }; + + const handleRevokeAll = () => { + setIsRevoke(true); + setDialogOpen(true); + }; + + const handleViewDetails = () => { + setDialogOpen(true); + }; + + return ( + <> + +
    +
    +
    +
    +

    Grantee

    +
    +
    +

    {shortenAddress(address, 30)}

    + +
    +
    +
    + network-logo +

    + {chainName} +

    +
    +
    +
    +

    Permissions

    +
    + {grants.map((g, idx) => ( + + ))} +
    +
    +
    +
    +
    + +
    + View Details +
    +
    +
    +
    + + ); +}; + +const GrantCard = ({ + idx, + g, + chainID, +}: { + idx: number; + g: Authorization; + chainID: string; +}) => { + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [selectedPermission, setSelectedPermission] = useState(); + const [selectPermission, setSelectPermission] = + useState(null); + + const handleDeletePermission = (permission: string) => { + setSelectedPermission(permission); + setDeleteDialogOpen(true); + }; + + const handleCloseDeleteDialog = () => { + setDeleteDialogOpen(false); + }; + + const { txRevokeAuthzInputs } = useGetAuthzRevokeMsgs({ + granter: selectPermission?.granter || '', + grantee: selectPermission?.grantee || '', + chainID, + typeURLs: [selectPermission?.typeUrl || ''], + }); + + const dispatch = useAppDispatch(); + + const { basicChainInfo, denom, feeAmount, feegranter, msgs } = + txRevokeAuthzInputs; + + const loading = useAppSelector( + (state: RootState) => state.authz.chains?.[chainID].tx.status + ); + + useEffect(() => { + if (loading === TxStatus.IDLE) { + setDeleteDialogOpen(false); + } + }, [loading]); + + const handleDelete = () => { + dispatch( + txAuthzRevoke({ + basicChainInfo, + denom, + feeAmount, + feegranter, + msgs, + }) + ); + }; + + return ( +
    + + +
    +

    {getMsgNameFromAuthz(g)}

    + delete-icon { + handleDeletePermission(getMsgNameFromAuthz(g)); + setSelectPermission({ + granter: g.granter, + grantee: g.grantee, + typeUrl: getTypeURLFromAuthorization(g), + }); + }} + style={{ cursor: 'pointer' }} + /> +
    +
    + ); +}; + +export default GrantedByMe; diff --git a/frontend/src/app/(routes)/settings/authz/components/GrantedToMe.tsx b/frontend/src/app/(routes)/settings/authz/components/GrantedToMe.tsx new file mode 100644 index 000000000..a38adb511 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/components/GrantedToMe.tsx @@ -0,0 +1,188 @@ +import React, { useState } from 'react'; +import Image from 'next/image'; +import Copy from '@/components/common/Copy'; + +import { TICK_ICON } from '@/constants/image-names'; +import DialogAuthzDetails from './DialogAuthzDetails'; +import useAuthzGrants from '@/custom-hooks/useAuthzGrants'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getMsgNameFromAuthz } from '@/utils/authorizations'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import GrantToMeLoading from './GrantToMeLoading'; +import { setAuthzMode } from '@/utils/localStorage'; +import { RootState } from '@/store/store'; +import { groupBy } from 'lodash'; +import { enableAuthzMode } from '@/store/features/authz/authzSlice'; +import { exitFeegrantMode } from '@/store/features/feegrant/feegrantSlice'; +import { shortenAddress } from '@/utils/util'; +import WithConnectionIllustration from '@/components/illustrations/withConnectionIllustration'; + +const GrantedToMe = ({ chainIDs }: { chainIDs: string[] }) => { + const { getGrantsToMe } = useAuthzGrants(); + const { convertToCosmosAddress } = useGetChainInfo(); + + const authzGrants = getGrantsToMe(chainIDs); + /* eslint-disable @typescript-eslint/no-explicit-any */ + let grantsList: any[] = []; + authzGrants.forEach((grant) => { + const data = { + ...grant, + cosmosAddress: convertToCosmosAddress(grant.address), + }; + grantsList = [...grantsList, data]; + }); + const groupedGrants = groupBy(grantsList, 'cosmosAddress'); + + const loading = useAppSelector((state) => state.authz.getGrantsByMeLoading); + + return ( +
    + {Object.entries(groupedGrants).map(([granterKey, grants]) => ( +
    + {grants.map((grant, index) => ( + + ))} +
    + ))} + {!!loading ? ( + + ) : ( + <> + {!authzGrants?.length && ( + <> + + + )} + + )} +
    + ); +}; + +const GrantToMeCard = ({ + index, + grant, +}: { + index: number; + grant: AddressGrants; +}) => { + const dispatch = useAppDispatch(); + const authzData = useAppSelector((state: RootState) => state.authz); + const { authzModeEnabled, authzAddress: SelectedauthzGranterAddr } = + authzData; + const { disableAuthzMode } = useAuthzGrants(); + + const { getCosmosAddress, convertToCosmosAddress } = useGetChainInfo(); + + const granteeCosmosAddr = getCosmosAddress(); + const granterCosmosAddr = convertToCosmosAddress(grant.address); + + // setAuthzMode(granteeCosmosAddr, granterCosmosAddr); + + const [dialogOpen, setDialogOpen] = useState(false); + + const handleViewDetails = () => { + setDialogOpen(true); + }; + + const { getChainInfo } = useGetChainInfo(); + const { chainLogo } = getChainInfo(grant?.chainID); + + const handleCloseDialog = () => { + setDialogOpen(false); + }; + + const isGranterSelected = (cosmosAddr: string) => { + return SelectedauthzGranterAddr === cosmosAddr; + }; + + const handleUseAuthz = () => { + if (authzModeEnabled) { + disableAuthzMode(); + } else { + dispatch(enableAuthzMode({ address: granterCosmosAddr })); + dispatch(exitFeegrantMode()); + setAuthzMode(granteeCosmosAddr, granterCosmosAddr); + } + }; + + return ( +
    +
    +
    +

    Granter

    + {isGranterSelected(convertToCosmosAddress(grant?.address)) && ( +
    + used-icon + Currently Using +
    + )} +
    +
    +

    + {shortenAddress(grant.address, 30)} +

    + +
    +
    +
    +

    Permissions

    +
    + {grant?.grants?.map((g, idx) => ( +
    +

    {getMsgNameFromAuthz(g)}

    + network-logo +
    + ))} +
    +
    +
    +
    +
    + +
    + View Details +
    +
    +
    + + +
    + ); +}; + +export default GrantedToMe; diff --git a/frontend/src/app/(routes)/settings/authz/error.tsx b/frontend/src/app/(routes)/settings/authz/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/settings/authz/loading.tsx b/frontend/src/app/(routes)/settings/authz/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/components/CustomRadioGroup.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/components/CustomRadioGroup.tsx new file mode 100644 index 000000000..9137733ca --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/components/CustomRadioGroup.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +interface CustomRadioGroupProps { + isDenyList: boolean; + onSelect: (value: boolean) => void; +} + +const CustomRadioGroup: React.FC = (props) => { + const { isDenyList, onSelect } = props; + return ( +
    +
    onSelect(false)} + > + +
    + Allow List +
    +
    +
    onSelect(true)}> + +
    + Deny List +
    +
    +
    + ); +}; + +export default CustomRadioGroup; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/components/DialogAuthzTxStatus.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/components/DialogAuthzTxStatus.tsx new file mode 100644 index 000000000..ecba95e17 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/components/DialogAuthzTxStatus.tsx @@ -0,0 +1,152 @@ +import Copy from '@/components/common/Copy'; +import CustomDialog from '@/components/common/CustomDialog'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { + capitalizeFirstLetter, + cleanURL, + shortenAddress, + shortenName, +} from '@/utils/util'; +import { Tooltip } from '@mui/material'; +import Image from 'next/image'; +import Link from 'next/link'; +import React from 'react'; +import { REDIRECT_ICON } from '@/constants/image-names'; +import SelectedChains from '../../../(general)/components/SelectedChains'; + +const DialogAuthzTxStatus = ({ + onClose, + open, + chainsStatus, + selectedChains, + selectedMsgs, + granteeAddress, +}: { + open: boolean; + onClose: () => void; + selectedMsgs: string[]; + chainsStatus: Record; + selectedChains: string[]; + granteeAddress: string; +}) => { + const { getChainInfo } = useGetChainInfo(); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + + return ( + +
    +
    +
    + Grantee Address +
    +
    +
    {shortenAddress(granteeAddress, 20)}
    + +
    +
    + +
    + {selectedChains.map((chain) => { + const chainID = nameToChainIDs?.[chain.toLowerCase()] || ''; + const { chainLogo, chainName, explorerTxHashEndpoint } = + getChainInfo(chainID); + return ( +
    +
    +
    +
    + +
    + {capitalizeFirstLetter(chainName)} +
    +
    + +
    + {chainsStatus?.[chainID]?.txStatus === 'pending' ? ( +
    + Transaction Pending + +
    + ) : ( + <> + {chainsStatus?.[chainID]?.isTxSuccess ? ( +
    +
    +
    + {shortenName( + chainsStatus?.[chainID]?.txHash, + 18 + )} +
    + +
    + +
    Transaction Successful
    + + +
    + ) : ( + + + {chainsStatus?.[chainID]?.error === + 'Request rejected' + ? 'Wallet request rejected' + : 'Transaction Failed'} + + + )} + + )} +
    +
    +
    +
    +
    + {selectedMsgs.map((msg) => ( +
    + {msg} +
    + ))} +
    +
    + ); + })} +
    +
    +
    + ); +}; + +export default DialogAuthzTxStatus; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/components/ExpirationField.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/components/ExpirationField.tsx new file mode 100644 index 000000000..d1145ae08 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/components/ExpirationField.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Control, Controller } from 'react-hook-form'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import { TextField } from '@mui/material'; +import { expirationFieldStyles } from '../../../styles'; + +const ExpirationField = ({ + control, + msg, +}: { + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: Control; + msg: string; +}) => { + const date = new Date(); + const expiration = new Date(date.setTime(date.getTime() + 365 * 86400000)); + + return ( + ( + + ( + (event.target.readOnly = true)} + onBlur={(event) => (event.target.readOnly = false)} + /> + )} + value={value} + onChange={onChange} + /> + + )} + /> + ); +}; + +export default ExpirationField; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/components/GranteeAddress.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/components/GranteeAddress.tsx new file mode 100644 index 000000000..911651217 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/components/GranteeAddress.tsx @@ -0,0 +1,29 @@ +import { customTextFieldStyles } from '@/utils/commonStyles'; +import { TextField } from '@mui/material'; +import React from 'react'; + +const GranteeAddress = ({ + handleChange, + value, +}: { + value: string; + handleChange: HandleChangeEvent; +}) => { + return ( +
    +
    Grantee Address
    + +
    + ); +}; + +export default GranteeAddress; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/components/NewAuthzPage.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/components/NewAuthzPage.tsx new file mode 100644 index 000000000..1e5e8db4b --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/components/NewAuthzPage.tsx @@ -0,0 +1,463 @@ +import React, { ChangeEvent, useEffect, useState } from 'react'; +import SelectNetworks from '../../../components/NetworksList'; +import { fromBech32 } from '@cosmjs/encoding'; +import { convertToSnakeCase, shortenAddress } from '@/utils/util'; +import Copy from '@/components/common/Copy'; +import SelectedChains from '../../../(general)/components/SelectedChains'; +import { PERMISSION_NOT_SELECTED_ERROR } from '@/utils/errors'; +import useMultiTxTracker from '@/custom-hooks/useGetMultiChainTxLoading'; +import { CircularProgress } from '@mui/material'; +import GranteeAddress from './GranteeAddress'; +import { + authzMsgTypes, + GENRIC_GRANTS, + grantAuthzFormDefaultValues, + MAP_TXN_MSG_TYPES, + STAKE_GRANTS, +} from '@/utils/authorizations'; +import MsgItem from '../../../(general)/components/MsgItem'; +import { FieldValues, useForm } from 'react-hook-form'; +import ExpirationField from './ExpirationField'; +import SendAuthzForm from './SendAuthzForm'; +import StakeAuthzForm from './StakeAuthzForm'; +import useGetGrantAuthzMsgs from '@/custom-hooks/useGetGrantAuthzMsgs'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import useGetFeegranter from '@/custom-hooks/useGetFeegranter'; +import Image from 'next/image'; +import CustomButton from '@/components/common/CustomButton'; +import DialogAuthzTxStatus from './DialogAuthzTxStatus'; +import { NO_MESSAGES_ILLUSTRATION } from '@/constants/image-names'; +import { AUTHZ } from '@/utils/constants'; + +const NewAuthzPage = () => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { + trackTxs, + ChainsStatus: chainsStatus, + currentTxCount, + } = useMultiTxTracker(); + const { getFeegranter } = useGetFeegranter(); + const msgTypes = authzMsgTypes(); + + const [txStatusOpen, setTxStatusOpen] = useState(false); + const [selectedChains, setSelectedChains] = useState([]); + const [txnStarted, setTxnStarted] = useState(false); + const [granteeAddress, setGranteeAddress] = useState(''); + const [addressValidationError, setAddressValidationError] = useState(''); + const [formValidationError, setFormValidationError] = useState(''); + const [selectedMsgs, setSelectedMsgs] = useState([]); + /* + List of allow/deny validators for StakeAuthorization + */ + const [selectedDelegateValidators, setSelectedDelegateValidators] = useState< + string[] + >([]); + const [isDenyDelegateList, setIsDenyDelegateList] = useState(false); + const [selectedUndelegateValidators, setSelectedUndelegateValidators] = + useState([]); + const [isDenyUndelegateList, setIsDenyUndelegateList] = + useState(false); + const [selectedRedelegateValidators, setSelectedRedelegateValidators] = + useState([]); + const [isDenyRedelegateList, setIsDenyRedelegateList] = + useState(false); + const [sendAdvanced, setSendAdvanced] = useState(false); + const [delegateAdvanced, setDelegateAdvanced] = useState(false); + const [undelegateAdvanced, setUndelegateAdvanced] = useState(false); + const [redelegateAdvanced, setRedelegateAdvanced] = useState(false); + const [recentlyAdded, setRecentlyAdded] = useState(null); + + const { + handleSubmit, + control, + formState: { errors: formErrors }, + } = useForm({ + defaultValues: grantAuthzFormDefaultValues(), + }); + + const handleSelectChain = (chainName: string) => { + if (txnStarted) { + return; + } + const updatedSelection = selectedChains.includes(chainName) + ? selectedChains.filter((id) => id !== chainName) + : [...selectedChains, chainName]; + setSelectedChains(updatedSelection); + }; + + const handleAddressChange = (e: ChangeEvent) => { + setGranteeAddress(e.target.value); + validateAddress(e.target.value); + }; + + const handleSelectMsg = (msgType: string) => { + if (selectedMsgs.includes(msgType)) { + resetCustomData(msgType); + } + const updatedSelection = selectedMsgs.includes(msgType) + ? selectedMsgs.filter((id) => id !== msgType) + : [msgType, ...selectedMsgs]; + + setSelectedMsgs(updatedSelection); + setRecentlyAdded(0); + }; + + useEffect(() => { + if (recentlyAdded !== null) { + const timer = setTimeout(() => setRecentlyAdded(null), 1000); + return () => clearTimeout(timer); + } + }, [recentlyAdded]); + + const resetCustomData = (msg: string) => { + switch (msg) { + case 'Send': + setSendAdvanced(false); + break; + case 'Delegate': + setSelectedDelegateValidators([]); + setDelegateAdvanced(false); + setIsDenyDelegateList(false); + break; + case 'Undelegate': + setSelectedUndelegateValidators([]); + setUndelegateAdvanced(false); + setIsDenyUndelegateList(false); + break; + case 'Redelegate': + setSelectedRedelegateValidators([]); + setRedelegateAdvanced(false); + setIsDenyRedelegateList(false); + break; + default: + break; + } + }; + + const handleRemoveMsg = (index: number, msg: string) => { + resetCustomData(msg); + const arr = selectedMsgs.filter((_, i) => i !== index); + setSelectedMsgs(arr); + }; + + const validateAddress = (address: string) => { + if (!address.length) { + setAddressValidationError('Please enter grantee address'); + return false; + } + try { + fromBech32(address); + setAddressValidationError(''); + return true; + } catch (error) { + setAddressValidationError('Invalid grantee address'); + return false; + } + }; + + const validateForm = () => { + if (!selectedChains.length) { + setFormValidationError('Atleast one chain must be selected'); + return false; + } + if (!validateAddress(granteeAddress)) { + setFormValidationError(''); + return false; + } + if (!selectedMsgs.length) { + setFormValidationError(PERMISSION_NOT_SELECTED_ERROR); + return false; + } + setFormValidationError(''); + return true; + }; + + const { getGrantAuthzMsgs } = useGetGrantAuthzMsgs(); + + const onSubmit = (e: FieldValues) => { + if (!validateForm()) { + return; + } + const fieldValues = e; + fieldValues['delegate'] = { + expiration: fieldValues['delegate'].expiration, + max_tokens: fieldValues['delegate'].max_tokens, + isDenyList: isDenyDelegateList, + validators_list: selectedDelegateValidators, + }; + fieldValues['undelegate'] = { + expiration: fieldValues['undelegate'].expiration, + max_tokens: fieldValues['undelegate'].max_tokens, + isDenyList: isDenyUndelegateList, + validators_list: selectedUndelegateValidators, + }; + fieldValues['redelegate'] = { + expiration: fieldValues['redelegate'].expiration, + max_tokens: fieldValues['redelegate'].max_tokens, + isDenyList: isDenyRedelegateList, + validators_list: selectedRedelegateValidators, + }; + const msgsList: string[] = selectedMsgs.reduce((list: string[], msg) => { + list.push(convertToSnakeCase(msg)); + return list; + }, []); + const grantsList: Grant[] = []; + msgsList.forEach((msg) => { + grantsList.push({ msg: msg, ...fieldValues[msg] }); + }); + + /* + getGrantAuthzMsgs returns the all messages chainwise + */ + const { chainWiseGrants } = getGrantAuthzMsgs({ + grantsList, + selectedChains, + granteeAddress, + }); + const txCreateAuthzInputs: MultiChainTx[] = []; + chainWiseGrants.forEach((chain) => { + const chainID = chain.chainID; + const msgs = chain.msgs; + const basicChainInfo = getChainInfo(chainID); + const { minimalDenom, decimals } = getDenomInfo(chainID); + const { feeAmount: avgFeeAmount } = basicChainInfo; + const feeAmount = avgFeeAmount * 10 ** decimals; + + txCreateAuthzInputs.push({ + ChainID: chainID, + txInputs: { + basicChainInfo: basicChainInfo, + msgs: msgs, + denom: minimalDenom, + feeAmount: feeAmount, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['grant_authz']), + }, + }); + }); + setTxnStarted(true); + setTxStatusOpen(true); + trackTxs(txCreateAuthzInputs); + }; + + const renderForm = (msg: string) => { + const msgType = convertToSnakeCase(msg); + const sendGrant = 'send'; + if (GENRIC_GRANTS.includes(msgType)) { + return ( +
    +
    + Set Expiry +
    + +
    + ); + } else if (msgType === sendGrant) { + return ( + setSendAdvanced((prevState) => !prevState)} + /> + ); + } else if (STAKE_GRANTS.includes(msgType)) { + switch (msgType) { + case 'delegate': + return ( + setDelegateAdvanced((prevState) => !prevState)} + /> + ); + case 'undelegate': + return ( + setUndelegateAdvanced((prevState) => !prevState)} + /> + ); + case 'redelegate': + return ( + setRedelegateAdvanced((prevState) => !prevState)} + /> + ); + } + } + }; + + useEffect(() => { + if (currentTxCount === 0) { + setTxnStarted(false); + } + }, [currentTxCount]); + + return ( +
    +
    + + +
    +
    +
    + Select Permissions +
    +
    +
    + {msgTypes.map((msg, index) => ( + + ))} +
    +
    +
    +
    +
    + +
    +
    + Grantee Address +
    +
    +
    {shortenAddress(granteeAddress, 20)}
    + {granteeAddress ? : null} +
    +
    +
    onSubmit(e))} + id="create-authz-form" + > +
    Permissions
    + {selectedMsgs?.length ? ( +
    + {selectedMsgs.map((msg, index) => ( +
    +
    +
    +
    {msg}
    + +
    +
    +
    +
    {renderForm(msg)}
    +
    + ))} +
    +
    + {addressValidationError || formValidationError} +
    + {selectedMsgs?.length ? ( + + ) : null} +
    +
    + ) : ( +
    + No Messages +
    + Select a permissions from the left side to add here +
    +
    + )} +
    +
    + {selectedMsgs?.length ? null : ( + + )} +
    + { + setTxStatusOpen(false); + setTxnStarted(false); + }} + chainsStatus={chainsStatus} + selectedChains={selectedChains} + selectedMsgs={selectedMsgs} + granteeAddress={granteeAddress} + /> +
    + ); +}; + +export default NewAuthzPage; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/components/SendAuthzForm.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/components/SendAuthzForm.tsx new file mode 100644 index 000000000..4d066de43 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/components/SendAuthzForm.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import ExpirationField from './ExpirationField'; +import { Control, Controller } from 'react-hook-form'; +import { TextField } from '@mui/material'; +import { expirationFieldStyles } from '../../../styles'; + +const SendAuthzForm = ({ + control, + advanced, + toggle, + formError, +}: { + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: Control; + advanced: boolean; + toggle: () => void; + formError: string; +}) => { + return ( +
    +
    +
    +
    + Set Expiry +
    + +
    + {advanced && ( + <> + { + const amount = Number(value); + if (value?.length && (Number.isNaN(amount) || amount <= 0)) + return 'Invalid Amount'; + }, + }} + render={({ field }) => ( + + )} + /> +
    + + {formError || ''} + +
    + + )} +
    +
    + +
    +
    + ); +}; + +export default SendAuthzForm; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/components/StakeAuthzForm.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/components/StakeAuthzForm.tsx new file mode 100644 index 000000000..297f7ac23 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/components/StakeAuthzForm.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { Control, Controller } from 'react-hook-form'; +import ExpirationField from './ExpirationField'; +import { TextField } from '@mui/material'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { expirationFieldStyles } from '../../../styles'; +import CustomRadioGroup from './CustomRadioGroup'; +import ValidatorAutoComplete from './ValidatorAutoComplete'; + +interface StakeAuthzFormProps { + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: Control; + advanced: boolean; + toggle: () => void; + msg: string; + selectedChains: string[]; + setSelectedValidators: React.Dispatch>; + isDenyList: boolean; + setIsDenyList: React.Dispatch>; + maxTokensError: string; +} + +const StakeAuthzForm = ({ + control, + advanced, + toggle, + msg, + selectedChains, + setSelectedValidators, + isDenyList, + setIsDenyList, + maxTokensError, +}: StakeAuthzFormProps) => { + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const chainID = selectedChains.length + ? nameToChainIDs?.[selectedChains[0]] + : ''; + const onSelect = (value: boolean) => { + setIsDenyList(value); + }; + + const handleSelectValidators = (data: string[]) => { + setSelectedValidators(data); + }; + + return ( +
    +
    + + {selectedChains.length === 1 && advanced && ( + <> +
    + { + const amount = Number(value); + if (value?.length && (Number.isNaN(amount) || amount <= 0)) + return 'Invalid Amount'; + }, + }} + render={({ field }) => ( + + )} + /> +
    + + {maxTokensError || ''} + +
    +
    + + + + )} +
    + {selectedChains.length === 1 && ( +
    + +
    + )} +
    + ); +}; + +export default StakeAuthzForm; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/components/ValidatorAutoComplete.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/components/ValidatorAutoComplete.tsx new file mode 100644 index 000000000..cea98a189 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/components/ValidatorAutoComplete.tsx @@ -0,0 +1,228 @@ +import useStaking from '@/custom-hooks/txn-builder/useStaking'; +import React, { useState } from 'react'; +import ValidatorLogo from '@/app/(routes)/staking/components/ValidatorLogo'; +import { + customAutoCompleteStyles, + customTextFieldStyles, +} from '@/app/(routes)/transfers/styles'; +import NoOptions from '@/components/common/NoOptions'; +import { shortenName } from '@/utils/util'; +import { + Autocomplete, + CircularProgress, + Paper, + TextField, +} from '@mui/material'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { TxStatus } from '@/types/enums'; +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import { REMOVE_ICON_OUTLINED } from '@/constants/image-names'; +import Image from 'next/image'; + +const listItemStyle = css` + &:hover { + background-color: #ffffff09 !important; + } +`; + +const ValidatorAutoComplete = ({ + chainID, + handleSelectValidators, +}: { + chainID: string; + handleSelectValidators: (data: string[]) => void; +}) => { + const { getValidators } = useStaking(); + const { validatorsList } = getValidators({ chainID }); + const [selectedOptions, setSelectedOptions] = useState([]); + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + + const handleValidatorChange = ( + options: ValidatorOption[] | ValidatorOption | null + ) => { + const updatedValidators = Array.isArray(options) + ? options + : options + ? [options] + : []; + setSelectedOptions(updatedValidators); + const validators = updatedValidators.map((validator) => validator.address); + handleSelectValidators(validators); + }; + + const removeValidator = (validatorToRemove: ValidatorOption) => { + const updatedValidators = selectedOptions.filter( + (option) => option.address !== validatorToRemove.address + ); + setSelectedOptions(updatedValidators); + const validators = updatedValidators.map((validator) => validator.address); + handleSelectValidators(validators); + }; + + return ( +
    + +
    + {selectedOptions.map((option) => ( +
    + +
    {shortenName(option.label, 15)}
    + +
    + ))} +
    +
    + ); +}; + +export default ValidatorAutoComplete; + +const CustomAutoComplete = ({ + options, + selectedOptions, + handleChange, + dataLoading, + name, + emptyText, + multiple, +}: { + options: ValidatorOption[]; + selectedOptions: ValidatorOption[]; + handleChange: (options: ValidatorOption[] | ValidatorOption | null) => void; + dataLoading: boolean; + name: string; + emptyText: string; + multiple?: boolean; +}) => { + const isOptionSelected = (option: ValidatorOption) => + selectedOptions.some((selected) => selected.address === option.address); + + const handleOptionClick = (option: ValidatorOption) => { + if (isOptionSelected(option)) { + handleChange( + selectedOptions.filter( + (selected) => selected.address !== option.address + ) + ); + } else { + handleChange(multiple ? [...selectedOptions, option] : [option]); + } + }; + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderOption = (props: any, option: ValidatorOption) => ( +
  • handleOptionClick(option)} + > +
    + +
    + + {shortenName(option.label, 15)} + +
    +
    +
  • + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderInput = (params: any) => ( + , + }} + sx={{ + '& .MuiInputBase-input': { + color: 'white', + fontSize: '14px', + fontWeight: 300, + fontFamily: 'Libre Franklin', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: 'white', + }, + }} + /> + ); + + return ( + option.label} + renderOption={renderOption} + renderInput={renderInput} + noOptionsText={} + onChange={(_, newValue) => handleChange(newValue)} + value={selectedOptions} + multiple={multiple} + PaperComponent={({ children }) => ( + + {options?.length ? ( + <>{children} + ) : dataLoading ? ( +
    + +
    + ) : ( + + )} +
    + )} + sx={{ ...customTextFieldStyles, ...customAutoCompleteStyles }} + /> + ); +}; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/error.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/loading.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/settings/authz/new-authz/page.tsx b/frontend/src/app/(routes)/settings/authz/new-authz/page.tsx new file mode 100644 index 000000000..01ebab8c7 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/new-authz/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import React from 'react'; +import '../../settings.css'; +import Link from 'next/link'; +import PageHeader from '@/components/common/PageHeader'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import NewAuthzPage from './components/NewAuthzPage'; + +const PageCreateAuthz = () => { + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const connectWallet = () => { + dispatch(setConnectWalletOpen(true)); + }; + return ( +
    +
    + + Back + + +
    + {isWalletConnected ? ( + + ) : ( +
    + +
    + )} +
    + ); +}; + +export default PageCreateAuthz; diff --git a/frontend/src/app/(routes)/settings/authz/page.tsx b/frontend/src/app/(routes)/settings/authz/page.tsx new file mode 100644 index 000000000..a894f4834 --- /dev/null +++ b/frontend/src/app/(routes)/settings/authz/page.tsx @@ -0,0 +1,41 @@ +'use client'; + +import React from 'react'; +import '../settings.css'; +import SettingsLayout from '../SettingsLayout'; +import { useRouter } from 'next/navigation'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import AuthzPage from './AuthzPage'; + +const Page = () => { + const router = useRouter(); + + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const chainIDs = Object.keys(nameToChainIDs).map( + (chainName) => nameToChainIDs[chainName] + ); + + const createNewAuthz = () => { + router.push('/settings/authz/new-authz'); + }; + + return ( + +
    + {chainIDs.length ? ( + + ) : ( +
    + - Chain Not found - +
    + )} +
    +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/settings/components/AddressBook.tsx b/frontend/src/app/(routes)/settings/components/AddressBook.tsx new file mode 100644 index 000000000..52ec70086 --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/AddressBook.tsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import Image from 'next/image'; +import DialogConfirmDelete from '@/components/common/DialogConfirmDelete'; +import DialogAddAddress from '../components/DialogAddAddress'; + +interface Address { + name: string; + address: string; +} + +const addresses: Address[] = [ + { + name: 'Pavani', + address: 'pasg1y0hvu8ts6m8hzwp57t9rhdgvnpc7yltguyufwf', + }, + { + name: 'Alex', + address: 'pasg1y0hvu8ts6m8hzwp57t9rhdgvnpc7yltguyufwg', + }, + { + name: 'Sam', + address: 'pasg1y0hvu8ts6m8hzwp57t9rhdgvnpc7yltguyufwh', + }, +]; + +const AddressBook: React.FC = () => { + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [selectedAddress, setSelectedAddress] = useState(null); + const [loading, setLoading] = useState(false); + + const handleDeleteClick = (address: string) => { + setSelectedAddress(address); + setIsDeleteDialogOpen(true); + }; + + const handleCloseDeleteDialog = () => { + setIsDeleteDialogOpen(false); + setSelectedAddress(null); + }; + + const handleDelete = async () => { + if (selectedAddress) { + setLoading(true); + + setTimeout(() => { + console.log(`Address deleted: ${selectedAddress}`); + setLoading(false); + handleCloseDeleteDialog(); + }, 1000); + } + }; + + const handleOpenAddDialog = () => { + setIsAddDialogOpen(true); + }; + + const handleCloseAddDialog = () => { + setIsAddDialogOpen(false); + }; + + return ( +
    + + + + + + + + + + + {addresses.map((entry, index) => ( + + + + + + ))} + +
    NameAddress
    + Avatar + {entry.name} + {entry.address} + +
    + + +
    + ); +}; + + + // Address book header // + +const AddressBookHeader: React.FC<{ onAddClick: () => void }> = ({ onAddClick }) => { + return ( +
    +
    +
    Address Book
    +
    +
    + Connect your wallet now to access all the modules on resolute{' '} +
    +
    +
    +
    +
    + +
    +
    + ); +}; + +export default AddressBook; diff --git a/frontend/src/app/(routes)/settings/components/AddressBookLoading.tsx b/frontend/src/app/(routes)/settings/components/AddressBookLoading.tsx new file mode 100644 index 000000000..907804d8c --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/AddressBookLoading.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +const AddressBookLoading = () => { + return ( +
    + + + + {['Name', 'Address', ''].map( + (header, hIndex) => ( + + ) + )} + + + + {Array(3) + .fill(null) + .map((_, colIndex) => ( + + {Array(3) + .fill(null) + .map((_, colIndex) => ( + + ))} + + ))} + + +
    +
    + {header} +
    +
    +
    +
    +
    + ) +}; + +export default AddressBookLoading; diff --git a/frontend/src/app/(routes)/settings/components/CustomNetworkLoading.tsx b/frontend/src/app/(routes)/settings/components/CustomNetworkLoading.tsx new file mode 100644 index 000000000..dc0a0877c --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/CustomNetworkLoading.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +const CustomNetworkLoading = () => { + return ( +
    + {[1, 2, 3].map((_, index) => ( +
    +
    +
    +

    +

    +
    +
    +
    +
    +
    +

    Network

    +

    +
    +
    +

    Network ID

    +

    +
    +
    +
    + ))} +
    + ); +}; + +export default CustomNetworkLoading; diff --git a/frontend/src/app/(routes)/settings/components/DialogAddAddress.tsx b/frontend/src/app/(routes)/settings/components/DialogAddAddress.tsx new file mode 100644 index 000000000..d53e7fa49 --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/DialogAddAddress.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import CustomDialog from '../../../../components/common/CustomDialog'; +import { TextField } from '@mui/material'; +import { customTransferTextFieldStyles } from '../../transfers/styles'; + +const DialogAddAddress = ({ + onClose, + open, +}: { + open: boolean; + onClose: () => void; +}) => { + return ( + +
    +
    +
    Name
    + +
    +
    +
    Address
    + +
    + +
    +
    + ); +}; + +export default DialogAddAddress; diff --git a/frontend/src/app/(routes)/settings/components/DialogRevokeAll.tsx b/frontend/src/app/(routes)/settings/components/DialogRevokeAll.tsx new file mode 100644 index 000000000..27e90f211 --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/DialogRevokeAll.tsx @@ -0,0 +1,97 @@ +import CustomDialog from '@/components/common/CustomDialog'; +import React from 'react'; +import Image from 'next/image'; + +const DialogRevokeAll = ({ + onClose, + open, +}: { + open: boolean; + onClose: () => void; +}) => { + return ( + +
    +
    +
    +
    + network-logo +

    Send

    +
    +
    +

    Spend Limit

    +

    120 AKT

    +
    +
    +

    Expiry

    +

    23rd March 2024, 11:23 pm

    +
    +
    +
    +
    + network-logo +

    Grant Authz

    +
    +
    +

    Expiry

    +

    23rd March 2024, 11:23 pm

    +
    +
    +
    +
    +
    + network-logo +

    Send

    +
    +
    +
    +
    +

    Spend Limit

    +

    120 AKT

    +
    +
    +

    Expiry

    +

    23rd March 2024, 11:23 pm

    +
    +
    +
    +

    Validator List (Deny)

    +
    + {['Vitwit', 'Stakefish', 'Polkachu'].map((validator, index) => ( +
    + validator-logo +

    {validator}

    +
    + ))} +
    +
    +
    +
    + +
    +
    + ); +}; + +export default DialogRevokeAll; diff --git a/frontend/src/app/(routes)/settings/components/DialogViewDetails.tsx b/frontend/src/app/(routes)/settings/components/DialogViewDetails.tsx new file mode 100644 index 000000000..cdff5b337 --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/DialogViewDetails.tsx @@ -0,0 +1,95 @@ +import CustomDialog from '@/components/common/CustomDialog'; +import React from 'react'; +import Image from 'next/image'; + +const DialogViewDetails = ({ + onClose, + open, +}: { + open: boolean; + onClose: () => void; +}) => { + return ( + +
    +
    +
    +
    + network-logo +

    Send

    +
    +
    +

    Spend Limit

    +

    120 AKT

    +
    +
    +

    Expiry

    +

    23rd March 2024, 11:23 pm

    +
    +
    +
    +
    + network-logo +

    Grant Authz

    +
    +
    +

    Expiry

    +

    23rd March 2024, 11:23 pm

    +
    +
    +
    +
    +
    + network-logo +

    Send

    +
    +
    +
    +
    +

    Spend Limit

    +

    120 AKT

    +
    +
    +

    Expiry

    +

    23rd March 2024, 11:23 pm

    +
    +
    +
    +

    Validator List (Deny)

    +
    + {['Vitwit', 'Stakefish', 'Polkachu'].map((validator, index) => ( +
    + validator-logo +

    {validator}

    +
    + ))} +
    +
    +
    +
    +
    +
    + ); +}; + +export default DialogViewDetails; diff --git a/frontend/src/app/(routes)/settings/components/NetworksList.tsx b/frontend/src/app/(routes)/settings/components/NetworksList.tsx new file mode 100644 index 000000000..a960c71de --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/NetworksList.tsx @@ -0,0 +1,191 @@ +import { SEARCH_ICON } from '@/constants/image-names'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { AUTHZ, FEEGRANT } from '@/utils/constants'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; + +interface SelectNetworksProps { + selectedNetworks: string[]; + handleSelectChain: (chainName: string) => void; + module: string; +} + +const DISPLAYED_NETWORKS_COUNT = 5; + +const SelectNetworks = (props: SelectNetworksProps) => { + const { selectedNetworks, handleSelectChain, module } = props; + + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const chainNames = Object.keys(nameToChainIDs); + const [displayedChains, setDisplayedChains] = useState( + chainNames?.slice(0, DISPLAYED_NETWORKS_COUNT) + ); + const [viewAllChains, setViewAllChains] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [filteredChains, setFilteredChains] = useState([]); + + const handleSearchQueryChange = (e: React.ChangeEvent) => { + const query = e.target.value; + const filtered = chainNames.filter((chain) => + chain.toLowerCase().includes(query.toLowerCase()) + ); + setFilteredChains(filtered); + setSearchQuery(query); + }; + + const handleViewAllChains = (value: boolean) => { + setDisplayedChains( + value ? chainNames : chainNames.slice(0, DISPLAYED_NETWORKS_COUNT) + ); + setViewAllChains(value); + }; + + useEffect(() => { + if (Object.keys(nameToChainIDs).length) { + const chains = Object.keys(nameToChainIDs); + setDisplayedChains(chains.slice(0, DISPLAYED_NETWORKS_COUNT)); + } + }, [nameToChainIDs]); + + return ( +
    +
    +
    Select Networks
    + setSearchQuery('')} + /> +
    + + +
    + ); +}; + +export default SelectNetworks; + +const SearchNetworkInput = ({ + searchQuery, + handleSearchQueryChange, + resetInput, +}: { + searchQuery: string; + handleSearchQueryChange: (e: React.ChangeEvent) => void; + resetInput: () => void; +}) => { + return ( +
    + + + {searchQuery?.length > 0 && ( + + )} +
    + ); +}; + +const NetworkItem = ({ + chainLogo, + chainName, + selected, + handleSelectChain, +}: { + chainName: string; + chainLogo: string; + selected: boolean; + handleSelectChain: () => void; +}) => { + return ( + + ); +}; + +const NetworksList = ({ + networks, + selectedNetworks, + handleSelectChain, + module, +}: { + networks: string[]; + selectedNetworks: string[]; + handleSelectChain: (chainName: string) => void; + module: string; +}) => { + const { getChainInfo } = useGetChainInfo(); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + + return ( +
    + {networks?.length ? ( +
    + {networks.map((network) => { + const chainID = nameToChainIDs?.[network]; + const { chainName, chainLogo, enableModules } = + getChainInfo(chainID); + return chainName && + ((enableModules.authz && module === AUTHZ) || + (enableModules.feegrant && module === FEEGRANT)) ? ( + handleSelectChain(chainName)} + /> + ) : null; + })} +
    + ) : ( +
    +
    Network not found
    +
    + )} +
    + ); +}; diff --git a/frontend/src/app/(routes)/settings/components/SettingsHeader.tsx b/frontend/src/app/(routes)/settings/components/SettingsHeader.tsx new file mode 100644 index 000000000..d925adc07 --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/SettingsHeader.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import TabsGroup from './TabsGroup'; + +interface SettingsHeaderProps { + action: () => void; + actionName: string; + tabName: string; +} + +const SettingsHeader = (props: SettingsHeaderProps) => { + const { action, actionName, tabName } = props; + return ( +
    +
    Settings
    + +
    + ); +}; + +export default SettingsHeader; diff --git a/frontend/src/app/(routes)/settings/components/TabsGroup.tsx b/frontend/src/app/(routes)/settings/components/TabsGroup.tsx new file mode 100644 index 000000000..cca708da5 --- /dev/null +++ b/frontend/src/app/(routes)/settings/components/TabsGroup.tsx @@ -0,0 +1,54 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import Link from 'next/link'; +import React from 'react'; + +const TABS = ['general', 'authz', 'feegrant']; + +const TabsGroup = ({ + selectedTab, + action, + actionName, +}: { + selectedTab: string; + action: () => void; + actionName: string; +}) => { + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork.chainName + ); + const getPathUrl = (tab: string) => { + if (tab === 'general') { + return '/settings'; + } else if (selectedNetwork) { + return `/settings/${tab}/${selectedNetwork.toLowerCase()}`; + } else { + return `/settings/${tab}`; + } + }; + return ( +
    +
    + {TABS.map((tab) => ( +
    + + {tab} + +
    +
    + ))} +
    + +
    + ); +}; + +export default TabsGroup; diff --git a/frontend/src/app/(routes)/settings/feegrant/FeegrantPage.tsx b/frontend/src/app/(routes)/settings/feegrant/FeegrantPage.tsx new file mode 100644 index 000000000..6f3d263d1 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/FeegrantPage.tsx @@ -0,0 +1,31 @@ +import useInitFeegrant from '@/custom-hooks/useInitFeegrant'; +import React, { useState } from 'react'; +import FeegrantFilters from './components/FeegrantFilters'; +import FeegrantsToMe from './components/FeegrantsToMe'; +import FeegrantsByMe from './components/FeegrantsByMe'; + +const FeegrantPage = ({ chainIDs }: { chainIDs: string[] }) => { + useInitFeegrant({ chainIDs, shouldFetch: true }); + + const [isGrantedToMe, setIsGrantedToMe] = useState(true); + + const handleFilterChange = (value: boolean) => { + setIsGrantedToMe(value); + }; + + return ( +
    + + {isGrantedToMe ? ( + + ) : ( + + )} +
    + ); +}; + +export default FeegrantPage; diff --git a/frontend/src/app/(routes)/settings/feegrant/[network]/error.tsx b/frontend/src/app/(routes)/settings/feegrant/[network]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/[network]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/settings/feegrant/[network]/loading.tsx b/frontend/src/app/(routes)/settings/feegrant/[network]/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/[network]/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/settings/feegrant/[network]/page.tsx b/frontend/src/app/(routes)/settings/feegrant/[network]/page.tsx new file mode 100644 index 000000000..0b9ea6b5b --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/[network]/page.tsx @@ -0,0 +1,46 @@ +'use client'; + +import React from 'react'; +import '../../settings.css'; +import SettingsLayout from '../../SettingsLayout'; +import { useParams, useRouter } from 'next/navigation'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import FeegrantPage from '../FeegrantPage'; + +const Page = () => { + const params = useParams(); + const paramChains = params.network; + const chainNames = + typeof paramChains === 'string' ? [paramChains.toLowerCase()] : paramChains; + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const chainIDs: string[] = []; + Object.keys(nameToChainIDs).forEach((chain) => { + chainNames.forEach((paramChain) => { + if (chain === paramChain.toLowerCase()) + chainIDs.push(nameToChainIDs[chain]); + }); + }); + + const router = useRouter(); + const createNewFeegrant = () => { + router.push('/settings/feegrant/new-feegrant'); + }; + + return ( + + {chainIDs.length ? ( + + ) : ( +
    + - Chain Not found - +
    + )} +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/DialogFeegrantDetails.tsx b/frontend/src/app/(routes)/settings/feegrant/components/DialogFeegrantDetails.tsx new file mode 100644 index 000000000..71c9d3004 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/DialogFeegrantDetails.tsx @@ -0,0 +1,124 @@ +import CustomDialog from '@/components/common/CustomDialog'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getTypeURLName } from '@/utils/authorizations'; +import { getLocalTime, getTimeDifferenceToFutureDate } from '@/utils/dataTime'; +import { parseTokens } from '@/utils/denom'; +import { + ALLOWED_MSG_ALLOWANCE, + getExpiryDate, + getPeriodExpiryDate, + getPeriodSpendLimit, + getSpendLimit, + PERIODIC_ALLOWANCE, +} from '@/utils/feegrant'; +import { capitalizeFirstLetter, convertToSpacedName } from '@/utils/util'; +import { Tooltip } from '@mui/material'; +import { get } from 'lodash'; +import Image from 'next/image'; +import React from 'react'; + +interface DialogFeegrantDetailsProps { + chainID: string; + open: boolean; + onClose: () => void; + grant: BasicAllowance | PeriodicAllowance | AllowedMsgAllowance; +} + +const DialogFeegrantDetails = (props: DialogFeegrantDetailsProps) => { + const { chainID, onClose, open, grant } = props; + + const { getDenomInfo, getChainInfo } = useGetChainInfo(); + const { displayDenom, decimals } = getDenomInfo(chainID); + const { chainLogo, chainName } = getChainInfo(chainID); + + let allowedMsgs: Array; + const isPeriodic = + get(grant, '@type') === PERIODIC_ALLOWANCE || + get(grant, 'allowance.@type') === PERIODIC_ALLOWANCE; + const spendLimit = getSpendLimit(grant); + const allowanceExpiry = getExpiryDate(grant); + const periodSpendLimit = getPeriodSpendLimit(grant); + const periodExpiry = getPeriodExpiryDate(grant); + + if (get(grant, '@type') === ALLOWED_MSG_ALLOWANCE) { + allowedMsgs = get(grant, 'allowed_messages', []); + } else { + allowedMsgs = []; + } + + return ( + +
    +
    + + + {isPeriodic ? ( + <> + + + + ) : null} +
    +
    + {allowedMsgs.length > 0 ? ( + allowedMsgs.map((message: string) => ( +
    + + {chainName} + +

    + {convertToSpacedName(getTypeURLName(message))} +

    +
    + )) + ) : ( +
    + + {chainName} + +

    All Messages

    +
    + )} +
    +
    +
    + ); +}; + +export default DialogFeegrantDetails; + +const FeegrantInfoCard = ({ name, value }: { name: string; value: string }) => { + return ( +
    +
    {name}
    +
    {value}
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/FeegrantByMeLoading.tsx b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantByMeLoading.tsx new file mode 100644 index 000000000..bf446eec7 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantByMeLoading.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +const FeegrantByMeLoading = () => { + return ( +
    + {[1, 2, 3].map((_, index) => ( +
    +
    +
    +

    +

    +
    +
    +

    +

    +
    +
    +
    +

    Allowed Messages

    +
    +

    +

    +

    +

    +
    +
    +
    +
    +
    +
    +
    + ))} +
    + ); +}; + +export default FeegrantByMeLoading; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/FeegrantFilters.tsx b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantFilters.tsx new file mode 100644 index 000000000..1743d07ea --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantFilters.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +const FeegrantFilters = ({ + handleFilterChange, + isGrantsToMe, +}: { + isGrantsToMe: boolean; + handleFilterChange: (value: boolean) => void; +}) => { + return ( +
    + + +
    + ); +}; + +export default FeegrantFilters; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/FeegrantToMeLoading.tsx b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantToMeLoading.tsx new file mode 100644 index 000000000..20ce018d1 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantToMeLoading.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +const FeegrantToMeLoading = () => { + return ( +
    + {[1, 2, 3].map((_, index) => ( +
    +
    +
    Address
    +
    +

    +

    +
    +
    +
    +

    Allowed Messages

    +
    +

    +

    +

    +

    +
    +
    +
    +
    +
    +
    +
    + ))} +
    + ); +}; + +export default FeegrantToMeLoading; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/FeegrantTypeBadge.tsx b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantTypeBadge.tsx new file mode 100644 index 000000000..14f01b922 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantTypeBadge.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const FeegrantTypeBadge = ({ isPeriodic }: { isPeriodic: boolean }) => { + return ( +
    + {isPeriodic ? 'Periodic' : 'Basic'} +
    + ); +}; + +export default FeegrantTypeBadge; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/FeegrantsByMe.tsx b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantsByMe.tsx new file mode 100644 index 000000000..b8eb637ca --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantsByMe.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import useFeeGrants from '@/custom-hooks/useFeeGrants'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import FeegrantByMeLoading from './FeegrantByMeLoading'; +import GrantByMeCard from './GrantByMeCard'; +import WithConnectionIllustration from '@/components/illustrations/withConnectionIllustration'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { groupBy } from 'lodash'; + +const FeegrantsByMe = ({ chainIDs }: { chainIDs: string[] }) => { + const { getGrantsByMe } = useFeeGrants(); + const { convertToCosmosAddress } = useGetChainInfo(); + const addressGrants = getGrantsByMe(chainIDs); + const loading = useAppSelector( + (state) => state.feegrant.getGrantsByMeLoading + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + let grantsList: any[] = []; + addressGrants.forEach((grant) => { + const data = { + ...grant, + cosmosAddress: convertToCosmosAddress(grant.address), + }; + grantsList = [...grantsList, data]; + }); + const groupedGrants = groupBy(grantsList, 'cosmosAddress'); + + return ( +
    + {Object.entries(groupedGrants).map(([granterKey, grants]) => ( +
    + {grants.map((grant, index) => ( + + ))} +
    + ))} + {!!loading ? ( + + ) : ( + <> + {!addressGrants?.length && ( + + )} + + )} +
    + ); +}; + +export default FeegrantsByMe; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/FeegrantsToMe.tsx b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantsToMe.tsx new file mode 100644 index 000000000..50e8bb0ba --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/FeegrantsToMe.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import useFeeGrants from '@/custom-hooks/useFeeGrants'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import FeegrantToMeLoading from './FeegrantToMeLoading'; +import GrantToMeCard from './GrantToMeCard'; +import WithConnectionIllustration from '@/components/illustrations/withConnectionIllustration'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { groupBy } from 'lodash'; + +const FeegrantsToMe = ({ chainIDs }: { chainIDs: string[] }) => { + const { getGrantsToMe } = useFeeGrants(); + const { convertToCosmosAddress } = useGetChainInfo(); + const addressGrants = getGrantsToMe(chainIDs); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + let grantsList: any[] = []; + addressGrants.forEach((grant) => { + const data = { + ...grant, + cosmosAddress: convertToCosmosAddress(grant.address), + }; + grantsList = [...grantsList, data]; + }); + const groupedGrants = groupBy(grantsList, 'cosmosAddress'); + + const loading = useAppSelector( + (state) => state.feegrant.getGrantsToMeLoading + ); + + return ( +
    + {Object.entries(groupedGrants).map(([granterKey, grants]) => ( +
    + {grants.map((grant, index) => ( + + ))} +
    + ))} + {!!loading ? ( + + ) : ( + <> + {!addressGrants?.length && ( + + )} + + )} +
    + ); +}; + +export default FeegrantsToMe; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/GrantByMeCard.tsx b/frontend/src/app/(routes)/settings/feegrant/components/GrantByMeCard.tsx new file mode 100644 index 000000000..cb6680824 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/GrantByMeCard.tsx @@ -0,0 +1,148 @@ +import React, { useEffect, useState } from 'react'; +import Image from 'next/image'; +import Copy from '@/components/common/Copy'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + ALLOWED_MSG_ALLOWANCE, + MAP_TXN_MSG_TYPES, + PERIODIC_ALLOWANCE, +} from '@/utils/feegrant'; +import { get } from 'lodash'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { convertToSpacedName, shortenAddress } from '@/utils/util'; +import { getTypeURLName } from '@/utils/authorizations'; +import CustomButton from '@/components/common/CustomButton'; +import { TxStatus } from '@/types/enums'; +import { txRevoke } from '@/store/features/feegrant/feegrantSlice'; +import DialogFeegrantDetails from './DialogFeegrantDetails'; +import FeegrantTypeBadge from './FeegrantTypeBadge'; +import useGetFeegranter from '@/custom-hooks/useGetFeegranter'; + +interface GrantByMeCardProps { + chainID: string; + address: string; + grant: Allowance; +} + +const GrantByMeCard: React.FC = ({ + chainID, + address, + grant, +}) => { + const dispatch = useAppDispatch(); + const [viewDetailsOpen, setViewDetailsOpen] = useState(false); + const [selectedGrantee, setSelectedGrantee] = useState(''); + + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { chainLogo, chainName } = getChainInfo(chainID); + const { minimalDenom } = getDenomInfo(chainID); + const { getFeegranter } = useGetFeegranter(); + + const txRevokeStatus = useAppSelector( + (state) => state.feegrant.chains[chainID].tx.status + ); + const txRevokeLoading = + txRevokeStatus === TxStatus.PENDING && address === selectedGrantee; + + const { allowance } = grant; + const allowedMsgs = + get(allowance, '@type') === ALLOWED_MSG_ALLOWANCE + ? get(allowance, 'allowed_messages', []) + : []; + const isPeriodic = + get(grant, '@type') === PERIODIC_ALLOWANCE || + get(grant, 'allowance.@type') === PERIODIC_ALLOWANCE; + + useEffect(() => { + if (txRevokeStatus !== TxStatus.PENDING) { + setSelectedGrantee(''); + } + }, [txRevokeStatus]); + + const handleRevokeFeegrant = () => { + setSelectedGrantee(grant.grantee); + dispatch( + txRevoke({ + granter: grant.granter, + grantee: grant.grantee, + basicChainInfo: getChainInfo(chainID), + baseURLs: getChainInfo(chainID).restURLs, + feegranter: getFeegranter( + chainID, + MAP_TXN_MSG_TYPES['revoke_feegrant'] + ), + denom: minimalDenom, + }) + ); + }; + + const toggleViewDetails = () => { + setViewDetailsOpen((prev) => !prev); + }; + + const renderAllowedMessages = () => { + if (allowedMsgs.length > 0) { + return allowedMsgs.map((message) => ( +
    +

    + {convertToSpacedName(getTypeURLName(message))} +

    +
    + )); + } + + return ( +
    +

    All

    +
    + ); + }; + + return ( +
    +
    +
    +
    +

    Grantee

    +
    +
    +

    {shortenAddress(address, 20)}

    + + +
    +
    +
    + network-logo +

    {chainName}

    +
    +
    +
    +

    Allowed Messages

    +
    {renderAllowedMessages()}
    +
    +
    +
    +
    + +
    + View Details +
    +
    +
    + +
    + ); +}; + +export default GrantByMeCard; diff --git a/frontend/src/app/(routes)/settings/feegrant/components/GrantToMeCard.tsx b/frontend/src/app/(routes)/settings/feegrant/components/GrantToMeCard.tsx new file mode 100644 index 000000000..e5d8ff1cf --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/components/GrantToMeCard.tsx @@ -0,0 +1,160 @@ +import React, { useState } from 'react'; +import Image from 'next/image'; +import Copy from '@/components/common/Copy'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { TICK_ICON } from '@/constants/image-names'; +import { get } from 'lodash'; +import { + capitalizeFirstLetter, + convertToSpacedName, + shortenAddress, +} from '@/utils/util'; +import { getTypeURLName } from '@/utils/authorizations'; +import { Tooltip } from '@mui/material'; +import { ALLOWED_MSG_ALLOWANCE, PERIODIC_ALLOWANCE } from '@/utils/feegrant'; +import DialogFeegrantDetails from './DialogFeegrantDetails'; +import FeegrantTypeBadge from './FeegrantTypeBadge'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getAddressByPrefix } from '@/utils/address'; +import useFeeGrants from '@/custom-hooks/useFeeGrants'; +import { enableFeegrantMode } from '@/store/features/feegrant/feegrantSlice'; +import { setFeegrantMode } from '@/utils/localStorage'; +import { exitAuthzMode } from '@/store/features/authz/authzSlice'; + +interface GrantToMeCardProps { + chainID: string; + address: string; + grant: Allowance; +} + +const GrantToMeCard: React.FC = ({ + chainID, + address, + grant, +}) => { + const dispatch = useAppDispatch(); + const [viewDetailsOpen, setViewDetailsOpen] = useState(false); + const { getChainInfo, convertToCosmosAddress } = useGetChainInfo(); + const { disableFeegrantMode } = useFeeGrants(); + const { + chainLogo, + chainName, + prefix, + address: walletAddress, + } = getChainInfo(chainID); + const feegranterAddress = useAppSelector( + (state) => state.feegrant.feegrantAddress + ); + const feegrantModeEnabled = useAppSelector( + (state) => state.feegrant.feegrantModeEnabled + ); + const feegranterCosmosAddress = convertToCosmosAddress(address); + const feegranteeCosmosAddress = convertToCosmosAddress(walletAddress); + + const { allowance } = grant; + const allowedMsgs = + get(allowance, '@type') === ALLOWED_MSG_ALLOWANCE + ? get(allowance, 'allowed_messages', []) + : []; + const isPeriodic = + get(grant, '@type') === PERIODIC_ALLOWANCE || + get(grant, 'allowance.@type') === PERIODIC_ALLOWANCE; + + const isGranterSelected = () => { + const nativeFeegranterAddress = getAddressByPrefix( + feegranterAddress, + prefix + ); + return nativeFeegranterAddress === address; + }; + const isSelected = isGranterSelected(); + + const handleUseFeegrant = () => { + if (feegrantModeEnabled) { + disableFeegrantMode(); + } else { + dispatch(enableFeegrantMode({ address: feegranterCosmosAddress })); + dispatch(exitAuthzMode()); + setFeegrantMode(feegranteeCosmosAddress, feegranterCosmosAddress); + } + }; + + const toggleViewDetails = () => { + setViewDetailsOpen((prev) => !prev); + }; + + const renderAllowedMessages = () => { + if (allowedMsgs.length > 0) { + return allowedMsgs.map((message) => ( +
    +

    + {convertToSpacedName(getTypeURLName(message))} +

    + + {chainName} + +
    + )); + } + + return ( +
    +

    All

    + + {chainName} + +
    + ); + }; + + return ( +
    +
    +
    +

    Granter

    + {isSelected && ( +
    + used-icon + + Currently Using + +
    + )} +
    +
    +

    {shortenAddress(address, 20)}

    + + +
    +
    +
    +

    Allowed Messages

    +
    {renderAllowedMessages()}
    +
    +
    +
    +
    + +
    + View Details +
    +
    +
    + +
    + ); +}; + +export default GrantToMeCard; diff --git a/frontend/src/app/(routes)/settings/feegrant/error.tsx b/frontend/src/app/(routes)/settings/feegrant/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/settings/feegrant/loading.tsx b/frontend/src/app/(routes)/settings/feegrant/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/CreateFeegrantForm.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/CreateFeegrantForm.tsx new file mode 100644 index 000000000..8eed1b7d4 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/CreateFeegrantForm.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { Control, FieldErrors, UseFormGetValues } from 'react-hook-form'; +import { feegrantMsgTypes } from '@/utils/feegrant'; +import GranteeAddressField from './GranteeAddressField'; +import CustomTextField from './CustomTextField'; +import ExpirationField from './ExpirationField'; +import ToggleSwitch from '@/components/common/ToggleSwitch'; +import MsgItem from '../../../(general)/components/MsgItem'; + +interface CreateFeegrantFormFields { + grantee_address: string; + expiration: string; + spend_limit: string; + period: string; + period_spend_limit: string; +} + +interface ICreateFeegrantForm { + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: Control; + errors: FieldErrors; + isPeriodic: boolean; + setIsPeriodic: (value: boolean) => void; + handleSelectMsg: (msgType: string) => void; + selectedMsgs: string[]; + allTxns: boolean; + setAllTxns: (value: boolean) => void; + getValues: UseFormGetValues; +} + +const CreateFeegrantForm: React.FC = (props) => { + const { + control, + errors, + isPeriodic, + setIsPeriodic, + handleSelectMsg, + selectedMsgs, + allTxns, + setAllTxns, + getValues, + } = props; + const msgTypes = feegrantMsgTypes(); + return ( +
    + + +
    +
    +
    + Spend Limit +
    + setIsPeriodic(checked)} + text={'Use Periodic'} + /> +
    + +
    + {isPeriodic && ( +
    +
    +
    + Period +
    + +
    +
    +
    + Period Spend Limit +
    + +
    +
    + )} +
    +
    +
    + Transaction Messages +
    +
    + setAllTxns(checked)} + text="All Transactions" + height={16} + width={22.7} + /> +
    +
    + {!allTxns && ( +
    + {msgTypes.map((msg, index) => ( + + ))} +
    + )} +
    +
    + ); +}; + +export default CreateFeegrantForm; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/CustomTextField.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/CustomTextField.tsx new file mode 100644 index 000000000..0573afdc5 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/CustomTextField.tsx @@ -0,0 +1,53 @@ +import { TextField } from '@mui/material'; +import React from 'react'; +import { Control, Controller } from 'react-hook-form'; +import { customTextFieldStyles } from '@/utils/commonStyles'; + +const CustomTextField = ({ + error, + control, + name, + title, +}: { + error: string; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + control: Control; + name: string; + title: string; +}) => { + return ( +
    + { + const amount = Number(value); + if (value?.length && (Number.isNaN(amount) || amount <= 0)) + return 'Invalid input'; + }, + }} + render={({ field }) => ( + + )} + /> +
    + + {error || ''} + +
    +
    + ); +}; + +export default CustomTextField; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/DialogFeegrantTxStatus.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/DialogFeegrantTxStatus.tsx new file mode 100644 index 000000000..3ede13af5 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/DialogFeegrantTxStatus.tsx @@ -0,0 +1,152 @@ +import Copy from '@/components/common/Copy'; +import CustomDialog from '@/components/common/CustomDialog'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { + capitalizeFirstLetter, + cleanURL, + shortenAddress, + shortenName, +} from '@/utils/util'; +import { Tooltip } from '@mui/material'; +import Image from 'next/image'; +import Link from 'next/link'; +import React from 'react'; +import { REDIRECT_ICON } from '@/constants/image-names'; +import SelectedChains from '../../../(general)/components/SelectedChains'; + +const DialogFeegrantTxStatus = ({ + onClose, + open, + chainsStatus, + selectedChains, + selectedMsgs, + granteeAddress, +}: { + open: boolean; + onClose: () => void; + selectedMsgs: string[]; + chainsStatus: Record; + selectedChains: string[]; + granteeAddress: string; +}) => { + const { getChainInfo } = useGetChainInfo(); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + + return ( + +
    +
    +
    + Grantee Address +
    +
    +
    {shortenAddress(granteeAddress, 20)}
    + +
    +
    + +
    + {selectedChains.map((chain) => { + const chainID = nameToChainIDs?.[chain.toLowerCase()]; + const { chainLogo, chainName, explorerTxHashEndpoint } = + getChainInfo(chainID); + return ( +
    +
    +
    +
    + +
    + {capitalizeFirstLetter(chainName)} +
    +
    + +
    + {chainsStatus?.[chainID]?.txStatus === 'pending' ? ( +
    + Transaction Pending + +
    + ) : ( + <> + {chainsStatus?.[chainID]?.isTxSuccess ? ( +
    +
    +
    + {shortenName( + chainsStatus?.[chainID]?.txHash, + 18 + )} +
    + +
    + +
    Transaction Successful
    + + +
    + ) : ( + + + {chainsStatus?.[chainID]?.error === + 'Request rejected' + ? 'Wallet request rejected' + : 'Transaction Failed'} + + + )} + + )} +
    +
    +
    +
    +
    + {selectedMsgs.map((msg) => ( +
    + {msg} +
    + ))} +
    +
    + ); + })} +
    +
    +
    + ); +}; + +export default DialogFeegrantTxStatus; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/ExpirationField.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/ExpirationField.tsx new file mode 100644 index 000000000..23017a187 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/ExpirationField.tsx @@ -0,0 +1,57 @@ +import { TextField } from '@mui/material'; +import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import React from 'react'; +import { Control, Controller } from 'react-hook-form'; +import { expirationFieldStyles } from '../../../styles'; + +const ExpirationField = ({ + control, +}: { + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: Control; +}) => { + const date = new Date(); + const expiration = new Date(date.setTime(date.getTime() + 365 * 86400000)); + + return ( +
    +
    + Set Expiry +
    + ( + + ( + { + event.target.readOnly = true; + }} + onBlur={(event) => { + event.target.readOnly = false; + }} + /> + )} + value={value} + onChange={onChange} + /> + + )} + /> +
    + ); +}; + +export default ExpirationField; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/GranteeAddressField.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/GranteeAddressField.tsx new file mode 100644 index 000000000..fe611ea00 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/GranteeAddressField.tsx @@ -0,0 +1,56 @@ +import { customTextFieldStyles } from '@/utils/commonStyles'; +import { validateAddress } from '@/utils/util'; +import { TextField } from '@mui/material'; +import React from 'react'; +import { Control, Controller, UseFormGetValues } from 'react-hook-form'; + +const GranteeAddressField = ({ + error, + control, + getValues, +}: { + error: string; + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: Control; + getValues: UseFormGetValues; +}) => { + return ( +
    +
    + Grantee Address +
    + { + if (!validateAddress(getValues('grantee_address'))) { + return 'Invalid Address'; + } + }, + }} + render={({ field }) => ( + + )} + /> +
    + + {error || ''} + +
    +
    + ); +}; + +export default GranteeAddressField; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/NewFeegrantPage.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/NewFeegrantPage.tsx new file mode 100644 index 000000000..ca42a82b6 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/components/NewFeegrantPage.tsx @@ -0,0 +1,239 @@ +import React, { useState } from 'react'; +import SelectNetworks from '../../../components/NetworksList'; +import useGetFeegrantMsgs from '@/custom-hooks/useGetFeegrantMsgs'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { FieldValues, useForm } from 'react-hook-form'; +import { getFeegrantFormDefaultValues, MAP_TXN_MSG_TYPES } from '@/utils/feegrant'; +import CreateFeegrantForm from './CreateFeegrantForm'; +import { + CHAIN_NOT_SELECTED_ERROR, + MSG_NOT_SELECTED_ERROR, +} from '@/utils/errors'; +import useMultiTxTracker from '@/custom-hooks/useGetCreateFeegrantTxLoading'; +import { CircularProgress } from '@mui/material'; +import Copy from '@/components/common/Copy'; +import { shortenAddress } from '@/utils/util'; +import DialogFeegrantTxStatus from './DialogFeegrantTxStatus'; +import useGetFeegranter from '@/custom-hooks/useGetFeegranter'; +import SelectedChains from '../../../(general)/components/SelectedChains'; +import { FEEGRANT } from '@/utils/constants'; + +const NewFeegrantPage = () => { + const [selectedChains, setSelectedChains] = useState([]); + const [isPeriodic, setIsPeriodic] = useState(false); + const [selectedMsgs, setSelectedMsgs] = useState([]); + const [allTxns, setAllTxns] = useState(true); + const [txnStarted, setTxnStarted] = useState(false); + const [formValidationError, setFormValidationError] = useState({ + chains: '', + msgs: '', + }); + const [txStatusOpen, setTxStatusOpen] = useState(false); + + const { getFeegrantMsgs } = useGetFeegrantMsgs(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { getFeegranter } = useGetFeegranter(); + const { trackTxs, chainsStatus, currentTxCount } = useMultiTxTracker(); + + const { + handleSubmit, + control, + formState: { errors }, + getValues, + watch, + } = useForm({ + defaultValues: getFeegrantFormDefaultValues(), + }); + + const handleSelectChain = (chainName: string) => { + if (txnStarted) { + return; + } + const updatedSelection = selectedChains.includes(chainName) + ? selectedChains.filter((id) => id !== chainName) + : [...selectedChains, chainName]; + setSelectedChains(updatedSelection); + }; + + const handleSelectMsg = (msgType: string) => { + if (txnStarted) { + return; + } + const updatedSelection = selectedMsgs.includes(msgType) + ? selectedMsgs.filter((id) => id !== msgType) + : [...selectedMsgs, msgType]; + setSelectedMsgs(updatedSelection); + }; + + const validateForm = () => { + if (!selectedChains.length) { + setFormValidationError((prevState) => ({ + ...prevState, + chains: CHAIN_NOT_SELECTED_ERROR, + })); + return false; + } else if (!selectedMsgs.length && !allTxns) { + setFormValidationError({ + chains: '', + msgs: MSG_NOT_SELECTED_ERROR, + }); + return false; + } + setFormValidationError({ chains: '', msgs: '' }); + return true; + }; + + const onSubmit = (fieldValues: FieldValues) => { + if (!validateForm()) { + return; + } + const { chainWiseGrants } = getFeegrantMsgs({ + isFiltered: !allTxns, + msgsList: selectedMsgs, + selectedChains, + isPeriodic, + fieldValues: fieldValues, + }); + const txCreateFeegrantInputs: MultiChainFeegrantTx[] = []; + + chainWiseGrants.forEach((chain) => { + const chainID = chain.chainID; + const msgs = chain.msg; + const basicChainInfo = getChainInfo(chainID); + const { minimalDenom, decimals } = getDenomInfo(chainID); + const { feeAmount: avgFeeAmount } = basicChainInfo; + const feeAmount = avgFeeAmount * 10 ** decimals; + + txCreateFeegrantInputs.push({ + ChainID: chainID, + txInputs: { + basicChainInfo: basicChainInfo, + msg: msgs, + denom: minimalDenom, + feeAmount: feeAmount, + feegranter: getFeegranter( + chainID, + MAP_TXN_MSG_TYPES['grant_feegrant'] + ), + }, + }); + }); + + setTxnStarted(true); + setTxStatusOpen(true); + trackTxs(txCreateFeegrantInputs); + }; + + return ( +
    +
    + +
    onSubmit(e))} + id="create-feegrant-form" + > + setIsPeriodic(value)} + handleSelectMsg={handleSelectMsg} + selectedMsgs={selectedMsgs} + allTxns={allTxns} + setAllTxns={(value: boolean) => setAllTxns(value)} + getValues={getValues} + /> + +
    +
    +
    + +
    +
    + Grantee Address +
    +
    +
    + {shortenAddress(watch('grantee_address'), 20)} +
    + {watch('grantee_address') ? ( + + ) : null} +
    +
    + + {isPeriodic ? ( + + ) : null} + {isPeriodic ? ( + + ) : null} +
    +
    +
    + {formValidationError.chains || formValidationError.msgs} +
    + +
    +
    + { + setTxStatusOpen(false); + setTxnStarted(false); + }} + chainsStatus={chainsStatus} + selectedChains={selectedChains} + selectedMsgs={allTxns ? ['All Transactions'] : selectedMsgs} + granteeAddress={watch('grantee_address')} + /> +
    + ); +}; + +export default NewFeegrantPage; + +const DisplayNumber = ({ value, name }: { value: string; name: string }) => { + const parsedAmount = Number(value); + return ( +
    +
    + {name} +
    +
    +
    + {!Number.isNaN(parsedAmount) && parsedAmount ? parsedAmount : null} +
    +
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/error.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/loading.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/settings/feegrant/new-feegrant/page.tsx b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/page.tsx new file mode 100644 index 000000000..c49f71cc5 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/new-feegrant/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import React from 'react'; +import '../../settings.css'; +import NewFeegrantPage from './components/NewFeegrantPage'; +import Link from 'next/link'; +import PageHeader from '@/components/common/PageHeader'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; + +const PageCreateFeegrant = () => { + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const connectWallet = () => { + dispatch(setConnectWalletOpen(true)); + }; + return ( +
    +
    + + Back + + +
    + {isWalletConnected ? ( + + ) : ( +
    + +
    + )} +
    + ); +}; + +export default PageCreateFeegrant; diff --git a/frontend/src/app/(routes)/settings/feegrant/page.tsx b/frontend/src/app/(routes)/settings/feegrant/page.tsx new file mode 100644 index 000000000..54bf5a6b1 --- /dev/null +++ b/frontend/src/app/(routes)/settings/feegrant/page.tsx @@ -0,0 +1,41 @@ +'use client'; + +import React from 'react'; +import '../settings.css'; +import SettingsLayout from '../SettingsLayout'; +import { useRouter } from 'next/navigation'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import FeegrantPage from './FeegrantPage'; + +const Page = () => { + const router = useRouter(); + + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const chainIDs = Object.keys(nameToChainIDs).map( + (chainName) => nameToChainIDs[chainName] + ); + + const createNewFeegrant = () => { + router.push('/settings/feegrant/new-feegrant'); + }; + + return ( + +
    + {chainIDs.length ? ( + + ) : ( +
    + - Chain Not found - +
    + )} +
    +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/settings/settings.css b/frontend/src/app/(routes)/settings/settings.css new file mode 100644 index 000000000..4073de083 --- /dev/null +++ b/frontend/src/app/(routes)/settings/settings.css @@ -0,0 +1,119 @@ +.selected-tab { + background: linear-gradient(90deg, #4453df 0%, #7f5ced 100%); + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); +} + +.feegrant-main { + @apply pt-10 relative flex flex-col; + height: calc(100vh - 56px); +} + +.authz-main { + @apply pt-10 relative flex flex-col; + min-height: calc(100vh - 56px); +} + +.search-network-input { + @apply w-full border-none cursor-text focus:outline-none bg-transparent placeholder:text-[#FFFFFF30] placeholder:font-normal text-[#ffffff] flex-1 text-[14px]; +} + +.chain-item, +.msg-item { + @apply border-[0.25px] border-[#FFFFFF30] rounded-lg p-2 pr-3 flex items-center gap-1; +} + +.chain-item-selected, +.msg-item-selected { + @apply bg-[#FFFFFF10] border-transparent; +} + +.selected-validator { + @apply border-[0.25px] border-[#FFFFFF30] rounded-lg p-2 flex items-center gap-2; +} + +.error-box { + @apply flex justify-end mt-2; +} + +.error-chip { + @apply text-[12px] rounded-lg bg-[#ff00005b] text-center leading-normal max-w-fit px-2 truncate; +} + +.grants-card { + @apply flex p-6 rounded-2xl w-full; + background: rgba(255, 255, 255, 0.02); +} + +.selected-grants-card { + @apply flex p-6 border rounded-2xl border-solid border-[#4453DF] w-full; + background: linear-gradient( + 90deg, + rgba(68, 83, 223, 0.1) 0%, + rgba(127, 92, 237, 0.1) 100% + ); +} + +.permission-card { + @apply h-8 flex justify-center items-center gap-2 px-4 py-2 rounded-lg; + background: rgba(255, 255, 255, 0.06); +} + +.cancel-btn { + @apply border-[1px] text-[14px] rounded-full px-4 py-[10.5px] leading-[21px] !h-8 flex justify-center items-center text-[#fffffff0] border-solid border-[#D92101]; + background: rgba(217, 33, 1, 0.1); +} + +.selected-filter { + @apply flex justify-center items-center gap-4 px-4 py-[10.5px] rounded-lg border-[0.25px] hover:bg-[#ffffff14] hover:border-transparent w-[50%]; +} + +.dialog-permission { + @apply flex flex-col items-start gap-6 pb-6 rounded-2xl; + background: rgba(255, 255, 255, 0.02); +} + +.dialog-permission-header { + @apply flex items-start gap-2 w-full px-6 py-4 rounded-2xl; + background: rgba(255, 255, 255, 0.02); +} + +.feegrant-badge { + @apply text-[14px] h-6 max-w-[85px] border flex justify-center items-center gap-2 px-3 py-1 rounded-[100px] border-solid; +} + +.basic-badge { + @apply border-[#2BA472]; + background: rgba(43, 164, 114, 0.5); +} + +.periodic-badge { + @apply border-[#DA561E]; + background: rgba(218, 86, 30, 0.5); +} +.customnetwork-card { + @apply flex flex-col items-start gap-10 px-4 py-6 rounded-2xl; + background: linear-gradient( + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% + ); +} + +@keyframes pop-in { + 0% { + transform: scale(0.9); + opacity: 0; + } + 50% { + transform: scale(1.06); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.pop-in { + animation: pop-in 0.5s ease-in-out; +} diff --git a/frontend/src/app/(routes)/settings/styles.ts b/frontend/src/app/(routes)/settings/styles.ts new file mode 100644 index 000000000..916c2493d --- /dev/null +++ b/frontend/src/app/(routes)/settings/styles.ts @@ -0,0 +1,26 @@ +export const expirationFieldStyles = { + '& .MuiInputBase-input': { + color: '#fffffff0', + fontSize: '14px', + fontWeight: 200, + fontFamily: 'Libre Franklin', + lineHeight: '21px', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '0.25px solid #ffffff10', + borderRadius: '100px', + height: '40px', + }, + '& .Mui-focused': { + border: '0.25px solid #ffffff4a', + borderRadius: '100px', + }, + '& .MuiInputAdornment-root': { + '& button': { + color: '#ffffff80', + }, + }, +}; diff --git a/frontend/src/app/(routes)/staking/Staking.tsx b/frontend/src/app/(routes)/staking/Staking.tsx index 1183e56f0..853031047 100644 --- a/frontend/src/app/(routes)/staking/Staking.tsx +++ b/frontend/src/app/(routes)/staking/Staking.tsx @@ -1,14 +1,16 @@ 'use client'; import React from 'react'; -import StakingOverview from './components/StakingOverview'; -import StakingOverviewSidebar from './components/StakingOverviewSidebar'; +// import StakingOverview from './components/StakingOverview'; +// import StakingOverviewSidebar from './components/StakingOverviewSidebar'; +import StakingDashboard from './components/StakingDashboard'; const Staking = () => { return (
    - - + {/* + */} +
    ); }; diff --git a/frontend/src/app/(routes)/staking/[network]/ChainStaking.tsx b/frontend/src/app/(routes)/staking/[network]/ChainStaking.tsx deleted file mode 100644 index efa441f76..000000000 --- a/frontend/src/app/(routes)/staking/[network]/ChainStaking.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; -import React from 'react'; -import StakingPage from '../components/StakingPage'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; - -const ChainStaking = ({ - paramChain, - queryParams, -}: { - paramChain: string; - queryParams?: { [key: string]: string | undefined }; -}) => { - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const validChain = Object.keys(nameToChainIDs).some( - (chain) => paramChain.toLowerCase() === chain.toLowerCase() - ); - const validatorAddress = queryParams?.validator_address || ''; - const action = queryParams?.action || ''; - - return ( -
    - {validChain ? ( - - ) : ( - <> -
    - - Chain not found - -
    - - )} -
    - ); -}; - -export default ChainStaking; diff --git a/frontend/src/app/(routes)/staking/[network]/NewDelegationButton.tsx b/frontend/src/app/(routes)/staking/[network]/NewDelegationButton.tsx new file mode 100644 index 000000000..672fb29a8 --- /dev/null +++ b/frontend/src/app/(routes)/staking/[network]/NewDelegationButton.tsx @@ -0,0 +1,85 @@ +import CustomButton from '@/components/common/CustomButton'; +import { ValidatorInfo } from '@/types/staking'; +import React, { useEffect, useState } from 'react'; +import NewDelegationDialog from '../components/NewDelegationDialog'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { useRouter, useSearchParams } from 'next/navigation'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import useValidators from '@/custom-hooks/staking/useValidators'; +import { TxStatus } from '@/types/enums'; + +const NewDelegationButton = ({ chainID }: { chainID: string }) => { + const router = useRouter(); + const paramAction = useSearchParams().get('action'); + const paramValidator = useSearchParams().get('validator_address'); + const { getValidatorInfoByAddress } = useValidators(); + + const { getChainInfo } = useGetChainInfo(); + const { chainName } = getChainInfo(chainID); + const [selectedValidator, setSelectedValidator] = + useState(null); + const [newDelegationOpen, setNewDelegationOpen] = useState(false); + + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + + const handleValidatorChange = (option: ValidatorInfo | null) => { + setSelectedValidator(option); + if (option?.address) { + router.push(`?validator_address=${option.address}&action=new_delegation`); + } else { + router.push(`?action=new_delegation`); + } + }; + + const handleCloseNewDelegation = () => { + setNewDelegationOpen(false); + setSelectedValidator(null); + router.push(`/staking/${chainName.toLowerCase()}`); + }; + + const handleOpenNewDelegation = () => { + setNewDelegationOpen(true); + }; + + useEffect(() => { + if (paramAction === 'new_delegation') { + handleOpenNewDelegation(); + if (validatorsLoading === TxStatus.IDLE) { + if (paramValidator?.length) { + const validatorInfo = getValidatorInfoByAddress({ + chainID, + address: paramValidator, + }); + if (validatorInfo) { + setSelectedValidator(validatorInfo); + } else { + router.push(`?action=new_delegation`); + } + } + } + } + }, [paramAction, validatorsLoading]); + + return ( + <> + { + handleOpenNewDelegation(); + router.push(`?action=new_delegation`); + }} + /> + + + ); +}; + +export default NewDelegationButton; diff --git a/frontend/src/app/(routes)/staking/[network]/SingleChain.tsx b/frontend/src/app/(routes)/staking/[network]/SingleChain.tsx new file mode 100644 index 000000000..05e72dea6 --- /dev/null +++ b/frontend/src/app/(routes)/staking/[network]/SingleChain.tsx @@ -0,0 +1,48 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import React from 'react'; +import SingleStakingDashboard from './SingleChainDashboard'; + +function SingleChain({ + paramChain, + // queryParams, +}: { + paramChain: string; + // queryParams?: { [key: string]: string | undefined }; +}) { + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + const allNameToChainIDs = useAppSelector( + (state: RootState) => state.common.nameToChainIDs + ); + const connectedNameToChainIDs = useAppSelector( + (state: RootState) => state.wallet.nameToChainIDs + ); + const nameToChainIDs = isWalletConnected + ? connectedNameToChainIDs + : allNameToChainIDs; + + const validChain = Object.keys(nameToChainIDs).some( + (chain) => paramChain.toLowerCase() === chain.toLowerCase() + ); + + const chainID = nameToChainIDs[paramChain]; + + // const validatorAddress = queryParams?.validator_address || ''; + // const action = queryParams?.action || ''; + + return ( +
    + {validChain ? ( + + ) : ( + <> +
    + - Chain not found - +
    + + )} +
    + ); +} + +export default SingleChain; diff --git a/frontend/src/app/(routes)/staking/[network]/SingleChainDashboard.tsx b/frontend/src/app/(routes)/staking/[network]/SingleChainDashboard.tsx new file mode 100644 index 000000000..bd4a21b74 --- /dev/null +++ b/frontend/src/app/(routes)/staking/[network]/SingleChainDashboard.tsx @@ -0,0 +1,68 @@ +'use client'; + +import useSingleStaking from '@/custom-hooks/useSingleStaking'; +import StakingSummary from '../components/StakingSummary'; +import StakingUnDelegations from '../components/StakingUnDelegations'; +import StakingDelegations from '../components/StakingDelegations'; +import ValidatorTable from '../components/ValidatorTable'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import PageHeader from '@/components/common/PageHeader'; +import NewDelegationButton from './NewDelegationButton'; + +const SingleStakingDashboard = ({ chainID }: { chainID: string }) => { + const staking = useSingleStaking(chainID); + const { + totalStakedAmount, + rewardsAmount, + totalUnStakedAmount, + availableAmount, + } = staking.getStakingAssets(); + const delegations = staking.getAllDelegations(chainID); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const stakingData = useAppSelector((state) => state.staking.chains); + const authzStakingData = useAppSelector( + (state) => state.staking.authz.chains + ); + const hasUnbondings = isAuthzMode + ? authzStakingData[chainID]?.unbonding?.hasUnbonding + : stakingData[chainID]?.unbonding.hasUnbonding; + + return ( +
    +
    +
    +
    + +
    + +
    + + +
    + + {/* Delegations */} + + + {/* Unbonding */} + {hasUnbondings ? ( + + ) : null} + + {/* Validator */} + +
    + ); +}; +export default SingleStakingDashboard; diff --git a/frontend/src/app/(routes)/staking/[network]/error.tsx b/frontend/src/app/(routes)/staking/[network]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/staking/[network]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/staking/[network]/loading.tsx b/frontend/src/app/(routes)/staking/[network]/loading.tsx new file mode 100644 index 000000000..169002929 --- /dev/null +++ b/frontend/src/app/(routes)/staking/[network]/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import React from 'react'; +import StakingLoading from '../components/StakingLoading'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/staking/[network]/page.tsx b/frontend/src/app/(routes)/staking/[network]/page.tsx index da3fe0f7f..69f382a40 100644 --- a/frontend/src/app/(routes)/staking/[network]/page.tsx +++ b/frontend/src/app/(routes)/staking/[network]/page.tsx @@ -1,17 +1,21 @@ +'use client'; + import React from 'react'; import '../staking.css'; -import ChainStaking from './ChainStaking'; +import SingleChain from './SingleChain'; +// import ChainStaking from './ChainStaking'; const page = ({ params, - searchParams, + // searchParams, }: { params: { network: string }; - searchParams?: { [key: string]: string | undefined }; + // searchParams?: { [key: string]: string | undefined }; }) => { const { network: paramChain } = params; - return ; + return ; + // return ; }; export default page; diff --git a/frontend/src/app/(routes)/staking/components/ActiveValidators.tsx b/frontend/src/app/(routes)/staking/components/ActiveValidators.tsx deleted file mode 100644 index 9d635bda2..000000000 --- a/frontend/src/app/(routes)/staking/components/ActiveValidators.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { StakingMenuAction, Validators } from '@/types/staking'; -import { VALIDATORS_PER_PAGE } from '@/utils/constants'; -import { getValidatorRank } from '@/utils/util'; -import { Pagination } from '@mui/material'; -import React, { useEffect, useState } from 'react'; -import ValidatorComponent from './ValidatorComponent'; -import FilteredValidators from './FilteredValidators'; -import { paginationComponentStyles } from '../styles'; - -interface ActiveValidators { - validators: Validators; - searchTerm: string; - onMenuAction: StakingMenuAction; -} - -const ActiveValidators = ({ - validators, - searchTerm, - onMenuAction, -}: ActiveValidators) => { - const [slicedValidators, setSlicedValidators] = useState([]); - const [filtered, setFiltered] = useState([]); - - useEffect(() => { - if (validators?.activeSorted) { - if (validators.activeSorted.length < VALIDATORS_PER_PAGE) { - setSlicedValidators(validators?.activeSorted); - } else { - setSlicedValidators( - validators?.activeSorted?.slice(0, 1 * VALIDATORS_PER_PAGE) - ); - } - } - }, [validators?.activeSorted]); - - useEffect(() => { - const filteredValidators = validators?.activeSorted.filter( - (validator) => - validators.active[validator]?.description.moniker - .toLowerCase() - .includes(searchTerm.toLowerCase()) - ); - setFiltered(filteredValidators); - }, [searchTerm, validators?.activeSorted]); - return ( - <> -
    - {searchTerm.length ? ( - <> - - - ) : ( - <> -
    - {slicedValidators?.map((validator) => { - const moniker = - validators.active[validator]?.description.moniker; - const identity = - validators.active[validator]?.description.identity; - const commission = - Number( - validators.active[validator]?.commission?.commission_rates - .rate - ) * 100; - const jailed = validators.active[validator]?.jailed; - const status = validators.active[validator]?.status; - const rank = getValidatorRank( - validator, - validators.activeSorted - ); - - return ( - - ); - })} -
    -
    -
    - { - setSlicedValidators( - validators?.activeSorted?.slice( - (v - 1) * VALIDATORS_PER_PAGE, - v * VALIDATORS_PER_PAGE - ) - ); - }} - /> -
    - - )} -
    - - ); -}; - -export default ActiveValidators; diff --git a/frontend/src/app/(routes)/staking/components/AddressField.tsx b/frontend/src/app/(routes)/staking/components/AddressField.tsx new file mode 100644 index 000000000..2dcd48d5d --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/AddressField.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import InputField from './InputField'; + +const AddressField = ({ + quickSelectAmount, + availableAmount, + denom, + onChange, + value, + balanceTypeText, +}: { + quickSelectAmount: (value: number) => void; + availableAmount?: number; + denom?: string; + onChange: (event: React.ChangeEvent) => void; + value: number; + balanceTypeText: string; +}) => { + return ( +
    +
    +
    +
    {denom}
    +
    + +
    +
    + + +
    +
    +
    + {balanceTypeText} Balance {availableAmount?.toFixed(6)} {denom} +
    +
    +
    + ); +}; +export default AddressField; diff --git a/frontend/src/app/(routes)/staking/components/AllValidators.tsx b/frontend/src/app/(routes)/staking/components/AllValidators.tsx deleted file mode 100644 index 02624f1eb..000000000 --- a/frontend/src/app/(routes)/staking/components/AllValidators.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { TxStatus } from '@/types/enums'; -import { AllValidatorsProps } from '@/types/staking'; -import { CircularProgress } from '@mui/material'; -import React from 'react'; -import ValidatorItem from './ValidatorItem'; -import DialogAllValidators from './DialogAllValidators'; - -const AllValidators = ({ - validators, - currency, - onMenuAction, - validatorsStatus, - allValidatorsDialogOpen, - toggleValidatorsDialog, -}: AllValidatorsProps) => { - const slicedValidatorsList = validators?.activeSorted.slice(0, 15) || []; - return ( -
    -
    -

    - All Validators -

    - {validatorsStatus === TxStatus.IDLE ? ( -
    - View All -
    - ) : null} -
    - {validatorsStatus === TxStatus.PENDING ? ( -
    - -
    - ) : ( - <> - {slicedValidatorsList.map((validator) => { - const { moniker, identity } = - validators.active[validator]?.description; - const commission = - Number( - validators.active[validator]?.commission?.commission_rates.rate - ) * 100; - const tokens = Number(validators.active[validator]?.tokens); - return ( - - ); - })} - - )} - -
    - ); -}; - -export default AllValidators; diff --git a/frontend/src/app/(routes)/staking/components/AmountInputField.tsx b/frontend/src/app/(routes)/staking/components/AmountInputField.tsx deleted file mode 100644 index 311518e64..000000000 --- a/frontend/src/app/(routes)/staking/components/AmountInputField.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { INSUFFICIENT_BALANCE } from '@/utils/errors'; -import { InputAdornment, TextField } from '@mui/material'; -import React, { useState } from 'react'; -import { Control, Controller, FieldErrors } from 'react-hook-form'; -import { textFieldInputPropStyles, textFieldStyles } from '../styles'; - -interface AmountInputField { - /* eslint-disable @typescript-eslint/no-explicit-any */ - control: Control; - availableAmount: number; - displayDenom: string; - errors: FieldErrors<{ - amount: string; - }>; - setValue: any; - feeAmount: number; -} - -const AmountInputField: React.FC = (props) => { - const { - availableAmount, - control, - displayDenom, - errors, - setValue, - feeAmount, - } = props; - const [amountOption, setAmountOption] = useState(''); - - return ( - <> - { - const amount = Number(value); - if (isNaN(amount) || amount <= 0) return 'Invalid Amount'; - if (amount > availableAmount) return INSUFFICIENT_BALANCE; - }, - }} - render={({ field }) => ( - -
    - - -
    - - {displayDenom} - -
    - ), - sx: textFieldInputPropStyles, - }} - /> - )} - /> -
    - - {errors.amount?.message} - -
    - - ); -}; - -export default AmountInputField; diff --git a/frontend/src/app/(routes)/staking/components/ChainDelegations.tsx b/frontend/src/app/(routes)/staking/components/ChainDelegations.tsx deleted file mode 100644 index 6742eb721..000000000 --- a/frontend/src/app/(routes)/staking/components/ChainDelegations.tsx +++ /dev/null @@ -1,292 +0,0 @@ -'use client'; -import { - ChainDelegationsProps, - DelegateTxInputs, - RedelegateTxInputs, - UndelegateTxInputs, - Validator, -} from '@/types/staking'; -import React, { useEffect, useState } from 'react'; -import StakingCard from './StakingCard'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { useRouter } from 'next/navigation'; -import { - txDelegate, - txReDelegate, - txUnDelegate, -} from '@/store/features/staking/stakeSlice'; -import DialogDelegate from './DialogDelegate'; -import { parseBalance } from '@/utils/denom'; -import DialogUndelegate from './DialogUndelegate'; -import DialogRedelegate from './DialogRedelegate'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { parseDenomAmount } from '@/utils/util'; -import { TxStatus } from '@/types/enums'; - -const ChainDelegations = ({ - chainID, - delegations, - validators, - currency, - chainName, - rewards, - validatorAddress, - action, - chainSpecific, -}: ChainDelegationsProps) => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const { getChainInfo } = useGetChainInfo(); - - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const networkLogo = networks[chainID]?.network.logos.menu; - - const [validatorRewards, setValidatorRewards] = React.useState<{ - [key: string]: number; - }>({}); - - const txStatus = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.tx - ); - const balance = useAppSelector( - (state: RootState) => state.bank.balances[chainID] - ); - const stakingParams = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.params - ); - - const [availableBalance, setAvailableBalance] = useState(0); - const [delegateOpen, setDelegateOpen] = useState(false); - const [undelegateOpen, setUndelegateOpen] = useState(false); - const [redelegateOpen, setRedelegateOpen] = useState(false); - const [selectedValidator, setSelectedValidator] = useState(); - const [processingValAddr, setProcessingValAddr] = useState(''); - - const handleCardClick = (valAddr: string) => { - setProcessingValAddr(valAddr); - }; - - const handleDialogClose = () => { - if (chainSpecific) { - router.push(`/staking/${chainName.toLowerCase()}`); - } - setDelegateOpen(false); - setUndelegateOpen(false); - setRedelegateOpen(false); - }; - - const onMenuAction = (type: string, validator: Validator) => { - const valAddress = validator?.operator_address; - - setSelectedValidator(validator); - - switch (type) { - case 'delegate': - setDelegateOpen(true); - break; - case 'undelegate': - setUndelegateOpen(true); - break; - case 'redelegate': - setRedelegateOpen(true); - break; - default: - console.log('unsupported type'); - } - if (chainSpecific) { - router.push(`?validator_address=${valAddress}&action=${type}`); - } - }; - - const allChainInfo = networks[chainID]; - const chainInfo = allChainInfo?.network; - const { feeAmount: avgFeeAmount, address } = getChainInfo(chainID); - const feeAmount = avgFeeAmount * 10 ** currency?.coinDecimals; - const [allValidators, setAllValidators] = useState>( - {} - ); - - const onDelegateTx = (data: DelegateTxInputs) => { - dispatch( - txDelegate({ - basicChainInfo: getChainInfo(chainID), - delegator: address, - validator: data.validator, - amount: data.amount * 10 ** currency.coinDecimals, - denom: currency.coinMinimalDenom, - feeAmount: feeAmount, - feegranter: '', - }) - ); - }; - - const onUndelegateTx = (data: UndelegateTxInputs) => { - dispatch( - txUnDelegate({ - basicChainInfo: getChainInfo(chainID), - delegator: address, - validator: data.validator, - amount: data.amount * 10 ** currency.coinDecimals, - denom: currency.coinMinimalDenom, - feeAmount: feeAmount, - feegranter: '', - }) - ); - }; - - const onRedelegateTx = (data: RedelegateTxInputs) => { - dispatch( - txReDelegate({ - basicChainInfo: getChainInfo(chainID), - delegator: address, - srcVal: data.src, - destVal: data.dest, - amount: data.amount * 10 ** currency.coinDecimals, - denom: currency.coinMinimalDenom, - feeAmount: feeAmount, - feegranter: '', - }) - ); - }; - - useEffect(() => { - if (chainInfo.config.currencies?.length) { - setAvailableBalance( - parseBalance( - balance?.list?.length ? balance.list : [], - currency.coinDecimals, - currency.coinMinimalDenom - ) - ); - } - }, [balance]); - - useEffect(() => { - if (rewards?.length) { - for (let i = 0; i < rewards.length; i++) { - if (rewards[i].reward.length > 0) { - const reward = rewards[i].reward; - for (let j = 0; j < reward.length; j++) { - if (reward[j].denom === currency.coinMinimalDenom) { - const valReward = validatorRewards; - valReward[rewards[i].validator_address] = parseDenomAmount( - reward[j].amount, - currency?.coinDecimals - ); - setValidatorRewards((prevState) => ({ - ...prevState, - ...valReward, - })); - } - } - } else { - const valReward = validatorRewards; - valReward[rewards[i].validator_address] = 0.0; - setValidatorRewards((prevState) => ({ ...prevState, ...valReward })); - } - } - } - }, [rewards]); - - useEffect(() => { - if (validatorAddress.length && action.length && validators?.active) { - const validatorInfo = - validators.active[validatorAddress] || - validators.inactive[validatorAddress] || - {}; - const validatorExist = Object.keys(validatorInfo).length ? true : false; - setSelectedValidator(validatorInfo); - if (action === 'delegate' && validatorExist) { - setDelegateOpen(true); - } else if (action === 'undelegate' && validatorExist) { - setUndelegateOpen(true); - } else if (action === 'redelegate' && validatorExist) { - setRedelegateOpen(true); - } - } - }, [validatorAddress, action, validators]); - - useEffect(() => { - setAllValidators({ ...validators?.active, ...validators?.inactive }); - }, [validators]); - - useEffect(() => { - if (txStatus?.status === TxStatus.IDLE) { - handleDialogClose(); - } - }, [txStatus?.status]); - - return ( - <> - {delegations?.delegation_responses.map((row, index) => ( - - ))} - - - - - ); -}; - -export default ChainDelegations; diff --git a/frontend/src/app/(routes)/staking/components/ChainUnbondings.tsx b/frontend/src/app/(routes)/staking/components/ChainUnbondings.tsx deleted file mode 100644 index 5d1d755bc..000000000 --- a/frontend/src/app/(routes)/staking/components/ChainUnbondings.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { ChainUnbondingsProps, Validator } from '@/types/staking'; -import React, { useEffect, useState } from 'react'; -import UnbondingCard from './UnbondingCard'; -import { parseDenomAmount } from '@/utils/util'; - -//TODO: Add cancelUnbondingDelegation msg and reducer - -const ChainUnbondings = ({ - chainID, - unbondings, - validators, - currency, - chainName, -}: ChainUnbondingsProps) => { - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const networkLogo = networks[chainID]?.network?.logos?.menu; - const [allValidators, setAllValidators] = useState>( - {} - ); - - useEffect(() => { - setAllValidators({ ...validators?.active, ...validators?.inactive }); - }, [validators]); - - return ( - <> - {unbondings?.unbonding_responses?.map((row) => { - const entries = row.entries; - return entries.map((entry) => { - return ( - - ); - }); - })} - - ); -}; - -export default ChainUnbondings; diff --git a/frontend/src/app/(routes)/staking/components/DelegatePopup.tsx b/frontend/src/app/(routes)/staking/components/DelegatePopup.tsx new file mode 100644 index 000000000..6ba620df6 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/DelegatePopup.tsx @@ -0,0 +1,145 @@ +'use client'; + +import CustomDialog from '@/components/common/CustomDialog'; +import { useState } from 'react'; +import Image from 'next/image'; +import AddressField from './AddressField'; +import useStaking from '@/custom-hooks/useStaking'; +import { get } from 'lodash'; +import useSingleStaking from '@/custom-hooks/useSingleStaking'; +import ValidatorName from './ValidatorName'; + +interface PopupProps { + validator: string; + chainID: string; + openPopup: boolean; + onClose: () => void; + commission: number; +} + +const DelegatePopup: React.FC = ({ + validator, + commission, + chainID, + openPopup, + onClose, +}) => { + // Local state to manage the amount and the open status of the dialog + const [amount, setAmount] = useState(0); + const [open, setOpen] = useState(openPopup); + + // Custom hook to get single staking information based on chainID + const singleStake = useSingleStaking(chainID); + + // Get the available staking assets and denomination + // const { availableAmount } = singleStake.getStakingAssets(); + const denom = singleStake.getDenomWithChainID(chainID); + + // Custom hook to get staking information + const staking = useStaking(); + + // Get the current validator's information from the staking module + const stakeModule = staking.getAllDelegations(); + let val = stakeModule[chainID]?.validators?.active?.[validator]; + if (!val) { + val = stakeModule[chainID]?.validators?.inactive?.[validator]; + } + + const availableAmount = singleStake.getAvaiailableAmount(chainID); + + // Calculate the commission rate for the validator + const getCommisionRate = () => { + return Number(get(val, 'commission.commission_rates.rate', 0)) * 100; + }; + + // Handler for input change to set the amount + const onChange = (event: React.ChangeEvent) => + setAmount(Number(event.target.value)); + + // Handler for quick select amount + const onChangeAmount = (value: number) => { + setAmount(Number((value * availableAmount).toFixed(6))); + }; + + // Function to perform the delegation transaction + const doTxDelegate = () => { + staking.txDelegateTx(validator, amount, chainID); + }; + + // Status of the delegation transaction + const delegteStatus = staking.txAllChainStakeTxStatus[chainID]?.tx?.status; + + return ( + { + onClose(); + setOpen(false); + }} + title="Delegate" + description="Delegate Your Tokens and Manage Your Stake for Optimal Performance and Rewards" + > +
    + {/* Validator details */} +
    +
    + +
    +
    +

    + {get(val, 'description.details', '-')} +

    +

    + {commission ? commission : getCommisionRate()}% Commission +

    +
    +
    +
    + + {/* Address field for the amount */} + + + {/* Staking alert */} +
    +
    + info-icon +

    Important

    +

    + Staking will lock your funds for 21 days. +

    +
    +
    + To make your staked assets liquid, undelegation will take 21 days. +
    +
    + + {/* Delegate button */} + +
    +
    + ); +}; + +export default DelegatePopup; diff --git a/frontend/src/app/(routes)/staking/components/DialogAllValidators.tsx b/frontend/src/app/(routes)/staking/components/DialogAllValidators.tsx deleted file mode 100644 index 75e4ef549..000000000 --- a/frontend/src/app/(routes)/staking/components/DialogAllValidators.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { StakingMenuAction, Validators } from '@/types/staking'; -import { Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React, { useState } from 'react'; -import ActiveValidators from './ActiveValidators'; -import InactiveValidators from './InactiveValidators'; -import { CLOSE_ICON_PATH } from '@/utils/constants'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -interface DialogAllValidatorsProps { - toggleValidatorsDialog: () => void; - open: boolean; - validators: Validators; - onMenuAction: StakingMenuAction; -} - -const DialogAllValidators = ({ - toggleValidatorsDialog, - open, - validators, - onMenuAction, -}: DialogAllValidatorsProps) => { - const [searchTerm, setSearchTerm] = useState(''); - const [active, setActive] = useState(true); - - return ( - { - setSearchTerm(''); - toggleValidatorsDialog(); - setActive(true); - }} - maxWidth="lg" - PaperProps={{ - sx: dialogBoxPaperPropStyles, - }} - > - -
    -
    -
    { - setSearchTerm(''); - toggleValidatorsDialog(); - }} - > - Close -
    -
    -
    -

    All Validators

    -
    -
    -
    setActive(true)} - > -
    - {active ? ( -
    - ) : null} -
    -
    Active
    -
    -
    setActive(false)} - > -
    - {!active ? ( -
    - ) : null} -
    -
    Inactive
    -
    -
    -
    -
    -
    - Search -
    -
    - setSearchTerm(e.target.value)} - autoFocus={true} - /> -
    -
    -
    - {active ? ( - - ) : ( - - )} -
    -
    -
    - ); -}; - -export default DialogAllValidators; diff --git a/frontend/src/app/(routes)/staking/components/DialogDelegate.tsx b/frontend/src/app/(routes)/staking/components/DialogDelegate.tsx deleted file mode 100644 index 6980c34ce..000000000 --- a/frontend/src/app/(routes)/staking/components/DialogDelegate.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { DialogDelegateProps } from '@/types/staking'; -import { formatCoin, formatUnbondingPeriod } from '@/utils/util'; -import { CircularProgress, Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React, { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { CLOSE_ICON_PATH } from '@/utils/constants'; -import AmountInputField from './AmountInputField'; -import ValidatorLogo from './ValidatorLogo'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -const DialogDelegate = ({ - open, - onClose, - validator, - stakingParams, - availableBalance, - loading, - displayDenom, - onDelegate, - feeAmount, -}: DialogDelegateProps) => { - const handleClose = () => { - onClose(); - reset(); - }; - - const { - handleSubmit, - control, - setValue, - formState: { errors }, - reset, - } = useForm({ - defaultValues: { - amount: '', - }, - }); - - const onSubmit = (data: { amount: string }) => { - if (validator) { - onDelegate({ - validator: validator?.operator_address || '', - amount: Number(data?.amount) || 0, - }); - } - }; - - useEffect(() => { - if (!open) { - reset(); - } - }, [open]); - - return ( - - -
    -
    -
    - Close -
    -
    -
    -
    -
    - -

    - {validator?.description?.moniker || '-'} -

    -
    -
    -
    -
    -
    - Commission -
    -
    - {Number(validator?.commission?.commission_rates?.rate) * - 100} - % -
    -
    -
    -
    - Available for Delegation -
    -
    - {formatCoin(availableBalance, displayDenom)} -
    -
    -
    -
    -
    -
    -

    Staking will lock your

    -

    - funds for {formatUnbondingPeriod(stakingParams)} days -

    -
    -
    -

    - You will need to undelegate in order for your staked - assets to be liquid again.{' '} -

    -

    - This process will take{' '} - {formatUnbondingPeriod(stakingParams)} days to complete. -

    -
    -
    -
    -
    - -
    - - -
    - -
    -
    -
    -
    -
    -
    - ); -}; - -export default DialogDelegate; diff --git a/frontend/src/app/(routes)/staking/components/DialogRedelegate.tsx b/frontend/src/app/(routes)/staking/components/DialogRedelegate.tsx deleted file mode 100644 index 14de341e5..000000000 --- a/frontend/src/app/(routes)/staking/components/DialogRedelegate.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { DialogRedelegateProps, Validator } from '@/types/staking'; -import { - formatCoin, - formatUnbondingPeriod, - parseDelegation, -} from '@/utils/util'; -import { - Autocomplete, - CircularProgress, - Dialog, - DialogContent, - TextField, -} from '@mui/material'; -import Image from 'next/image'; -import React, { useEffect } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { CLOSE_ICON_PATH } from '@/utils/constants'; -import AmountInputField from './AmountInputField'; -import ValidatorLogo from './ValidatorLogo'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -interface ValidatorSet { - [key: string]: Validator; -} - -interface ValidatorInfo { - addr: string; - label: string; -} - -function parseValidators({ - active, - inactive, - validator, -}: { - active: ValidatorSet; - inactive: ValidatorSet; - validator: Validator | undefined; -}) { - let result: ValidatorInfo[] = []; - - for (const v in active) { - if (active[v]) { - result = [...result, { addr: v, label: active[v]?.description?.moniker }]; - } - } - - for (const v in inactive) { - if (inactive[v] && v !== validator?.operator_address) - result.push({ - addr: v, - label: inactive[v].description.moniker, - }); - } - - return result; -} - -const DialogRedelegate = ({ - open, - onClose, - validator, - stakingParams, - loading, - active, - inactive, - delegations, - onRedelegate, - currency, -}: DialogRedelegateProps) => { - const handleClose = () => { - onClose(); - reset(); - }; - const targetValidators = parseValidators({ active, inactive, validator }); - const delegationShare = parseDelegation({ delegations, validator, currency }); - - const { - handleSubmit, - control, - setValue, - formState: { errors }, - reset, - } = useForm({ - defaultValues: { - amount: '', - destination: null, - }, - }); - const onSubmit = (data: { - amount: string; - destination: null | { addr: string; label: string }; - }) => { - if (validator && data.destination) { - onRedelegate({ - amount: Number(data.amount) || 0, - dest: data?.destination?.addr || '', - src: validator?.operator_address || '', - }); - } - }; - - useEffect(() => { - if (!open) { - reset(); - } - }, [open]); - - return ( - - -
    -
    -
    - Close -
    -
    -
    -
    -
    - -

    - {validator?.description?.moniker || '-'} -

    -
    -
    -
    -
    -
    - Commission -
    -
    - {Number(validator?.commission?.commission_rates?.rate) * - 100} - % -
    -
    -
    -
    - Available for Delegation -
    -
    - {formatCoin(delegationShare, currency.coinDenom)} -
    -
    -
    -
    -
    -
    -

    Staking will lock your

    -

    - funds for {formatUnbondingPeriod(stakingParams)} days -

    -
    -
    -

    - You will need to undelegate in order for your staked - assets to be liquid again.{' '} -

    -

    - This process will take{' '} - {formatUnbondingPeriod(stakingParams)} days to complete. -

    -
    -
    -
    -
    -
    - ( - - option.addr === value.addr - } - options={targetValidators} - onChange={(event, item) => { - onChange(item); - }} - sx={{ - '& .MuiAutocomplete-inputRoot': { - padding: '12px !important', - '& input': { - color: 'white', - }, - '& button': { - color: 'white', - }, - }, - '& .MuiAutocomplete-popper': { - display: 'none !important', - }, - }} - renderInput={(params) => ( - - )} - /> - )} - /> -
    - -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    - ); -}; - -export default DialogRedelegate; diff --git a/frontend/src/app/(routes)/staking/components/DialogUndelegate.tsx b/frontend/src/app/(routes)/staking/components/DialogUndelegate.tsx deleted file mode 100644 index dd082ae1b..000000000 --- a/frontend/src/app/(routes)/staking/components/DialogUndelegate.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { DialogUndelegateProps } from '@/types/staking'; -import { - formatCoin, - formatUnbondingPeriod, - parseDelegation, -} from '@/utils/util'; -import { CircularProgress, Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import React, { useEffect } from 'react'; -import { useForm } from 'react-hook-form'; -import { CLOSE_ICON_PATH } from '@/utils/constants'; -import AmountInputField from './AmountInputField'; -import ValidatorLogo from './ValidatorLogo'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -const DialogUndelegate = ({ - open, - onClose, - validator, - stakingParams, - onUndelegate, - loading, - delegations, - currency, -}: DialogUndelegateProps) => { - const handleClose = () => { - onClose(); - reset(); - }; - - const delegationShare = parseDelegation({ delegations, validator, currency }); - const { - handleSubmit, - control, - setValue, - formState: { errors }, - reset, - } = useForm({ - defaultValues: { - amount: '', - }, - }); - - const onSubmit = (data: { amount: string }) => { - if (validator) { - onUndelegate({ - validator: validator?.operator_address || '', - amount: Number(data?.amount) || 0, - }); - } - }; - - useEffect(() => { - if (!open) { - reset(); - } - }, [open]); - - return ( - - -
    -
    -
    - Close -
    -
    -
    -
    -
    - -

    - {validator?.description?.moniker || '-'} -

    -
    -
    -
    -
    -
    - Commission -
    -
    - {Number(validator?.commission?.commission_rates?.rate) * - 100} - % -
    -
    -
    -
    - Available for Undelegation -
    -
    - {formatCoin(delegationShare, currency.coinDenom)} -
    -
    -
    -
    -
    -
    -

    Undelegating will lock

    -

    - your funds for {formatUnbondingPeriod(stakingParams)}{' '} - days -

    -
    -
    -
      -
    1. You will not receive staking rewards.
    2. -
    3. You will not be able to cancel the unbonding.
    4. -
    5. - You will not be able to withdraw your funds until{' '} - {formatUnbondingPeriod(stakingParams)}+ days after the - undelegation. -
    6. -
    -
    -
    -
    -
    - -
    - - -
    - -
    -
    -
    -
    -
    -
    - ); -}; - -export default DialogUndelegate; diff --git a/frontend/src/app/(routes)/staking/components/FilteredValidators.tsx b/frontend/src/app/(routes)/staking/components/FilteredValidators.tsx deleted file mode 100644 index 85bc9c809..000000000 --- a/frontend/src/app/(routes)/staking/components/FilteredValidators.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { StakingMenuAction, Validators } from '@/types/staking'; -import { VALIDATORS_PER_PAGE } from '@/utils/constants'; -import { getValidatorRank } from '@/utils/util'; -import React, { useEffect, useState } from 'react'; -import ValidatorComponent from './ValidatorComponent'; -import { Pagination } from '@mui/material'; -import { paginationComponentStyles } from '../styles'; - -interface FilteredValidatorsProps { - filtered: string[]; - validators: Validators; - active: boolean; - onMenuAction: StakingMenuAction; -} - -const FilteredValidators = ({ - filtered, - validators, - active, - onMenuAction, -}: FilteredValidatorsProps) => { - const [slicedValidators, setSlicedValidators] = useState([]); - - useEffect(() => { - if (filtered.length < VALIDATORS_PER_PAGE) { - setSlicedValidators(filtered); - } else { - setSlicedValidators(filtered?.slice(0, 1 * VALIDATORS_PER_PAGE)); - } - }, [filtered]); - - return ( - <> - {slicedValidators.length ? ( - <> -
    - {slicedValidators?.map((validatorAddress) => { - const validatorsSet = active - ? validators.active - : validators.inactive; - let rank; - if (active) { - rank = getValidatorRank( - validatorAddress, - validators.activeSorted - ); - } else { - rank = getValidatorRank(validatorAddress, [ - ...validators.activeSorted, - ...validators.inactiveSorted, - ]); - } - const { moniker, identity } = - validatorsSet[validatorAddress]?.description; - const commission = - Number( - validatorsSet[validatorAddress]?.commission?.commission_rates - .rate - ) * 100; - const jailed = validatorsSet[validatorAddress]?.jailed; - const status = validatorsSet[validatorAddress]?.status; - - return ( - - ); - })} -
    -
    -
    - { - setSlicedValidators( - filtered?.slice( - (v - 1) * VALIDATORS_PER_PAGE, - v * VALIDATORS_PER_PAGE - ) - ); - }} - /> -
    - - ) : ( -
    No validators found
    - )} - - ); -}; - -export default FilteredValidators; diff --git a/frontend/src/app/(routes)/staking/components/InactiveValidators.tsx b/frontend/src/app/(routes)/staking/components/InactiveValidators.tsx deleted file mode 100644 index d59ab6062..000000000 --- a/frontend/src/app/(routes)/staking/components/InactiveValidators.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { StakingMenuAction, Validators } from '@/types/staking'; -import { VALIDATORS_PER_PAGE } from '@/utils/constants'; -import { getValidatorRank } from '@/utils/util'; -import { Pagination } from '@mui/material'; -import React, { useEffect, useState } from 'react'; -import ValidatorComponent from './ValidatorComponent'; -import FilteredValidators from './FilteredValidators'; -import { paginationComponentStyles } from '../styles'; - -interface InactiveValidators { - validators: Validators; - searchTerm: string; - onMenuAction: StakingMenuAction; -} - -const InactiveValidators = ({ - validators, - searchTerm, - onMenuAction, -}: InactiveValidators) => { - const [slicedValidators, setSlicedValidators] = useState([]); - const [filtered, setFiltered] = useState([]); - - useEffect(() => { - if (validators?.activeSorted.length < VALIDATORS_PER_PAGE) { - setSlicedValidators(validators?.inactiveSorted); - } else { - setSlicedValidators( - validators?.inactiveSorted?.slice(0, 1 * VALIDATORS_PER_PAGE) - ); - } - }, [validators?.inactiveSorted]); - - useEffect(() => { - const filteredValidators = validators?.inactiveSorted.filter( - (validator) => - validators.inactive[validator]?.description.moniker - .toLowerCase() - .includes(searchTerm.toLowerCase()) - ); - setFiltered(filteredValidators); - }, [searchTerm, validators?.inactiveSorted]); - return ( - <> -
    - {searchTerm.length ? ( - <> - - - ) : ( - <> -
    - {slicedValidators?.map((validator) => { - const moniker = - validators.inactive[validator]?.description.moniker; - const identity = - validators.inactive[validator]?.description.identity; - const commission = - Number( - validators.inactive[validator]?.commission?.commission_rates - .rate - ) * 100; - const jailed = validators.inactive[validator]?.jailed; - const status = validators.inactive[validator]?.status; - const rank = getValidatorRank(validator, [ - ...validators.activeSorted, - ...validators.inactiveSorted, - ]); - - return ( - - ); - })} -
    -
    -
    - { - setSlicedValidators( - validators?.inactiveSorted?.slice( - (v - 1) * VALIDATORS_PER_PAGE, - v * VALIDATORS_PER_PAGE - ) - ); - }} - /> -
    - - )} -
    - - ); -}; - -export default InactiveValidators; diff --git a/frontend/src/app/(routes)/staking/components/InputField.tsx b/frontend/src/app/(routes)/staking/components/InputField.tsx new file mode 100644 index 000000000..88cf7b298 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/InputField.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import '../staking.css'; + +const InputField = ({ + value, + onChange, +}: { + value: number; + onChange: (event: React.ChangeEvent) => void; +}) => { + return ( + + ); +}; + +export default InputField; diff --git a/frontend/src/app/(routes)/staking/components/NewDelegationDialog.tsx b/frontend/src/app/(routes)/staking/components/NewDelegationDialog.tsx new file mode 100644 index 000000000..f954c18f0 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/NewDelegationDialog.tsx @@ -0,0 +1,142 @@ +'use client'; + +import CustomDialog from '@/components/common/CustomDialog'; +import { useState } from 'react'; +import Image from 'next/image'; +import AddressField from './AddressField'; +import useStaking from '@/custom-hooks/useStaking'; +import useSingleStaking from '@/custom-hooks/useSingleStaking'; +import useValidators from '@/custom-hooks/staking/useValidators'; +import { ValidatorInfo } from '@/types/staking'; +import ValidatorsAutoComplete from './ValidatorsAutoComplete'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { TxStatus } from '@/types/enums'; +import { setError } from '@/store/features/common/commonSlice'; + +interface PopupProps { + chainID: string; + open: boolean; + onClose: () => void; + selectedValidator: ValidatorInfo | null; + handleValidatorChange: (option: ValidatorInfo | null) => void; +} + +const NewDelegationDialog: React.FC = ({ + chainID, + open, + onClose, + handleValidatorChange, + selectedValidator, +}) => { + const dispatch = useAppDispatch(); + const { getValidators } = useValidators(); + const { validatorsList } = getValidators({ chainID }); + + // Local state to manage the amount and the open status of the dialog + const [amount, setAmount] = useState(0); + + // Custom hook to get single staking information based on chainID + const singleStake = useSingleStaking(chainID); + + // Get the available staking assets and denomination + // const { availableAmount } = singleStake.getStakingAssets(); + const denom = singleStake.getDenomWithChainID(chainID); + + // Custom hook to get staking information + const staking = useStaking(); + + const availableAmount = singleStake.getAvaiailableAmount(chainID); + + // Handler for input change to set the amount + const onChange = (event: React.ChangeEvent) => + setAmount(Number(event.target.value)); + + // Handler for quick select amount + const onChangeAmount = (value: number) => { + setAmount(Number((value * availableAmount).toFixed(6))); + }; + + // Function to perform the delegation transaction + const doTxDelegate = () => { + if (selectedValidator) { + if (!amount || amount <= 0) { + dispatch(setError({ type: 'error', message: 'Invalid amount' })); + return; + } + staking.txDelegateTx(selectedValidator?.address, amount, chainID); + } else { + dispatch( + setError({ type: 'error', message: 'Please select the validator' }) + ); + } + }; + + // Status of the delegation transaction + const delegteStatus = staking.txAllChainStakeTxStatus[chainID]?.tx?.status; + + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + + const handleDialogClose = () => { + onClose(); + }; + + return ( + +
    + {/* Validator details */} +
    +
    + +
    +
    + + {/* Address field for the amount */} + + + {/* Staking alert */} +
    +
    + info-icon +

    Important

    +

    + Staking will lock your funds for 21 days. +

    +
    +
    + To make your staked assets liquid, undelegation will take 21 days. +
    +
    + + {/* Delegate button */} + +
    +
    + ); +}; + +export default NewDelegationDialog; diff --git a/frontend/src/app/(routes)/staking/components/ReDelegatePopup.tsx b/frontend/src/app/(routes)/staking/components/ReDelegatePopup.tsx new file mode 100644 index 000000000..b6bc7f6df --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/ReDelegatePopup.tsx @@ -0,0 +1,242 @@ +'use client'; + +import CustomDialog from '@/components/common/CustomDialog'; +import { useState } from 'react'; +import Image from 'next/image'; +import AddressField from './AddressField'; +import useSingleStaking from '@/custom-hooks/useSingleStaking'; +import useStaking from '@/custom-hooks/useStaking'; +import { get } from 'lodash'; +import ValidatorLogo from './ValidatorLogo'; +import { WalletAddress } from '@/components/main-layout/SelectNetwork'; +import { Validator } from '@/types/staking'; +import ValidatorName from './ValidatorName'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { TxStatus } from '@/types/enums'; + +interface PopupProps { + validator: string; + chainID: string; + openPopup: boolean; + openReDelegatePopup: () => void; +} + +const ReDelegatePopup: React.FC = ({ + validator, + chainID, + openPopup, + openReDelegatePopup, +}) => { + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + // Local state to manage the amount, open status, and destination validator + const [amount, setAmount] = useState(0); + const [open, setOpen] = useState(openPopup); + const [destValidator, setDestValidator] = useState(); + + // Custom hooks to fetch staking details + const singleStake = useSingleStaking(chainID); + const totalStakedAmount = singleStake.totalValStakedAssets( + chainID, + validator + ); + const denom = singleStake.getDenomWithChainID(chainID); + const staking = useStaking(); + const allVals = singleStake.getValidators()?.active; + const stakeModule = staking.getAllDelegations(); + const val = stakeModule[chainID]?.validators?.active?.[validator]; + + // Function to get the commission rate of the validator + const getCommisionRate = () => { + return Number(get(val, 'commission.commission_rates.rate', 0)) * 100; + }; + + // Handler for input change to set the amount + const onChange = (event: React.ChangeEvent) => + setAmount(Number(event.target.value)); + + // Handler for quick select amount + const onChangeAmount = (value: number) => { + setAmount(Number((value * totalStakedAmount).toFixed(6))); + }; + + // Function to perform the re-delegation transaction + const doTxDelegate = () => { + staking.txReDelegateTx( + validator, + destValidator?.operator_address || '', + amount, + chainID + ); + }; + + // Status of the delegation transaction + const delegteStatus = staking.txAllChainStakeTxStatus[chainID]?.tx?.status; + + // State to manage the dropdown visibility + const [isOpen, setIsOpen] = useState(false); + + // Toggle the dropdown visibility + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + return ( + <> + { + setOpen(false); + openReDelegatePopup(); + }} + title="Re-Delegate" + description="Redelegate lets you move staked tokens from one validator to another without unstaking" + > +
    + {/* Validator details */} +
    +
    + +
    +
    +

    + {get(val, 'description.details', '-')} +

    +

    + {getCommisionRate()}% Commission +

    +
    +
    +
    + + {/* Address field for the amount */} + + + {/* Destination validator selection */} +
    +

    Destination Validator

    +
    + + + {isOpen && ( +
    +
    + {Object.entries(allVals || {}).map(([key, value]) => ( + + ))} +
    +
    + )} +
    +
    + + {/* Staking alert */} +
    +
    + info-icon +

    Important

    +

    + Staking will lock your funds for 21 days +

    +
    +
    + No staking rewards, cancellation of unbonding, or fund withdrawals + until 21+ days post-undelegation. +
    +
    + + {/* Re-Delegate button */} + +
    +
    + + ); +}; + +export default ReDelegatePopup; diff --git a/frontend/src/app/(routes)/staking/components/SearchValidator.tsx b/frontend/src/app/(routes)/staking/components/SearchValidator.tsx new file mode 100644 index 000000000..9b382ede9 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/SearchValidator.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Image from 'next/image'; + +const SearchValidator = ({ + searchQuery, + handleSearchQueryChange, +}: { + searchQuery: string; + handleSearchQueryChange: (e: React.ChangeEvent) => void; +}) => { + return ( +
    + + +
    + ); +}; + +export default SearchValidator; diff --git a/frontend/src/app/(routes)/staking/components/StakingActionsMenu.tsx b/frontend/src/app/(routes)/staking/components/StakingActionsMenu.tsx deleted file mode 100644 index cb096e5fe..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingActionsMenu.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -const StakingActionsMenu = ({ - handleMenuAction, -}: { - handleMenuAction: (type: string) => void; -}) => { - const actions = ['Undelegate', 'Redelegate']; - return ( -
    -
    handleMenuAction('delegate')} - > - Delegate -
    - {actions.map((action, index) => ( -
    handleMenuAction(action.toLowerCase())} - > - {action} -
    - ))} -
    - ); -}; - -export default StakingActionsMenu; diff --git a/frontend/src/app/(routes)/staking/components/StakingCard.tsx b/frontend/src/app/(routes)/staking/components/StakingCard.tsx deleted file mode 100644 index 0d236c4f5..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingCard.tsx +++ /dev/null @@ -1,308 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import React, { useEffect, useRef, useState } from 'react'; -import StakingActionsMenu from './StakingActionsMenu'; -import StakingCardStats from './StakingCardStats'; -import { CircularProgress, Tooltip } from '@mui/material'; -import ValidatorLogo from './ValidatorLogo'; -import { - StakingCardActionButtonProps, - StakingCardActionsProps, - StakingCardHeaderProps, - StakingCardProps, -} from '@/types/staking'; -import { capitalizeFirstLetter } from '@/utils/util'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import useGetTxInputs from '@/custom-hooks/useGetTxInputs'; -import { TxStatus } from '@/types/enums'; -import { txWithdrawAllRewards } from '@/store/features/distribution/distributionSlice'; -import { txRestake } from '@/store/features/staking/stakeSlice'; -import Link from 'next/link'; -import { setError } from '@/store/features/common/commonSlice'; -import { - NO_DELEGATIONS_ERROR, - NO_REWARDS_ERROR, - TXN_PENDING_ERROR, -} from '@/utils/errors'; - -const StakingCard = ({ - processingValAddr, - handleCardClick, - validator, - identity, - chainName, - commission, - delegated, - networkLogo, - rewards, - coinDenom, - onMenuAction, - validatorInfo, - chainID, -}: StakingCardProps) => { - const [isMenuOpen, setMenuOpen] = useState(false); - const menuRef = useRef(null); - const menuRef2 = useRef(null); - - const toggleMenu = () => { - setMenuOpen(!isMenuOpen); - }; - - const handleMenuAction = (type: string) => { - onMenuAction(type, validatorInfo); - }; - - const validatorAddress = validatorInfo?.operator_address; - - useEffect(() => { - const handleOutsideClick = (event: MouseEvent) => { - if ( - menuRef.current && - !menuRef.current.contains(event.target as Node) && - menuRef2.current && - !menuRef2.current.contains(event.target as Node) - ) { - setMenuOpen(false); - } - }; - - document.addEventListener('mousedown', handleOutsideClick); - - return () => { - document.removeEventListener('mousedown', handleOutsideClick); - }; - }, []); - - return ( -
    -
    - - - -
    - {isMenuOpen && ( -
    - -
    - )} -
    - ); -}; - -export default StakingCard; - -export const StakingCardHeader = ({ - validator, - identity, - network, - networkLogo, -}: StakingCardHeaderProps) => { - return ( -
    - -
    - -
    {validator || '-'}
    -
    -
    - -
    - {network} -
    - {capitalizeFirstLetter(network)} -
    -
    - -
    - ); -}; - -const StakingCardActions = ({ - toggleMenu, - menuRef, - chainID, - validatorAddress, - handleMenuAction, - processingValAddr, - handleCardClick, - enable, -}: StakingCardActionsProps) => { - const delegatorAddress = useAppSelector( - (state: RootState) => - state.wallet.networks[chainID]?.walletInfo?.bech32Address - ); - const txClaimStatus = useAppSelector( - (state: RootState) => state.distribution.chains[chainID]?.tx.status - ); - const txRestakeStatus = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.reStakeTxStatus - ); - const isClaimAll = useAppSelector( - (state) => state?.distribution?.chains?.[chainID]?.isTxAll || false - ); - const isReStakeAll = useAppSelector( - (state) => state?.staking?.chains?.[chainID]?.isTxAll || false - ); - - const dispatch = useAppDispatch(); - const { txWithdrawValidatorRewardsInputs, txRestakeValidatorInputs } = - useGetTxInputs(); - - const claim = () => { - if (txClaimStatus === TxStatus.PENDING) { - dispatch( - setError({ - type: 'error', - message: TXN_PENDING_ERROR('Claim'), - }) - ); - return; - } - const txInputs = txWithdrawValidatorRewardsInputs( - chainID, - validatorAddress, - delegatorAddress - ); - handleCardClick(validatorAddress); - if (txInputs.msgs.length) dispatch(txWithdrawAllRewards(txInputs)); - else { - dispatch( - setError({ - type: 'error', - message: NO_DELEGATIONS_ERROR, - }) - ); - } - }; - - const claimAndStake = () => { - if (txRestakeStatus === TxStatus.PENDING) { - dispatch( - setError({ - type: 'error', - message: TXN_PENDING_ERROR('Restake'), - }) - ); - return; - } - handleCardClick(validatorAddress); - const txInputs = txRestakeValidatorInputs(chainID, validatorAddress); - if (txInputs.msgs.length) dispatch(txRestake(txInputs)); - else { - dispatch( - setError({ - type: 'error', - message: NO_REWARDS_ERROR, - }) - ); - } - }; - - const delegate = () => { - handleMenuAction('delegate'); - }; - return ( -
    -
    -
    - -
    - - -
    - - - -
    - ); -}; - -const StakingCardActionButton = ({ - name, - action, - isPending, - enable, -}: StakingCardActionButtonProps) => { - return ( - - ); -}; diff --git a/frontend/src/app/(routes)/staking/components/StakingCardStats.tsx b/frontend/src/app/(routes)/staking/components/StakingCardStats.tsx deleted file mode 100644 index 810f5ebb9..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingCardStats.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { - StakingCardStatsProps, - StakingCardsStatsItemProps, -} from '@/types/staking'; -import { formatCoin, formatCommission } from '@/utils/util'; -import React from 'react'; - -const StakingCardStats = ({ - delegated, - rewards, - commission, - coinDenom, -}: StakingCardStatsProps) => { - return ( -
    - - - -
    - ); -}; - -export default StakingCardStats; - -const StakingCardStatsItem = ({ name, value }: StakingCardsStatsItemProps) => { - return ( -
    -
    {name}
    -
    {value}
    -
    - ); -}; diff --git a/frontend/src/app/(routes)/staking/components/StakingDashboard.tsx b/frontend/src/app/(routes)/staking/components/StakingDashboard.tsx new file mode 100644 index 000000000..20b0f7716 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/StakingDashboard.tsx @@ -0,0 +1,112 @@ +'use client'; + +import useStaking from '@/custom-hooks/useStaking'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import StakingSummary from './StakingSummary'; +import StakingUnDelegations from './StakingUnDelegations'; +import StakingDelegations from './StakingDelegations'; +import CustomButton from '@/components/common/CustomButton'; +import { setChangeNetworkDialogOpen } from '@/store/features/common/commonSlice'; + +const StakingDashboard = () => { + const dispatch = useAppDispatch(); + const staking = useStaking(); + const { + totalStakedAmount, + rewardsAmount, + totalUnStakedAmount, + availableAmount, + } = staking.getStakingAssets(); + + const delegations = staking.getAllDelegations(); + + const isWalletConnected = useAppSelector( + (state: RootState) => state.wallet.connected + ); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const hasUnbonding = useAppSelector( + (state: RootState) => state.staking.hasUnbonding + ); + const hasAuthzUnbonding = useAppSelector( + (state: RootState) => state.staking.authz.hasUnbonding + ); + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + + const openChangeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: true })); + }; + + return ( +
    +
    +
    +
    +
    Staking
    +
    +
    + {!isWalletConnected ? ( + 'Connect your wallet now to access all the modules on resolute' + ) : ( +

    + Here's an overview of your staked assets, including + delegation and undelegation details +

    + )} +
    +
    +
    +
    + {isWalletConnected && ( + + )} +
    + + {isWalletConnected ? ( + <> + {/* Staking summary */} + + + {/* Delegations */} + + + {/* Unbonding */} + {(!isAuthzMode && hasUnbonding) || + (isAuthzMode && hasAuthzUnbonding) ? ( + + ) : null} + + ) : ( +
    + +
    + )} +
    +
    + ); +}; +export default StakingDashboard; diff --git a/frontend/src/app/(routes)/staking/components/StakingDelegations.tsx b/frontend/src/app/(routes)/staking/components/StakingDelegations.tsx new file mode 100644 index 000000000..840c6dde4 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/StakingDelegations.tsx @@ -0,0 +1,422 @@ +import useStaking from '@/custom-hooks/useStaking'; +import { get } from 'lodash'; +import Image from 'next/image'; +import React, { useState, useRef, RefObject, useEffect } from 'react'; +import useValidator from '@/custom-hooks/useValidator'; +import { Chains } from '@/store/features/staking/stakeSlice'; +import DelegatePopup from '../components/DelegatePopup'; +import UndelegatePopup from '../components/UndelegatePopup'; +import ReDelegatePopup from '../components/ReDelegatePopup'; +import WithConnectionIllustration from '@/components/illustrations/withConnectionIllustration'; +import ValidatorName from './ValidatorName'; +import DelegationsLoading from './loaders/DelegationsLoading'; +import Link from 'next/link'; +import NumberFormat from '@/components/common/NumberFormat'; + +function StakingDelegations({ + delegations, + isSingleChain, +}: { + delegations: Chains; + isSingleChain: boolean; +}) { + const staking = useStaking(); + const validator = useValidator(); + + // Function to get the commission rate of a validator + const getCommisionRate = (valAddress: string, chainID: string) => { + const v = validator.getValidatorDetails(valAddress, chainID); + return Number(get(v, 'commission.commission_rates.rate', 0)) * 100; + }; + + // Function to get the total rewards for a specific chain + const getChainTotalRewards = (chainID: string) => + staking.chainTotalRewards(chainID); + + // Function to claim rewards for a specific validator + const withClaimValRewards = (validator: string, chainID: string) => + staking.txWithdrawValRewards(validator, chainID); + + // Function to claim rewards for a specific chain + const withClaimRewards = (chainID: string) => + staking.txWithdrawCliamRewards(chainID); + + // Function to restake rewards for a specific chain + const restakeRewards = (chainID: string) => + staking.transactionRestake(chainID); + + // Function to get the rewards for a specific validator + const getValRewards = (val: string, chainID: string) => + staking.chainTotalValRewards(val, chainID); + + // Get the status of claim transactions + const claimTxStatus = staking.getClaimTxStatus(); + + const restakeStatus = staking.txAllChainStakeTxStatus; + + let bondingCount = 0; + + Object.entries(delegations).forEach(([, value]) => { + get(value, 'delegations.delegations.delegation_responses', []).forEach( + () => { + bondingCount++; + } + ); + }); + + return ( +
    +
    +
    +
    +
    Delegations
    +
    + A delegation pool gathers and adds your stake to the native stake + pool of the validator for you +
    +
    +
    +
    +

    +

    Active

    +
    +
    +

    +

    Inactive

    +
    +
    +

    +

    Jailed

    +
    +
    +
    +
    +
    + + {!isSingleChain && staking.delegationsLoading === 0 && !bondingCount ? ( + + ) : null} + + {isSingleChain && !bondingCount ? ( + + ) : null} + + {Object.entries(delegations).map(([key, value], index) => + get(value, 'delegations.delegations.delegation_responses.length') ? ( +
    +
    +
    +
    +
    +
    + chain-logo + + {staking.chainName(key)} + +
    +
    +
    +

    + +

    +

    + Total Staked +

    +
    +
    +

    + +

    +

    + Total Rewards +

    +
    +
    +
    +
    +
    + + +
    +
    + {/*
    */} +
    +
    + {get( + value, + 'delegations.delegations.delegation_responses', + [] + ).map((data, dataid) => ( +
    +
    +
    +

    Validator Name

    + +
    +
    +

    Staked Amount

    +

    + +

    +
    +
    +

    Rewards

    +

    + +

    +
    +
    +

    Commission

    +

    + {getCommisionRate( + get(data, 'delegation.validator_address'), + key + )}{' '} + % +

    +
    +
    + +
    +
    +
    + ))} +
    +
    + ) : null + )} + + {!isSingleChain && staking.delegationsLoading !== 0 ? ( + + ) : null} +
    + ); +} + +interface PopupProps { + validator: string; + chainID: string; + withClaimRewards: (validator: string, chainID: string) => void; + commission: number; +} + +const StakingActionsPopup: React.FC = ({ + validator, + chainID, + commission, + withClaimRewards, +}) => { + const [showPopup, setShowPopup] = useState(false); + + const [openDelegate, setOpenDelegate] = useState(false); + const [openUnDelegate, setOpenUnDelegate] = useState(false); + const [openReDelegate, setOpenReDelegate] = useState(false); + const popupRef = useRef(null); + const buttonRef: RefObject = useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if ( + popupRef.current && + !popupRef.current.contains(event.target as Node) && + buttonRef.current && + !buttonRef.current.contains(event.target as Node) + ) { + setShowPopup(false); + } + }; + + useEffect(() => { + document.addEventListener('click', handleClickOutside); + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, []); + + // Handle click on the image to toggle the popup + const handleImageClick = (): void => { + setShowPopup(!showPopup); + }; + + // Toggle the visibility of Delegate Popup + const openDelegatePopup = (): void => setOpenDelegate(!openDelegate); + + // Toggle the visibility of Undelegate Popup + const openUnDelegatePopup = (): void => setOpenUnDelegate(!openUnDelegate); + + // Toggle the visibility of Redelegate Popup + const openReDelegatePopup = (): void => setOpenReDelegate(!openReDelegate); + + // Claim rewards for the specified validator and chain + const claimRewards = () => withClaimRewards(validator, chainID); + + return ( +
    + {openDelegate && ( + + )} + {openUnDelegate && ( + + )} + {openReDelegate && ( + + )} + + More-Icon + {showPopup ? ( +
    + + + + +
    + ) : null} +
    + ); +}; + +export default StakingDelegations; diff --git a/frontend/src/app/(routes)/staking/components/StakingLoading.tsx b/frontend/src/app/(routes)/staking/components/StakingLoading.tsx new file mode 100644 index 000000000..5f03a3c91 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/StakingLoading.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import ValidatorTableLoading from './loaders/ValidatorTableLoading'; +import SummaryLoading from './loaders/SummaryLoading'; +import UnbondingLoading from './loaders/UnbondingLoading'; +import DelegationsLoading from './loaders/DelegationsLoading'; + +const StakingLoading = () => { + return ( +
    +
    +
    +
    Staking
    +
    + Here's an overview of your staked assets, including delegation + and undelegation details, and your total staked balance. +
    +
    +
    + + {/* Staking Summary */} + +
    + + {/* Delegations */} + + + {/* Unbonding */} + + + {/* Validator table */} + +
    + ); +}; + +export default StakingLoading; diff --git a/frontend/src/app/(routes)/staking/components/StakingOverview.tsx b/frontend/src/app/(routes)/staking/components/StakingOverview.tsx deleted file mode 100644 index c9b4d28b3..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingOverview.tsx +++ /dev/null @@ -1,175 +0,0 @@ -'use client'; - -import React, { useEffect } from 'react'; -import './../staking.css'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { - getAllValidators, - getDelegations, - getParams, - getUnbonding, -} from '@/store/features/staking/stakeSlice'; -import ChainDelegations from './ChainDelegations'; -import ChainUnbondings from './ChainUnbondings'; -import { getDelegatorTotalRewards } from '@/store/features/distribution/distributionSlice'; -import { getBalances } from '@/store/features/bank/bankSlice'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import Image from 'next/image'; -import { - NO_DELEGATIONS_MSG, - NO_MESSAGES_ILLUSTRATION, -} from '@/utils/constants'; -import { CircularProgress } from '@mui/material'; - -const StakingOverview = () => { - const dispatch = useAppDispatch(); - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const nameToChainIDs: Record = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const chainIDs = Object.keys(nameToChainIDs).map( - (chainName) => nameToChainIDs[chainName] - ); - - const stakingData = useAppSelector( - (state: RootState) => state.staking.chains - ); - const rewardsData = useAppSelector( - (state: RootState) => state.distribution.chains - ); - const hasDelegations = useAppSelector( - (state: RootState) => state.staking.hasDelegations - ); - const hasUnbonding = useAppSelector( - (state: RootState) => state.staking.hasUnbonding - ); - const delegationsLoading = useAppSelector( - (state: RootState) => state.staking.delegationsLoading - ); - const { getChainInfo, getDenomInfo } = useGetChainInfo(); - useEffect(() => { - if (chainIDs) { - chainIDs.forEach((chainID) => { - const { address, baseURL } = getChainInfo(chainID); - const { minimalDenom } = getDenomInfo(chainID); - - dispatch( - getDelegations({ - baseURL, - address, - chainID, - }) - ); - dispatch( - getAllValidators({ - baseURL, - chainID, - }) - ); - dispatch( - getUnbonding({ - baseURL, - address, - chainID, - }) - ); - dispatch( - getDelegatorTotalRewards({ - baseURL, - address, - chainID, - denom: minimalDenom, - }) - ); - dispatch( - getBalances({ - baseURL, - address, - chainID, - }) - ); - dispatch(getParams({ baseURL, chainID })); - }); - } - }, []); - - return ( -
    -

    Staking

    -
    - {chainIDs.map((chainID) => { - const delegations = stakingData[chainID]?.delegations.delegations; - const validators = stakingData[chainID]?.validators; - const currency = networks[chainID]?.network?.config?.currencies[0]; - const chainName = networks[chainID]?.network?.config?.chainName; - const rewards = rewardsData[chainID]?.delegatorRewards?.list; - - return ( - - ); - })} -
    - - {delegationsLoading === 0 && !hasDelegations ? ( -
    - {'No -
    - {NO_DELEGATIONS_MSG} -
    -
    - ) : null} - - {delegationsLoading !== 0 ? ( -
    - -
    - ) : null} - - {hasUnbonding ? ( -
    -

    Unbonding

    -
    - {chainIDs.map((chainID) => { - const unbondingDelegations = - stakingData[chainID]?.unbonding.unbonding; - const validators = stakingData[chainID]?.validators; - const { chainName, currencies } = - networks[chainID]?.network?.config; - - return ( - - ); - })} -
    -
    - ) : null} -
    - ); -}; - -export default StakingOverview; diff --git a/frontend/src/app/(routes)/staking/components/StakingOverviewSidebar.tsx b/frontend/src/app/(routes)/staking/components/StakingOverviewSidebar.tsx deleted file mode 100644 index a39a24b22..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingOverviewSidebar.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { formatDollarAmount } from '@/utils/util'; -import React from 'react'; -import StakingStatsCard from './StakingStatsCard'; -import TopNav from '@/components/TopNav'; -import useGetAssetsAmount from '@/custom-hooks/useGetAssetsAmount'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import StakingSideBarAds from './StakingSideBarAds'; -import { Tooltip } from '@mui/material'; - -const StakingOverviewSidebar = () => { - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const chainIDs = Object.values(nameToChainIDs); - - const [totalStakedAmount, , rewards] = useGetAssetsAmount(chainIDs); - return ( -
    -
    - -
    -
    - - -
    -
    - - - - - - -
    -
    -
    - -
    - ); -}; - -export default StakingOverviewSidebar; diff --git a/frontend/src/app/(routes)/staking/components/StakingPage.tsx b/frontend/src/app/(routes)/staking/components/StakingPage.tsx deleted file mode 100644 index a93e518c5..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingPage.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client'; - -import React, { useEffect, useState } from 'react'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { - getAllValidators, - getDelegations, - getParams, - getUnbonding, -} from '@/store/features/staking/stakeSlice'; - -import ChainDelegations from './ChainDelegations'; -import StakingSidebar from './StakingSidebar'; -import ChainUnbondings from './ChainUnbondings'; -import { getDelegatorTotalRewards } from '@/store/features/distribution/distributionSlice'; -import { Validator } from '@/types/staking'; -import { useRouter } from 'next/navigation'; -import { getBalances } from '@/store/features/bank/bankSlice'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { - NO_DELEGATIONS_MSG, - NO_MESSAGES_ILLUSTRATION, -} from '@/utils/constants'; -import Image from 'next/image'; -import { CircularProgress } from '@mui/material'; -import { TxStatus } from '@/types/enums'; - -const StakingPage = ({ - chainName, - validatorAddress, - action, -}: { - chainName: string; - validatorAddress: string; - action: string; -}) => { - const dispatch = useAppDispatch(); - const router = useRouter(); - const [allValidatorsDialogOpen, setAllValidatorsDialogOpen] = - useState(false); - const toggleValidatorsDialog = () => { - setAllValidatorsDialogOpen((prevState) => !prevState); - }; - const nameToChainIDs: Record = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const chainID = nameToChainIDs[chainName]; - const delegations = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.delegations.delegations - ); - const unbondingDelegations = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.unbonding.unbonding - ); - const validators = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.validators - ); - const currency = useAppSelector( - (state: RootState) => - state.wallet.networks[chainID]?.network?.config?.currencies[0] - ); - const rewards = useAppSelector( - (state: RootState) => - state.distribution.chains?.[chainID]?.delegatorRewards.list - ); - const hasUnbondings = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.unbonding?.hasUnbonding - ); - const hasDelegations = useAppSelector( - (state: RootState) => - state.staking.chains[chainID]?.delegations?.hasDelegations - ); - const delegationsLoading = useAppSelector( - (state: RootState) => state.staking.delegationsLoading - ); - const txStatus = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.tx - ); - - const { getChainInfo } = useGetChainInfo(); - const { address, baseURL } = getChainInfo(chainID); - - useEffect(() => { - dispatch( - getDelegations({ - baseURL, - address, - chainID, - }) - ); - dispatch( - getAllValidators({ - baseURL, - chainID, - }) - ); - dispatch( - getUnbonding({ - baseURL, - address, - chainID, - }) - ); - dispatch( - getDelegatorTotalRewards({ - baseURL, - address, - chainID, - denom: currency.coinMinimalDenom, - }) - ); - dispatch( - getBalances({ - baseURL, - address, - chainID, - }) - ); - dispatch(getParams({ baseURL, chainID })); - }, [chainID]); - - const onMenuAction = (type: string, validator: Validator) => { - const valAddress = validator?.operator_address; - if (valAddress?.length) { - router.push(`?validator_address=${valAddress}&action=${type}`); - } - }; - - useEffect(() => { - if (txStatus?.status === TxStatus.IDLE) { - setAllValidatorsDialogOpen(false); - } - }, [txStatus?.status]); - - return ( -
    -
    -

    Staking

    -
    - -
    - {delegationsLoading === 0 && !hasDelegations ? ( -
    - {'No -
    - {NO_DELEGATIONS_MSG} -
    - -
    - ) : null} - - {delegationsLoading !== 0 ? ( -
    - -
    - ) : null} - - {hasUnbondings ? ( -
    -

    Unbonding

    -
    - -
    -
    - ) : null} -
    - -
    - ); -}; - -export default StakingPage; diff --git a/frontend/src/app/(routes)/staking/components/StakingSideBarAds.tsx b/frontend/src/app/(routes)/staking/components/StakingSideBarAds.tsx deleted file mode 100644 index c66f4a5b0..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingSideBarAds.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client'; -import Image from 'next/image'; -import React, { useState } from 'react'; - -const StakingSideBarAds = () => { - const [ad1Open, setAd1Open] = useState(false); - const [ad2Open, setAd2Open] = useState(false); - - return ( -
    - {ad1Open ? ( -
    -
    - setAd1Open(false)} - src="/close.svg" - width={30} - height={30} - alt="Close ad" - draggable={false} - /> -
    - Ad -
    - ) : null} - {ad2Open ? ( -
    -
    - setAd2Open(false)} - src="/close.svg" - width={30} - height={30} - alt="Close ad" - draggable={false} - /> -
    - Ad -
    - ) : null} -
    - ); -}; - -export default StakingSideBarAds; diff --git a/frontend/src/app/(routes)/staking/components/StakingSidebar.tsx b/frontend/src/app/(routes)/staking/components/StakingSidebar.tsx deleted file mode 100644 index 2a6239cd9..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingSidebar.tsx +++ /dev/null @@ -1,175 +0,0 @@ -'use client'; - -import { StakingSidebarProps } from '@/types/staking'; -import { CircularProgress } from '@mui/material'; -import React from 'react'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import StakingStatsCard from './StakingStatsCard'; -import TopNav from '@/components/TopNav'; -import useGetTxInputs from '@/custom-hooks/useGetTxInputs'; -import { txWithdrawAllRewards } from '@/store/features/distribution/distributionSlice'; -import { TxStatus } from '@/types/enums'; -import { txRestake } from '@/store/features/staking/stakeSlice'; -import { setError } from '@/store/features/common/commonSlice'; -import AllValidators from './AllValidators'; -import { formatStakedAmount } from '@/utils/util'; -import { - NO_DELEGATIONS_ERROR, - NO_REWARDS_ERROR, - TXN_PENDING_ERROR, -} from '@/utils/errors'; - -const StakingSidebar = ({ - validators, - currency, - chainID, - onMenuAction, - allValidatorsDialogOpen, - toggleValidatorsDialog, -}: StakingSidebarProps) => { - const stakedBalance = useAppSelector( - (state: RootState) => - state.staking.chains?.[chainID]?.delegations.totalStaked || 0 - ); - const totalRewards = useAppSelector( - (state: RootState) => - state.distribution.chains?.[chainID]?.delegatorRewards.totalRewards || 0 - ); - const isClaimAll = useAppSelector( - (state) => state?.distribution?.chains?.[chainID]?.isTxAll || false - ); - const isReStakeAll = useAppSelector( - (state) => state?.staking?.chains?.[chainID]?.isTxAll || false - ); - const tokens = [ - { - amount: stakedBalance.toString(), - denom: currency.coinMinimalDenom, - }, - ]; - const rewardTokens = [ - { - amount: parseInt(totalRewards.toString()).toString(), - denom: currency.coinMinimalDenom, - }, - ]; - - const txClaimStatus = useAppSelector( - (state: RootState) => state.distribution.chains[chainID]?.tx.status - ); - const txRestakeStatus = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.reStakeTxStatus - ); - const validatorsStatus = useAppSelector( - (state: RootState) => state.staking.chains[chainID]?.validators.status - ); - const delegations = useAppSelector( - (state: RootState) => - state.staking.chains[chainID]?.delegations?.delegations - ?.delegation_responses - ); - - const dispatch = useAppDispatch(); - const { txWithdrawAllRewardsInputs, txRestakeInputs } = useGetTxInputs(); - - const claim = (chainID: string) => { - if (txClaimStatus === TxStatus.PENDING) { - dispatch( - setError({ - type: 'error', - message: TXN_PENDING_ERROR('Claim'), - }) - ); - return; - } - const txInputs = txWithdrawAllRewardsInputs(chainID); - txInputs.isTxAll = true; - if (txInputs.msgs.length) dispatch(txWithdrawAllRewards(txInputs)); - else { - dispatch( - setError({ - type: 'error', - message: NO_DELEGATIONS_ERROR, - }) - ); - } - }; - - const claimAndStake = (chainID: string) => { - if (txRestakeStatus === TxStatus.PENDING) { - dispatch( - setError({ - type: 'error', - message: TXN_PENDING_ERROR('Restake'), - }) - ); - return; - } - const txInputs = txRestakeInputs(chainID); - txInputs.isTxAll = true; - if (txInputs.msgs.length) dispatch(txRestake(txInputs)); - else { - dispatch( - setError({ - type: 'error', - message: NO_REWARDS_ERROR, - }) - ); - } - }; - - return ( -
    -
    - -
    - - -
    - {delegations?.length > 1 ? ( -
    - - -
    - ) : null} -
    -
    - -
    -
    - ); -}; - -export default StakingSidebar; diff --git a/frontend/src/app/(routes)/staking/components/StakingStatsCard.tsx b/frontend/src/app/(routes)/staking/components/StakingStatsCard.tsx deleted file mode 100644 index a3ceb1423..000000000 --- a/frontend/src/app/(routes)/staking/components/StakingStatsCard.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Image from 'next/image'; -import React from 'react'; - -const StakingStatsCard = ({ name, value }: { name: string; value: string }) => { - return ( -
    -
    -
    - Staked Balance -
    -
    - {name} -
    -
    -
    - {value} -
    -
    - ); -}; - -export default StakingStatsCard; diff --git a/frontend/src/app/(routes)/staking/components/StakingSummary.tsx b/frontend/src/app/(routes)/staking/components/StakingSummary.tsx new file mode 100644 index 000000000..245680c87 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/StakingSummary.tsx @@ -0,0 +1,66 @@ +import NumberFormat from '@/components/common/NumberFormat'; + +import React from 'react'; + +type AssetSummary = { icon: string; alt: string; type: string; amount: string }; + +function StakingSummary({ + stakedAmount, + rewardsAmount, + unstakeAmount, + availableAmount, +}: { + stakedAmount: number; + rewardsAmount: number; + unstakeAmount: number; + availableAmount: number; +}) { + const total = stakedAmount + rewardsAmount + unstakeAmount + availableAmount; + + const assetsSummaryData: AssetSummary[] = [ + { + icon: '/staked-bal.png', + alt: 'stake', + type: 'Total Amount', + amount: total?.toString(), + }, + { + icon: '/total-bal.png', + alt: 'available', + type: 'Staked Amount', + amount: stakedAmount?.toString(), + }, + { + icon: '/rewards.png', + alt: 'rewards', + type: 'Rewards', + amount: rewardsAmount?.toString(), + }, + + { + icon: '/avail-bal.png', + alt: 'Avail-bal-icon', + type: 'Available Balance', + amount: availableAmount?.toString(), + }, + ]; + + return ( +
    +
    + {assetsSummaryData.map((data, index) => ( +
    +
    +
    {data.type}
    +
    + +
    +
    +
    + ))} +
    +
    + ); +} + +export default StakingSummary; diff --git a/frontend/src/app/(routes)/staking/components/StakingUnDelegations.tsx b/frontend/src/app/(routes)/staking/components/StakingUnDelegations.tsx new file mode 100644 index 000000000..77421a974 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/StakingUnDelegations.tsx @@ -0,0 +1,152 @@ +import useStaking from '@/custom-hooks/useStaking'; +import { get } from 'lodash'; +// import Image from 'next/image' +import React from 'react'; +import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; +import { Chains } from '@/store/features/staking/stakeSlice'; +import '../staking.css'; +import WithConnectionIllustration from '@/components/illustrations/withConnectionIllustration'; +import ValidatorName from './ValidatorName'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import NumberFormat from '@/components/common/NumberFormat'; + +function StakingUnDelegations({ + undelegations, + isSingleChain, +}: { + undelegations: Chains; + isSingleChain?: boolean; +}) { + const staking = useStaking(); + + const nameToChainIDs = useAppSelector( + (state: RootState) => state.wallet.nameToChainIDs + ); + + const getChainName = (chainID: string) => { + return ( + Object.keys(nameToChainIDs).find( + (key) => nameToChainIDs[key] === chainID + ) || '-' + ); + }; + + const cancelUnbonding = ( + chainID: string, + delegator: string, + validator: string, + height: string, + amount: number + ) => { + staking.txCancelUnbond(chainID, delegator, validator, amount, height); + }; + + let unbondingCount = 0; + + Object.entries(undelegations).forEach(([, value]) => { + get(value, 'unbonding.unbonding.unbonding_responses', []).forEach((ud) => { + unbondingCount = get(ud, 'entries.length', 0); + }); + }); + + return ( +
    +
    +
    Unbonding
    +
    + Unbonding delegations will be locked until their locked time, after + which they will be available in your balance +
    +
    +
    + + {(staking.undelegationsLoading === 0 && !unbondingCount) || + (isSingleChain && !unbondingCount) ? ( + + ) : null} + +
    + {Object.entries(undelegations).map(([key, value]) => { + return get(value, 'unbonding.unbonding.unbonding_responses', []).map( + (ud) => { + return get(ud, 'entries', []).map((e, kIndex) => { + return ( +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +

    Network

    +

    {getChainName(key)}

    +
    +
    +

    Avail Days

    +

    + {getTimeDifferenceToFutureDate( + get(e, 'completion_time', '') + )} +

    +
    +
    +

    Amount

    +

    + + +

    +
    +
    +
    + ); + }); + } + ); + })} +
    +
    + ); +} + +export default StakingUnDelegations; diff --git a/frontend/src/app/(routes)/staking/components/StakingVal.tsx b/frontend/src/app/(routes)/staking/components/StakingVal.tsx new file mode 100644 index 000000000..9759a3ded --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/StakingVal.tsx @@ -0,0 +1,34 @@ +import Image from 'next/image'; + +const StakingVal = () => { + return ( +
    +
    +
    +
    + Staking +
    +
    + Connect your wallet now to access all the modules on resolute{' '} +
    +
    +
    +
    +
    + Dashboard-Image +
    + +
    +
    + {/* */} +
    + ); +}; +export default StakingVal; diff --git a/frontend/src/app/(routes)/staking/components/UnbondingCard.tsx b/frontend/src/app/(routes)/staking/components/UnbondingCard.tsx deleted file mode 100644 index 6ec9f0273..000000000 --- a/frontend/src/app/(routes)/staking/components/UnbondingCard.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { StakingCardHeader } from './StakingCard'; -import { formatCoin, getDaysLeftString } from '@/utils/util'; -import { getDaysLeft } from '@/utils/datetime'; -import { - CircularProgress, - Dialog, - DialogContent, - Tooltip, -} from '@mui/material'; -import { - UnbondingCardProps, - UnbondingCardStatsItemProps, - UnbondingCardStatsProps, -} from '@/types/staking'; -import Image from 'next/image'; -import { CLOSE_ICON_PATH } from '@/utils/constants'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { - resetCancelUnbondingTx, - txCancelUnbonding, -} from '@/store/features/staking/stakeSlice'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { TxStatus } from '@/types/enums'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -const UnbondingCard = ({ - moniker, - identity, - chainName, - amount, - networkLogo, - currency, - completionTime, - chainID, - validatorAddress, - creationHeight, -}: UnbondingCardProps) => { - const dispatch = useAppDispatch(); - const [unbondingDialogOpen, setUnbondingDialogOpen] = - useState(false); - const handleDialogClose = () => { - setUnbondingDialogOpen(false); - }; - const { getChainInfo } = useGetChainInfo(); - const { feeAmount: avgFeeAmount, address } = getChainInfo(chainID); - const feeAmount = avgFeeAmount * 10 ** currency?.coinDecimals; - - const loading = useAppSelector( - (state: RootState) => state.staking.chains[chainID].cancelUnbondingTxStatus - ); - useEffect(() => { - dispatch(resetCancelUnbondingTx({ chainID: chainID })); - }, []); - const onCancelUnbondingTx = () => { - dispatch( - txCancelUnbonding({ - basicChainInfo: getChainInfo(chainID), - delegator: address, - validator: validatorAddress, - amount: amount * 10 ** currency.coinDecimals, - denom: currency.coinMinimalDenom, - feeAmount: feeAmount, - feegranter: '', - creationHeight: creationHeight, - }) - ); - }; - return ( -
    - - -
    - - - -
    - -
    - ); -}; - -export default UnbondingCard; - -const UnbondingCardStats = ({ - completionTime, - amount, - coinDenom, -}: UnbondingCardStatsProps) => { - const daysLeft = getDaysLeft(completionTime); - return ( -
    - - -
    - ); -}; - -export const UnbondingCardStatsItem = ({ - name, - value, -}: UnbondingCardStatsItemProps) => { - return ( -
    -
    {name}
    -
    {value}
    -
    - ); -}; - -interface DialogCancelUnbondingProps { - open: boolean; - onClose: () => void; - onCancelUnbondingTx: () => void; - loading: TxStatus; -} - -const DialogCancelUnbonding: React.FC = (props) => { - const { open, onClose, onCancelUnbondingTx, loading } = props; - return ( - - -
    -
    -
    - Close -
    -
    -
    - Cancel Unbonding -
    -
    -

    - Cancel Unbonding -

    -
    - Once this action is done, It cannot be undone. -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - ); -}; diff --git a/frontend/src/app/(routes)/staking/components/UndelegatePopup.tsx b/frontend/src/app/(routes)/staking/components/UndelegatePopup.tsx new file mode 100644 index 000000000..ccf832f2d --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/UndelegatePopup.tsx @@ -0,0 +1,143 @@ +'use client'; + +import CustomDialog from '@/components/common/CustomDialog'; +import { useState } from 'react'; +import Image from 'next/image'; +import AddressField from './AddressField'; +import useStaking from '@/custom-hooks/useStaking'; +import { get } from 'lodash'; +import useSingleStaking from '@/custom-hooks/useSingleStaking'; +import ValidatorName from './ValidatorName'; + +interface PopupProps { + validator: string; + chainID: string; + openPopup: boolean; + openDelegatePopup: () => void; +} + +const UndelegatePopup: React.FC = ({ + validator, + chainID, + openPopup, + openDelegatePopup, +}) => { + // Local state to manage the amount and the open status of the dialog + const [amount, setAmount] = useState(0); + const [open, setOpen] = useState(openPopup); + + // Custom hook to get single staking information based on chainID + /* eslint-disable @typescript-eslint/no-explicit-any */ + const singleStake: any = useSingleStaking(chainID); + + // Get the total staked amount and denomination + // const { totalStakedAmount } = singleStake.getStakingAssets(); + const totalStakedAmount = singleStake.totalValStakedAssets( + chainID, + validator + ); + const denom = singleStake.getDenomWithChainID(chainID); + + // Custom hook to get staking information + const staking = useStaking(); + + // Get the current validator's information from the staking module + const stakeModule = staking.getAllDelegations(); + const val = stakeModule[chainID]?.validators?.active?.[validator]; + + // Calculate the commission rate for the validator + const getCommisionRate = () => { + return Number(get(val, 'commission.commission_rates.rate', 0)) * 100; + }; + + // Handler for input change to set the amount + const onChange = (event: React.ChangeEvent) => + setAmount(Number(event.target.value)); + + // Handler for quick select amount + const onChangeAmount = (value: number) => { + setAmount(Number((value * totalStakedAmount).toFixed(6))); + }; + + // Function to perform the undelegation transaction + const doTxUnDelegate = () => { + staking.txUnDelegateTx(validator, amount, chainID); + }; + + // Status of the undelegation transaction + const delegateStatus = staking.txAllChainStakeTxStatus[chainID]?.tx?.status; + + return ( + { + openDelegatePopup(); + setOpen(false); + }} + title="Undelegate" + description="Undelegate lets you withdraw staked tokens from a validator, usually after an unbonding period" + > +
    + {/* Validator details */} +
    +
    + +
    +
    +

    + {get(val, 'description.details', '-')} +

    +

    + {getCommisionRate()}% Commission +

    +
    +
    +
    + + {/* Address field for the amount */} + + + {/* Staking alert */} +
    +
    + info-icon +

    Important

    +

    + Staking will lock your funds for 21 days. +

    +
    +
    + To make your staked assets liquid, undelegation will take 21 days. +
    +
    + + {/* Undelegate button */} + +
    +
    + ); +}; + +export default UndelegatePopup; diff --git a/frontend/src/app/(routes)/staking/components/ValidatorComponent.tsx b/frontend/src/app/(routes)/staking/components/ValidatorComponent.tsx deleted file mode 100644 index cfca08c7b..000000000 --- a/frontend/src/app/(routes)/staking/components/ValidatorComponent.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { StakingMenuAction, Validator } from '@/types/staking'; -import { - canDelegate, - formatCommission, - getValidatorStatus, -} from '@/utils/util'; -import React from 'react'; -import ValidatorLogo from './ValidatorLogo'; -import { Tooltip } from '@mui/material'; - -interface ValidatorComponentProps { - moniker: string; - identity: string; - commission: number; - jailed: boolean; - status: string; - active: boolean; - rank: string; - onMenuAction: StakingMenuAction; - validator: Validator; -} - -const isJailed = (validatorStatus: string) => { - return validatorStatus.toLowerCase() === 'jailed'; -}; - -const ValidatorComponent = ({ - moniker, - identity, - commission, - jailed, - status, - active, - rank, - onMenuAction, - validator, -}: ValidatorComponentProps) => { - const validatorStatus = getValidatorStatus(jailed, status); - const enableDelegate = canDelegate(validatorStatus); - return ( -
    -
    -
    - -
    -
    -
    - -
    - {moniker} -
    -
    -
    -
    -
    -
    {rank}
    -
    - {formatCommission(commission)} Commission -
    - {active ? null : ( -
    - - {validatorStatus} - -
    - )} -
    - -
    -
    - ); -}; - -export default ValidatorComponent; diff --git a/frontend/src/app/(routes)/staking/components/ValidatorItem.tsx b/frontend/src/app/(routes)/staking/components/ValidatorItem.tsx deleted file mode 100644 index 5353f712f..000000000 --- a/frontend/src/app/(routes)/staking/components/ValidatorItem.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ValidatorItemProps } from '@/types/staking'; -import React from 'react'; -import ValidatorLogo from './ValidatorLogo'; -import { Tooltip } from '@mui/material'; -import { formatVotingPower } from '@/utils/denom'; -import { formatCommission } from '@/utils/util'; - -const ValidatorItem = ({ - moniker, - identity, - commission, - tokens, - currency, - onMenuAction, - validators, - validator, -}: ValidatorItemProps) => { - return ( -
    -
    -
    - -
    -
    -
    - -
    - {moniker} -
    -
    -
    -
    - {formatVotingPower(tokens, currency.coinDecimals)} -
    -
    -
    -
    - {formatCommission(commission)} Commission -
    -
    - -
    -
    - ); -}; - -export default ValidatorItem; diff --git a/frontend/src/app/(routes)/staking/components/ValidatorName.tsx b/frontend/src/app/(routes)/staking/components/ValidatorName.tsx new file mode 100644 index 000000000..01b83d13a --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/ValidatorName.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useCallback } from 'react'; +import { get } from 'lodash'; +import useValidator from '@/custom-hooks/useValidator'; +import ValidatorLogo from '../components/ValidatorLogo'; +import { WalletAddress } from '@/components/main-layout/SelectNetwork'; +import { Tooltip } from '@mui/material'; +import { shortenName } from '@/utils/util'; +import Link from 'next/link'; + +interface ValidatorNameProps { + valoperAddress: string; + chainID: string; + hasStatus?: boolean; + smallFont?: boolean; +} + +interface ValStatusObj { + [key: string]: string; +} + +const valStatusObj: ValStatusObj = { + BOND_STATUS_BONDED: 'Active', + BOND_STATUS_UNBONDED: 'InActive', +}; + +const ValidatorName: React.FC = ({ + valoperAddress, + chainID, + hasStatus, + smallFont, +}) => { + const { fetchValidator, getValidatorDetails } = useValidator(); + + const memoizedFetchValidator = useCallback(() => { + fetchValidator(valoperAddress, chainID); + }, [valoperAddress, chainID, fetchValidator]); + + useEffect(() => { + memoizedFetchValidator(); + }, [memoizedFetchValidator]); + + const validatorDetails = getValidatorDetails(valoperAddress, chainID); + const monikerName: string = get(validatorDetails, 'description.moniker', ''); + + return ( +
    + {!validatorDetails ? ( + 'Loading....' + ) : ( + <> + {hasStatus ? ( + // If the status is Active than use this css "status-active" + // If the status is Jailed than we can use this "status-jailed" + //And the status is Unbonded we can use this "status-unbonded" +
    + {/* {get(validatorDetails, 'jailed') + ? 'Jailed' + : valStatusObj[get(validatorDetails, 'status') || '']} */} +
    + ) : null} + {/* validator logo */} + {' '} +   + {/* Validator name */} + + +

    + {shortenName( + get(validatorDetails, 'description.moniker', ''), + 20 + )} +

    + +
    +   + {/* Copy address icon */} + + + )} +
    + ); +}; + +export default React.memo(ValidatorName); diff --git a/frontend/src/app/(routes)/staking/components/ValidatorTable.tsx b/frontend/src/app/(routes)/staking/components/ValidatorTable.tsx new file mode 100644 index 000000000..ff774d449 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/ValidatorTable.tsx @@ -0,0 +1,291 @@ +import React, { useEffect, useCallback, useMemo, useState } from 'react'; +import { get } from 'lodash'; +import useSingleStaking from '@/custom-hooks/useSingleStaking'; +import { WalletAddress } from '@/components/main-layout/SelectNetwork'; +import ValidatorLogo from '../components/ValidatorLogo'; +import DelegatePopup from '../components/DelegatePopup'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { useSelector } from 'react-redux'; +import { + selectFilteredValidators, + selectSearchQuery, +} from '../selectors/validatorsSelectors'; +import { + filterValidators, + setSearchQuery, + setValidators, + sortValidatorsByVotingPower, +} from '@/store/features/staking/stakeSlice'; +import { Validator } from '@/types/staking'; +import SearchValidator from '../components/SearchValidator'; +import { shortenName } from '@/utils/util'; +import { useRouter, useSearchParams } from 'next/navigation'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import NumberFormat from '@/components/common/NumberFormat'; +import Link from 'next/link'; + +interface ValStatusObj { + [key: string]: string; +} + +const valStatusObj: ValStatusObj = { + BOND_STATUS_BONDED: 'Active', + BOND_STATUS_UNBONDED: 'Unbonded', + BOND_STATUS_UNBONDING: 'Unbonding', +}; + +const ValidatorTable: React.FC<{ chainID: string }> = ({ chainID }) => { + const router = useRouter(); + const { getChainInfo } = useGetChainInfo(); + const { chainName } = getChainInfo(chainID); + const staking = useSingleStaking(chainID); + const dispatch = useAppDispatch(); + const filteredValidators = useSelector(selectFilteredValidators); + const searchQuery = useSelector(selectSearchQuery); + + const validators = staking.getValidators(); + + const paramValidatorAddress = useSearchParams().get('validator_address'); + const paramAction = useSearchParams().get('action'); + + useEffect(() => { + if (validators?.status === 'idle') { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const activeValidators: any = get(validators, 'active', {}); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const inactiveValidators: any = get(validators, 'inactive', {}); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const activeSsortedObj: any = {}; + let rank = 1; + get(validators, 'activeSorted', []).forEach((key) => { + if (activeValidators.hasOwnProperty(key)) { + activeSsortedObj[key] = { + rank: rank++, + ...activeValidators[key], + }; + } + }); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const inactiveSsortedObj: any = {}; + + get(validators, 'inactiveSorted', []).forEach((key) => { + if (inactiveValidators.hasOwnProperty(key)) { + inactiveSsortedObj[key] = { + rank: rank++, + ...inactiveValidators[key], + }; + } + }); + + dispatch(setValidators({ ...activeSsortedObj, ...inactiveSsortedObj })); + } + }, [validators?.status]); + + const handleSearch = useCallback( + (query: string) => { + dispatch(setSearchQuery(query)); + dispatch(filterValidators()); + dispatch(sortValidatorsByVotingPower({ chainID })); + }, + [dispatch] + ); + + const { getAmountWithDecimal } = useSingleStaking(chainID); + const [openDelegate, setOpenDelegate] = useState(false); + const [selectedValidator, setSelectedValidator] = useState(''); + const [selectValCommission, setSelectValCommission] = useState(0); + + const validatorRows = useMemo(() => { + return Object.entries(filteredValidators || {}).map(([key, value]) => ( + + + +
    +
    # {get(value, 'rank', '-')}
    +
    + + +
    + {' '} +   + +

    + {shortenName(get(value, 'description.moniker', ''), 12)} +

    {' '} + +   + +
    + + +
    + {parseInt( + ( + get(value, 'commission.commission_rates.rate') * 100 + ).toString() + )} + % +
    + + +
    + + + {/* {getAmountWithDecimal(Number(get(value, 'tokens')), chainID)} */} +
    + + +
    + {get(value, 'jailed') + ? 'Jailed' + : valStatusObj[get(value, 'status')]} +
    + + + {!get(value, 'jailed') ? ( + + ) : null} + + +
    + )); + }, [filteredValidators, chainID]); + + const handleOpenDelegateDialog = (validator: Validator) => { + setSelectedValidator(validator.operator_address); + const c = parseInt(get(validator, 'commission.commission_rates.rate'))*100 + setSelectValCommission(c) + router.push( + `?validator_address=${validator.operator_address}&action=delegate` + ); + }; + + const handleCloseDelegateDialog = () => { + setSelectedValidator(''); + router.push(`/staking/${chainName.toLowerCase()}`); + setOpenDelegate(false); + }; + + useEffect(() => { + if ( + paramValidatorAddress?.length && + paramAction?.length && + filteredValidators + ) { + if (paramAction.toLowerCase() === 'delegate') { + setSelectedValidator(paramValidatorAddress); + setOpenDelegate(true); + } + } + }, [paramValidatorAddress, paramAction, filteredValidators]); + + return ( +
    + {openDelegate ? ( + + ) : null} + +
    +
    Validators
    +
    + List of the validators in the network, including their voting power, + performance metrics and other details +
    +
    +
    +
    + handleSearch(e.target.value)} + searchQuery={searchQuery} + /> +
    + + + + + + + + + + + + + {validators?.status === 'pending' ? ( + <> + {Array(3) + .fill(null) + .map((_, colIndex) => ( + + {Array(6) + .fill(null) + .map((_, colIndex) => ( + + ))} + + ))} + + ) : ( + validatorRows + )} + +
    +
    Rank
    +
    +
    Validator
    +
    +
    Commission
    +
    +
    Voting Power
    +
    +
    Status
    +
    +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default ValidatorTable; diff --git a/frontend/src/app/(routes)/staking/components/ValidatorsAutoComplete.tsx b/frontend/src/app/(routes)/staking/components/ValidatorsAutoComplete.tsx new file mode 100644 index 000000000..09f4db7f5 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/ValidatorsAutoComplete.tsx @@ -0,0 +1,162 @@ +/** @jsxImportSource @emotion/react */ + +import ValidatorLogo from '@/app/(routes)/staking/components/ValidatorLogo'; +import { + customAutoCompleteStyles, + customTextFieldStyles, +} from '@/app/(routes)/transfers/styles'; +import { ValidatorInfo } from '@/types/staking'; +import { shortenName } from '@/utils/util'; +import { + Autocomplete, + CircularProgress, + InputAdornment, + Paper, + TextField, +} from '@mui/material'; +import React from 'react'; +import { css } from '@emotion/react'; + +const listItemStyle = css` + &:hover { + background-color: #ffffff09 !important; + } +`; + +const ValidatorsAutoComplete = ({ + options, + selectedValidator, + handleChange, + dataLoading, + name, +}: { + options: ValidatorInfo[]; + selectedValidator: ValidatorInfo | null; + handleChange: (option: ValidatorInfo | null) => void; + dataLoading: boolean; + name: string; +}) => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderOption = (props: any, option: ValidatorInfo) => ( +
  • +
    + +
    +
    +
    + {shortenName(option.label, 30)} +
    +
    + {shortenName(option?.description, 80)} +
    +
    +
    + {Math.trunc(option.commission || 0)}% Commission +
    +
    +
    +
  • + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderInput = (params: any) => ( + + {selectedValidator && ( + + )} + {params.InputProps.startAdornment} + + ), + endAdornment: ( + +
    + {selectedValidator && ( +
    + + {selectedValidator.commission}% Commission + +
    + )} + {params.InputProps.endAdornment} +
    +
    + ), + }} + sx={{ + '& .MuiInputBase-input': { + color: '#fffffff0', + fontSize: '14px', + fontWeight: 400, + fontFamily: 'Libre Franklin', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: '#fffffff0', + }, + }} + /> + ); + + return ( + option.label} + renderOption={renderOption} + renderInput={renderInput} + onChange={(_, newValue) => handleChange(newValue)} + value={selectedValidator} + PaperComponent={({ children }) => ( + + {dataLoading ? ( +
    + +
    + ) : ( + children + )} +
    + )} + sx={{ + ...customTextFieldStyles, + ...customAutoCompleteStyles, + }} + /> + ); +}; + +export default ValidatorsAutoComplete; diff --git a/frontend/src/app/(routes)/staking/components/loaders/DelegationsLoading.tsx b/frontend/src/app/(routes)/staking/components/loaders/DelegationsLoading.tsx new file mode 100644 index 000000000..6c8235a28 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/loaders/DelegationsLoading.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const DelegationsLoading = () => { + return ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default DelegationsLoading; diff --git a/frontend/src/app/(routes)/staking/components/loaders/SummaryLoading.tsx b/frontend/src/app/(routes)/staking/components/loaders/SummaryLoading.tsx new file mode 100644 index 000000000..ab35e3771 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/loaders/SummaryLoading.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const SummaryLoading = () => { + return ( +
    +
    + {[1, 2, 3, 4].map((_, index) => ( +
    + ))} +
    +
    + ); +}; + +export default SummaryLoading; diff --git a/frontend/src/app/(routes)/staking/components/loaders/UnbondingLoading.tsx b/frontend/src/app/(routes)/staking/components/loaders/UnbondingLoading.tsx new file mode 100644 index 000000000..a6f7fee49 --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/loaders/UnbondingLoading.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const UnbondingLoading = () => { + return ( +
    + {[1, 2, 3].map((_, index) => ( +
    + ))} +
    + ); +}; + +export default UnbondingLoading; diff --git a/frontend/src/app/(routes)/staking/components/loaders/ValidatorTableLoading.tsx b/frontend/src/app/(routes)/staking/components/loaders/ValidatorTableLoading.tsx new file mode 100644 index 000000000..a8b63bc7b --- /dev/null +++ b/frontend/src/app/(routes)/staking/components/loaders/ValidatorTableLoading.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +const ValidatorTableLoading = () => { + return ( +
    +
    +
    Validator
    +
    +
    +
    + + + + {[ + 'Rank', + 'Validator', + 'Commission', + 'Voting Power', + 'Status', + '', + ].map((header, hIndex) => ( + + ))} + + + + {Array(3) + .fill(null) + .map((_, colIndex) => ( + + {Array(6) + .fill(null) + .map((_, colIndex) => ( + + ))} + + ))} + +
    +
    + {header} +
    +
    +
    +
    +
    +
    + ); +}; + +export default ValidatorTableLoading; diff --git a/frontend/src/app/(routes)/staking/error.tsx b/frontend/src/app/(routes)/staking/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/staking/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/staking/loading.tsx b/frontend/src/app/(routes)/staking/loading.tsx new file mode 100644 index 000000000..4e5e5db35 --- /dev/null +++ b/frontend/src/app/(routes)/staking/loading.tsx @@ -0,0 +1,32 @@ +'use client'; + +import React from 'react'; +import SummaryLoading from './components/loaders/SummaryLoading'; +import UnbondingLoading from './components/loaders/UnbondingLoading'; +import DelegationsLoading from './components/loaders/DelegationsLoading'; + +const loading = () => ( + <> +
    +
    +
    +
    Staking
    +
    + Here's an overview of your staked assets, including delegation + and undelegation details, and your total staked balance. +
    +
    +
    + + {/* Staking Summary */} + +
    + {/* Unbonding */} + + {/* Delegations */} + +
    + +); + +export default loading; diff --git a/frontend/src/app/(routes)/staking/page.tsx b/frontend/src/app/(routes)/staking/page.tsx index 0c54bb831..1f78978d8 100644 --- a/frontend/src/app/(routes)/staking/page.tsx +++ b/frontend/src/app/(routes)/staking/page.tsx @@ -1,9 +1,16 @@ import React from 'react'; -import Staking from './Staking'; +import StakingDashboard from './components/StakingDashboard'; +// import Staking from './Staking'; +// import ValidatorTable from './components/ValidatorTable'; +// import StakingVal from './components/StakingVal'; +// import StakingDashboard from './components/StakingDashboard'; const page = () => { return (
    - + {/* */} + {/* */} + {/* */} +
    ); }; diff --git a/frontend/src/app/(routes)/staking/selectors/validatorsSelectors.ts b/frontend/src/app/(routes)/staking/selectors/validatorsSelectors.ts new file mode 100644 index 000000000..5bfe960e6 --- /dev/null +++ b/frontend/src/app/(routes)/staking/selectors/validatorsSelectors.ts @@ -0,0 +1,4 @@ +import { RootState } from "@/store/store"; + +export const selectFilteredValidators = (state: RootState) => state.staking.filteredValidators; +export const selectSearchQuery = (state: RootState) => state.staking.searchQuery; \ No newline at end of file diff --git a/frontend/src/app/(routes)/staking/staking.css b/frontend/src/app/(routes)/staking/staking.css index 748bf00c3..7bdd15a5b 100644 --- a/frontend/src/app/(routes)/staking/staking.css +++ b/frontend/src/app/(routes)/staking/staking.css @@ -34,7 +34,7 @@ .staking-card-action-button, .cancel-unbonding-btn { - @apply min-w-[80px] cursor-pointer rounded-lg text-white text-xs font-medium leading-5 tracking-[0.48px] h-8 flex items-center justify-center px-3; + @apply min-w-[80px] cursor-pointer rounded-lg text-xs font-medium leading-5 tracking-[0.48px] h-8 flex items-center justify-center px-3; background: linear-gradient( 180deg, @@ -135,7 +135,7 @@ } .error-chip { - @apply text-[12px] rounded-lg bg-[#ff00005b] text-white text-center leading-normal max-w-fit py-1 px-2 truncate; + @apply text-[12px] rounded-lg bg-[#ff00005b] text-center leading-normal max-w-fit py-1 px-2 truncate; } .no-data { @@ -143,7 +143,7 @@ } .amount-options { - @apply text-white text-sm not-italic font-normal leading-[normal] rounded-[100px]; + @apply text-sm not-italic font-normal leading-[normal] rounded-[100px]; background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(2px); } @@ -161,3 +161,68 @@ ), lightgray 50% / cover no-repeat; } + +.update-address-btn { + @apply rounded-2xl font-medium tracking-[0.64px] px-10 py-[10px] flex justify-center items-center; +} +.staking-rewards-background { + @apply flex justify-center items-center gap-2.5 px-10 py-10 rounded-3xl; + background: rgba(255, 255, 255, 0.1); +} +.staking-rewards-actions-btn { + @apply px-6 py-3 rounded-2xl; + background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +} + +.claim-button { + @apply py-[10px] px-10 rounded-2xl flex justify-center items-center; + background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +} +.amount-input-field { + @apply bg-transparent w-full border-none focus:outline-none text-[40px] font-bold placeholder:text-[#FFFFFF33]; +} + +.amount-input-field::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.staking-alert { + @apply flex flex-col items-start gap-2 px-6 py-4 rounded-2xl; + background: rgba(255, 255, 255, 0.08); +} +.unBonding-card { + @apply flex flex-col items-start gap-10 px-4 py-6 rounded-2xl; + /* background: rgba(255, 255, 255, 0.02); */ + background: linear-gradient(48deg, rgb(255 255 255 / 3%) 0%, rgb(153 153 153 / 25%) 100%); +} +.delegations-card { + @apply flex flex-col justify-center items-start gap-6 p-4 rounded-2xl; + /* background: rgba(255, 255, 255, 0.02); */ + background: linear-gradient(48deg, rgb(255 255 255 / 3%) 0%, rgb(153 153 153 / 25%) 100%); +} +.staked-amount-red-badge { + @apply border flex justify-center items-center gap-2 px-3 py-0 rounded-[100px] border-solid border-[#F15757] h-[25px]; + background: rgba(241, 87, 87, 0.5); +} +.status-active { + @apply flex h-2 w-2 justify-center items-center gap-2 border rounded-full border-solid border-[#2BA472]; + background: rgba(43, 164, 114, 0.5); +} +.status-jailed { + @apply flex h-2 w-2 justify-center items-center gap-2 border rounded-full border-solid border-[#D92101]; + background: rgba(217, 33, 1, 0.5); +} +.status-unbonded { + @apply border h-2 w-2 flex justify-center items-center rounded-full border-solid border-[#FFC13C]; + background: rgba(255, 193, 60, 0.5); +} +.reDelegate-dropdown { + @apply backdrop-blur-[15px] flex flex-col items-start rounded-2xl; + background: rgba(255, 255, 255, 0.08); +} +.staking-summary-card { + @apply flex-1 rounded-2xl p-6 flex flex-col justify-center items-center gap-4 h-[92px]; + /* background: rgba(255, 255, 255, 0.02); */ + background: linear-gradient(48deg, rgb(255 255 255 / 3%) 0%, rgb(153 153 153 / 25%) 100%); +} diff --git a/frontend/src/app/(routes)/staking/styles.ts b/frontend/src/app/(routes)/staking/styles.ts index 4098479f6..80a639ecc 100644 --- a/frontend/src/app/(routes)/staking/styles.ts +++ b/frontend/src/app/(routes)/staking/styles.ts @@ -1,19 +1,19 @@ export const paginationComponentStyles = { '& .MuiPaginationItem-page': { - color: '#fff', '&:hover': { - backgroundColor: '#ffffff1a', + backgroundColor: '#FFFFFF05', }, fontSize: '12px', minWidth: '24px', height: '24px', borderRadius: '4px', + color: '#ffffff80', + fontWeight: '200', }, '& .Mui-selected': { - background: 'linear-gradient(180deg, #4AA29C 0%, #8B3DA7 100%)', - '&:hover': { - opacity: '0.95', - }, + backgroundColor: '#FFFFFF05', + fontWeight: '600', + color: '#ffffff', }, '& .MuiPaginationItem-icon': { color: '#fff', @@ -67,3 +67,26 @@ export const textFieldInputPropStyles = { appearance: 'none', }, }; + +export const withdrawAddressFieldStyles = { + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '1px solid transparent', + borderRadius: '16px', + color: 'white', + }, + '& .Mui-focused': { + border: '1px solid #ffffff4a', + borderRadius: '16px', + }, + '& .Mui-disabled': { + WebkitTextFillColor: '#ffffff6b !important', + }, +}; diff --git a/frontend/src/app/(routes)/transactions/builder/[network]/PageMultiops.tsx b/frontend/src/app/(routes)/transactions/builder/[network]/PageMultiops.tsx new file mode 100644 index 000000000..c443951e0 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/builder/[network]/PageMultiops.tsx @@ -0,0 +1,143 @@ +'use client'; + +import EmptyScreen from '@/components/common/EmptyScreen'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import React, { useEffect, useState } from 'react'; +import PageHeader from '@/components/common/PageHeader'; +import TxnBuilder from '@/components/txn-builder/TxnBuilder'; +import { TxStatus } from '@/types/enums'; +import { txExecuteMultiMsg } from '@/store/features/multiops/multiopsSlice'; +import { parseBalance } from '@/utils/denom'; +import { getBalances } from '@/store/features/bank/bankSlice'; +import { TXN_BUILDER_DESCRIPTION } from '@/utils/constants'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; + +const PageMultiops = ({ paramChain }: { paramChain: string }) => { + const dispatch = useAppDispatch(); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const chainName = paramChain.toLowerCase(); + const validChain = chainName in nameToChainIDs; + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + const showAuthzAlert = useGetShowAuthzAlert(); + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + + return ( +
    +
    + +
    + {validChain ? ( + <> + {isWalletConnected ? ( + + ) : ( +
    + +
    + )} + + ) : ( + <> +
    + - The {chainName} is not supported - +
    + + )} +
    + ); +}; + +export default PageMultiops; + +const PageMultiopsEntry = ({ chainName }: { chainName: string }) => { + const dispatch = useAppDispatch(); + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const chainID = nameToChainIDs?.[chainName]; + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const basicChainInfo = getChainInfo(chainID); + const { + address, + prefix, + rest, + rpc, + baseURL, + restURLs: baseURLs, + } = basicChainInfo; + const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); + const currency = { + coinDenom: displayDenom, + coinDecimals: decimals, + coinMinimalDenom: minimalDenom, + }; + + const [availableBalance, setAvailableBalance] = useState(0); + + const txStatus = useAppSelector((state) => state.multiops.tx.status); + + const onSubmit = (data: TxnBuilderForm) => { + dispatch( + txExecuteMultiMsg({ + basicChainInfo, + aminoConfig: basicChainInfo.aminoConfig, + denom: currency.coinMinimalDenom, + feeAmount: data.fees, + feegranter: '', + memo: data.memo, + msgs: data.msgs, + prefix, + rest, + rpc, + gas: data.gas, + address, + }) + ); + }; + + const balance = useAppSelector( + (state) => state.bank.balances?.[chainID]?.list + ); + useEffect(() => { + if (balance) { + setAvailableBalance( + parseBalance(balance, currency.coinDecimals, currency.coinMinimalDenom) + ); + } + }, [balance]); + + useEffect(() => { + if (chainID) { + dispatch( + getBalances({ + address, + baseURL, + baseURLs, + chainID, + }) + ); + } + }, []); + + return ( + + ); +}; diff --git a/frontend/src/app/(routes)/transactions/builder/[network]/page.tsx b/frontend/src/app/(routes)/transactions/builder/[network]/page.tsx new file mode 100644 index 000000000..57369720c --- /dev/null +++ b/frontend/src/app/(routes)/transactions/builder/[network]/page.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import '@/app/(routes)/multiops/multiops.css'; +import PageMultiops from './PageMultiops'; + +const page = ({ params: { network } }: { params: { network: string } }) => { + return ; +}; + +export default page; diff --git a/frontend/src/app/(routes)/transactions/builder/page.tsx b/frontend/src/app/(routes)/transactions/builder/page.tsx new file mode 100644 index 000000000..0023b2f61 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/builder/page.tsx @@ -0,0 +1,52 @@ +'use client'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import PageHeader from '@/components/common/PageHeader'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setChangeNetworkDialogOpen } from '@/store/features/common/commonSlice'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import { TXN_BUILDER_DESCRIPTION } from '@/utils/constants'; +import React from 'react'; + +const Page = () => { + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const openChangeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: true })); + }; + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + + return ( +
    + +
    +
    + {isWalletConnected ? ( + + ) : ( + + )} +
    +
    +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/transactions/history/SearchTransaction.tsx b/frontend/src/app/(routes)/transactions/history/SearchTransaction.tsx new file mode 100644 index 000000000..109d1bb36 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/SearchTransaction.tsx @@ -0,0 +1,103 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getAnyChainTransaction } from '@/store/features/recent-transactions/recentTransactionsSlice'; +import React, { useEffect, useState } from 'react'; +import SearchTransactionHash from './[network]/components/SearchTransactionHash'; +import { TxStatus } from '@/types/enums'; +import { parseTxnData } from '@/utils/util'; +import Transaction from './[network]/components/Transaction'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { CircularProgress } from '@mui/material'; + +const SearchTransaction = () => { + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const [searchQuery, setSearchQuery] = useState(''); + + const handleSearchQueryChange = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + }; + + const handleClearSearch = () => { + setSearchQuery(''); + }; + + const onSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (searchQuery) dispatch(getAnyChainTransaction({ txhash: searchQuery })); + }; + + const loading = useAppSelector( + (state) => state.recentTransactions.txn?.status + ); + + const txnResult = useAppSelector( + (state) => state.recentTransactions.txn?.data?.[0] + ); + const txnStatus = useAppSelector( + (state) => state.recentTransactions.txn.status + ); + const { txHash = '' } = txnResult ? parseTxnData(txnResult) : {}; + + const [chainID, setChainID] = useState(''); + const { chainName } = getChainInfo(chainID); + + useEffect(() => { + if (txnResult) { + setChainID(txnResult.chain_id); + } + }, [txnResult]); + + return ( +
    +
    + + + + + {loading === TxStatus.PENDING ? ( +
    + +
    + Fetching transaction info +
    +
    + ) : ( + <> + {txnResult && chainID && chainName ? ( + + ) : null} + + )} + {txnStatus === TxStatus.REJECTED ? : null} +
    + ); +}; + +export default SearchTransaction; + +const TransactionNotFound = () => { + const txSearchError = useAppSelector( + (state) => state.recentTransactions.txn.error + ); + + if (txSearchError) + return ( +
    +

    + Sorry, the transaction you're looking for is not found. +

    +
    + ); + return <>; +}; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/[hash]/error.tsx b/frontend/src/app/(routes)/transactions/history/[network]/[hash]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/[hash]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/[hash]/loading.tsx b/frontend/src/app/(routes)/transactions/history/[network]/[hash]/loading.tsx new file mode 100644 index 000000000..81e6fd2ed --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/[hash]/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import React from 'react'; +import TransactionLoading from '../../loaders/TransactionLoading'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/[hash]/page.tsx b/frontend/src/app/(routes)/transactions/history/[network]/[hash]/page.tsx new file mode 100644 index 000000000..0f4d8f990 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/[hash]/page.tsx @@ -0,0 +1,77 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import React, { useEffect } from 'react'; +import Transaction from '../components/Transaction'; +import useGetTransactions from '@/custom-hooks/useGetTransactions'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import { TxStatus } from '@/types/enums'; +import TransactionLoading from '../../loaders/TransactionLoading'; + +const Page = () => { + const params = useParams(); + const paramHash = params.hash; + + const paramTxHash = typeof paramHash === 'string' ? [paramHash] : paramHash; + + const nameToChainsIDs = useAppSelector( + (state: RootState) => state.common.nameToChainIDs + ); + + const paramChains = params.network; + + const arrChainNames = + typeof paramChains === 'string' ? [paramChains] : paramChains; + + const chainID = nameToChainsIDs[arrChainNames[0]]; + + const txnHistory = useGetTransactions({ chainID }); + const txnStatus = useAppSelector( + (state) => state.recentTransactions.txn.status + ); + const txnResult = useAppSelector( + (state) => state.recentTransactions.txn?.data?.[0] + ); + + useEffect(() => { + txnHistory.fetchTransaction(paramTxHash[0]); + }, []); + + return ( +
    + {txnStatus === TxStatus.PENDING ? ( + + ) : ( + <> + {txnResult && chainID && ( + + )} + + )} + {txnStatus === TxStatus.REJECTED ? : null} +
    + ); +}; + +export default Page; + +const TransactionNotFound = () => { + const txSearchError = useAppSelector( + (state) => state.recentTransactions.txn.error + ); + + if (txSearchError) + return ( +
    +

    + Sorry, the transaction you're looking for is not found. +

    +
    + ); + return <>; +}; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/components/History.tsx b/frontend/src/app/(routes)/transactions/history/[network]/components/History.tsx new file mode 100644 index 000000000..643813dd5 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/components/History.tsx @@ -0,0 +1,38 @@ +import ChainNotFound from '@/components/ChainNotFound'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import React from 'react'; +import TransactionHistoryDashboard from './TransactionHistoryDashboard'; +import { useParams } from 'next/navigation'; +import PageHeader from '@/components/common/PageHeader'; + +const History = () => { + const { network } = useParams(); + const chainName = typeof network === 'string' ? network : ''; + + const nameToChainIDs = useAppSelector( + (state: RootState) => state.common.nameToChainIDs + ); + + const isValidChain = Object.keys(nameToChainIDs).some( + (chain) => chainName.toLowerCase() === chain.toLowerCase() + ); + + if (isValidChain) { + const chainID = nameToChainIDs[chainName]; + + return ( +
    + + +
    + ); + } + + return ; +}; + +export default History; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/components/SearchTransactionHash.tsx b/frontend/src/app/(routes)/transactions/history/[network]/components/SearchTransactionHash.tsx new file mode 100644 index 000000000..755a1cf0e --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/components/SearchTransactionHash.tsx @@ -0,0 +1,41 @@ +import Image from 'next/image'; +import React from 'react'; + +const SearchTransactionHash = ({ + searchQuery, + handleSearchQueryChange, + handleClearSearch, +}: { + searchQuery: string; + handleSearchQueryChange: (e: React.ChangeEvent) => void; + handleClearSearch?: () => void; +}) => { + return ( +
    +
    + +
    + + {searchQuery && handleClearSearch && ( + + )} +
    +
    +
    + ); +}; + +export default SearchTransactionHash; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/components/Transaction.tsx b/frontend/src/app/(routes)/transactions/history/[network]/components/Transaction.tsx new file mode 100644 index 000000000..f34ec8198 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/components/Transaction.tsx @@ -0,0 +1,203 @@ +import React, { useState } from 'react'; +import TransactionHeader from './TransactionHeader'; +import Image from 'next/image'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import { getLocalTime } from '@/utils/dataTime'; +import NumberFormat from '@/components/common/NumberFormat'; +import { get } from 'lodash'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import TxMsg from './TxMsg'; +import { getTxnURL, parseTxnData } from '@/utils/util'; +import { buildMessages } from '@/utils/transaction'; +import { txRepeatTransaction } from '@/store/features/recent-transactions/recentTransactionsSlice'; +import DialogLoader from '@/components/common/DialogLoader'; +import { TxStatus } from '@/types/enums'; +import CustomDialog from '@/components/common/CustomDialog'; + +const Transaction = ({ + chainName, + hash, + chainID, + isSearchPage = false, +}: { + chainName: string; + hash: string; + chainID: string; + isSearchPage?: boolean; +}) => { + const dispatch = useAppDispatch(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const basicChainInfo = getChainInfo(chainID); + const { chainLogo, explorerTxHashEndpoint } = basicChainInfo; + + const txnRepeatStatus = useAppSelector( + (state) => state.recentTransactions?.txnRepeat?.status + ); + const loading = txnRepeatStatus === TxStatus.PENDING; + + const txnResult = useAppSelector( + (state: RootState) => state.recentTransactions.txn?.data?.[0] + ); + const { success = false, messages = [] } = txnResult + ? parseTxnData(txnResult) + : {}; + const formattedMessages = messages ? buildMessages(messages) : []; + const disableAction = formattedMessages.length === 0 || !success; + + const [viewRawOpen, setViewRawOpen] = useState(false); + + const onRepeatTxn = () => { + dispatch( + txRepeatTransaction({ + basicChainInfo, + feegranter: '', + messages: formattedMessages, + }) + ); + }; + + const getAmount = (amount: number) => { + const { decimals, displayDenom } = getDenomInfo(chainID); + + return { + amount: (amount / 10 ** decimals).toFixed(6), + denom: displayDenom, + }; + }; + + const [expandedIndex, setExpandedIndex] = useState(0); + + const toggleExpand = (index: number) => { + setExpandedIndex(expandedIndex === index ? null : index); + }; + + const msgs = txnResult?.messages; + + return ( +
    + +
    +
    +
    +
    +
    +

    Network

    +
    + network-logo +

    {chainName}

    +
    +
    +
    +

    Fees

    +
    + +
    +
    +
    +
    +
    Messages: {msgs?.length}
    +
    + +
    +
    + {msgs?.map((msg, mIndex) => ( + + ))} +
    +
    + + {/* Sidebar content */} +
    +
    +
    +

    Gas Used / Wanted

    +
    + {txnResult?.gas_used || 0} + / + {txnResult?.gas_wanted || 0} +
    +
    +
    + +
    +
    +

    TimeStamp

    +
    + {getLocalTime(txnResult?.timestamp || '')} +
    +
    +
    + +
    +
    +

    Height

    +
    {txnResult?.height}
    +
    +
    + +
    +
    +

    Memo

    +
    {txnResult?.memo || '-'}
    +
    +
    +
    +
    + + setViewRawOpen(false)} + title="Raw Transaction" + > +
    + {txnResult ? ( +
    {JSON.stringify(txnResult, undefined, 2)}
    + ) : ( +
    - No Data -
    + )} +
    +
    +
    + ); +}; + +export default Transaction; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionCard.tsx b/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionCard.tsx new file mode 100644 index 000000000..3d4ba5a30 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionCard.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import Copy from '@/components/common/Copy'; +import { parseTxnData, shortenString } from '@/utils/util'; +// import NewTxnMsg from '@/components/NewTxnMsg'; +import Link from 'next/link'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { buildMessages, formattedMsgType } from '@/utils/transaction'; +import { txRepeatTransaction } from '@/store/features/recent-transactions/recentTransactionsSlice'; +import CustomButton from '@/components/common/CustomButton'; +import TxnTimeStamp from './TxnTimeStamp'; + +const TransactionCard = ({ + txn, + // currency, + basicChainInfo, +}: { + txn: ParsedTransaction; + // currency: Currency; + basicChainInfo: BasicChainInfo; +}) => { + const dispatch = useAppDispatch(); + const { success, messages, txHash, timeStamp } = parseTxnData(txn); + const formattedMessages = buildMessages(messages); + const disableAction = formattedMessages.length === 0 || !success; + const onRepeatTxn = () => { + dispatch( + txRepeatTransaction({ + basicChainInfo, + feegranter: '', + messages: formattedMessages, + }) + ); + }; + return ( +
    + +
    +
    +
    + +

    {shortenString(txHash, 24)}

    + + +
    +
    + {txn?.messages?.slice(0, 4).map((msg, index) => ( +
    + + {formattedMsgType(msg?.['@type'])} + +
    + ))} + {txn?.messages?.length > 4 && ( +
    + +{txn.messages.length - 4} +
    + )} +
    +
    + +
    +
    + ); +}; + +export default TransactionCard; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionHeader.tsx b/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionHeader.tsx new file mode 100644 index 000000000..1ef568fa3 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionHeader.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import Image from 'next/image'; +import Copy from '@/components/common/Copy'; +import CustomButton from '@/components/common/CustomButton'; +import Link from 'next/link'; +import { REDIRECT_ICON } from '@/constants/image-names'; + +type Status = 'success' | 'failed'; + +interface TransactionHeaderProps { + status: Status; + hash: string; + onRepeatTxn: () => void; + disableAction: boolean; + goBackUrl: string; + isSearchPage?: boolean; + mintscanURL: string; + rawLog?: string; +} + +const TransactionHeader: React.FC = ({ + status, + hash, + disableAction, + onRepeatTxn, + goBackUrl, + isSearchPage, + mintscanURL, + rawLog +}) => { + const isSuccess = status === 'success'; + const textColorClass = isSuccess ? 'text-[#2BA472]' : 'text-[#FA5E42]'; + + return ( +
    +
    + {isSearchPage ? null : ( + + Go back + + )} +
    +
    + {isSuccess +
    + {isSuccess ? 'Transaction Successful' : 'Transaction Failed'} +
    +
    + {!isSuccess &&
    } + { + !isSuccess &&
    +
    + {isSuccess ? null : rawLog} +
    +
    +
    || null + } +
    +
    +
    +
    +
    {hash}
    + +
    +
    +
    +
    +
    + {isSearchPage ? null : ( + + )} + {mintscanURL ? ( + +
    View on Mintscan
    + View Proposal + + ) : null} +
    +
    +
    + +
    +
    + ); +}; + +export default TransactionHeader; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionHistoryDashboard.tsx b/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionHistoryDashboard.tsx new file mode 100644 index 000000000..96bac507f --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/components/TransactionHistoryDashboard.tsx @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import SearchTransactionHash from './SearchTransactionHash'; +import useGetTransactions from '@/custom-hooks/useGetTransactions'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { RootState } from '@/store/store'; +import TransactionCard from './TransactionCard'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import DialogLoader from '@/components/common/DialogLoader'; +import { TxStatus } from '@/types/enums'; +import { Pagination } from '@mui/material'; +import { paginationComponentStyles } from '@/utils/commonStyles'; +import TxnsLoading from '../../loaders/TxnsLoading'; +import useInitTransactions from '@/custom-hooks/useInitTransactions'; +import { NO_DATA_ILLUSTRATION } from '@/constants/image-names'; +import EmptyScreen from '@/components/common/EmptyScreen'; + +const ITEMS_PER_PAGE = 7; + +const TransactionHistoryDashboard = ({ chainID }: { chainID: string }) => { + useInitTransactions({ chainID }); + const { getChainInfo } = useGetChainInfo(); + + const basicChainInfo = getChainInfo(chainID); + + const [currentPage, setCurrentPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(''); + const txnHistory = useGetTransactions({ chainID }); + + const handleSearchQueryChange = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + txnHistory.fetchTransaction(e.target.value); + }; + + const transactions = useAppSelector((state: RootState) => + searchQuery + ? state.recentTransactions.txn?.data + : state.recentTransactions.txns?.data + ); + const txnRepeatStatus = useAppSelector( + (state) => state.recentTransactions?.txnRepeat?.status + ); + const txnsLoading = useAppSelector( + (state) => state.recentTransactions.txns.status + ); + const loading = txnRepeatStatus === TxStatus.PENDING; + const handlePageChange = (value: number) => { + const offset = (value - 1) * ITEMS_PER_PAGE; + txnHistory.fetchTransactions(ITEMS_PER_PAGE, offset); + setCurrentPage(value); + }; + + const totalCount = useAppSelector( + (state) => state.recentTransactions?.txns?.total + ); + + const pagesCount = Math.ceil(totalCount / ITEMS_PER_PAGE); + const showPagination = + transactions?.length && + txnsLoading !== TxStatus.PENDING && + pagesCount !== 0; + + return ( +
    +
    + +
    +
    + {transactions?.map((txn) => ( + + ))} +
    + {txnsLoading === TxStatus.PENDING ? ( + + ) : ( + <> + {transactions?.length === 0 ? ( + + ) : null} + + )} + {showPagination ? ( +
    + { + handlePageChange(value); + }} + /> +
    + ) : null} + +
    + ); +}; + +export default TransactionHistoryDashboard; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/components/TxMsg.tsx b/frontend/src/app/(routes)/transactions/history/[network]/components/TxMsg.tsx new file mode 100644 index 000000000..891ee9e1c --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/components/TxMsg.tsx @@ -0,0 +1,347 @@ +import { getTypeURLName } from '@/utils/util'; +import { get } from 'lodash'; +import Image from 'next/image'; +import React from 'react'; +import { + SEND_TYPE_URL, + DELEGATE_TYPE_URL, + REDELEGATE_TYPE_URL, + UNDELEGATE_TYPE_URL, + WITHDRAW_DELEGATE_REWARD, + MSG_AUTHZ_EXEC, + VOTE_TYPE_URL, + MSG_AUTHZ_GRANT, + MSG_AUTHZ_REVOKE, + MSG_REVOKE_ALLOWANCE, + MSG_GRANT_ALLOWANCE, +} from '@/utils/constants'; +import { shortenAddress } from '@/utils/util'; +import NumberFormat from '@/components/common/NumberFormat'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getLocalTime } from '@/utils/dataTime'; + +interface TxMsgProps { + msg: NewMsg; + expandedIndex: number | null; + toggleExpand: (value: number) => void; + mIndex: number; + chainID: string; +} + +const voteOptions: Record = { + 'VOTE_OPTION_YES': 'Yes', + 'VOTE_OPTION_NO': 'Abstain', + 'VOTE_OPTION_ABSTAIN': 'No', + 'VOTE_OPTION_NO_WITH_VETO': 'No With Veto', +}; + +const TxMsg: React.FC = ({ msg, expandedIndex, chainID, toggleExpand, mIndex }) => { + const msgType = get(msg, '@type', ''); + + const { getDenomInfo } = useGetChainInfo(); + + const getAmount = (amount: number) => { + const { decimals, displayDenom } = getDenomInfo(chainID); + return { + amount: (amount / 10 ** decimals).toFixed(6), + denom: displayDenom, + }; + }; + + const renderMessageDetails = () => { + switch (msgType) { + case UNDELEGATE_TYPE_URL: + return ( +
    +
    +

    Validator

    + {shortenAddress(get(msg, 'validator_address', ''), 24)} +
    +
    +

    Amount

    + +
    +
    + ); + case SEND_TYPE_URL: + return ( +
    +
    +

    From

    + {shortenAddress(get(msg, 'from_address', ''), 24)} +
    +
    +

    To

    + {shortenAddress(get(msg, 'to_address', ''), 24)} +
    +
    +

    Amount

    + +
    +
    + ); + case REDELEGATE_TYPE_URL: + return ( +
    +
    +

    Source Validator

    + + {shortenAddress(get(msg, 'validator_src_address', ''), 24)} + +
    +
    +

    Destination Validator

    + + {shortenAddress(get(msg, 'validator_dst_address', ''), 24)} + +
    +
    +

    Amount

    + +
    +
    + ); + case DELEGATE_TYPE_URL: + return ( +
    +
    +

    Validator

    + + {shortenAddress(get(msg, 'validator_address', ''), 24)} + +
    +
    +

    Amount

    + +
    +
    + ); + case WITHDRAW_DELEGATE_REWARD: + return ( +
    +
    +

    Validator

    + + {shortenAddress(get(msg, 'validator_address', ''), 24)} + +
    +
    +

    Delegator

    + + {shortenAddress(get(msg, 'delegator_address', ''), 24)} + +
    +
    + ); + case VOTE_TYPE_URL: + return ( +
    +
    +

    Voter

    + + {shortenAddress(get(msg, 'voter', ''), 24)} + +
    +
    +

    Proposal ID

    + + {get(msg, 'proposal_id', '')} + +
    +
    +

    Option

    + {voteOptions[get(msg, 'option')]} +
    +
    + ); + case MSG_AUTHZ_REVOKE: + return ( +
    +
    +

    Granter

    + + {shortenAddress(get(msg, 'granter', ''), 24)} + +
    +
    +

    Grantee

    + + {shortenAddress(get(msg, 'grantee', ''), 24)} + +
    +
    +

    Type

    + {getTypeURLName(get(msg, 'msg_type_url', ''))} +
    +
    + ); + case MSG_REVOKE_ALLOWANCE: + return ( +
    +
    +

    Granter

    + + {shortenAddress(get(msg, 'granter', ''), 24)} + +
    +
    +

    Grantee

    + + {shortenAddress(get(msg, 'grantee', ''), 24)} + +
    +
    + ); + case MSG_GRANT_ALLOWANCE: + return ( + <> +
    +
    +

    Granter

    + + {shortenAddress(get(msg, 'granter', ''), 24)} + +
    +
    +

    Grantee

    + + {shortenAddress(get(msg, 'grantee', ''), 24)} + +
    +
    +
    +
    +

    Expiry

    + {getLocalTime(get(msg, 'allowance.expiration', '-'))} +
    +
    +

    Spend Limit

    + +
    +
    + + + ); + case MSG_AUTHZ_GRANT: + return (<> +
    +
    +

    Granter

    + + {shortenAddress(get(msg, 'granter', ''), 24)} + +
    +
    +

    Grantee

    + + {shortenAddress(get(msg, 'grantee', ''), 24)} + +
    +
    +

    Type

    + {getTypeURLName(get(msg, 'grant.authorization.msg', '-'))} +
    +
    + +
    + { + get(msg, 'grant.authorization.deny_list.address') &&
    +

    Deny List

    + { + get(msg, 'grant.authorization.deny_list.address', []).map((a: string, i: number) => ( +

    {shortenAddress(a, 24)}

    + )) + } +
    || null + } + +
    +

    Expiry

    + {getLocalTime(get(msg, 'grant.expiration', '-'))} +
    + { + get(msg, 'grant.authorization.max_tokens') && +
    +

    Max Tokens

    + +
    || null + } + +
    + + ); + default: + return
    +
    +                        {JSON.stringify(msg, null, 2)}
    +                    
    +
    ; + } + }; + + return ( +
    +
    +
    toggleExpand(mIndex)}> +

    {getTypeURLName(msg?.['@type']) || msg?.['@type']}

    + drop-icon +
    +
    + {expandedIndex === mIndex && ( + <> + {renderMessageDetails()} + {msgType === MSG_AUTHZ_EXEC && + get(msg, 'msgs', []).map((m: NewMsg, index: number) => ( + + ))} + + )} +
    + ); +}; + +export default TxMsg; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/components/TxnTimeStamp.tsx b/frontend/src/app/(routes)/transactions/history/[network]/components/TxnTimeStamp.tsx new file mode 100644 index 000000000..dbfda7383 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/components/TxnTimeStamp.tsx @@ -0,0 +1,26 @@ +import Image from 'next/image'; +import React from 'react'; + +const TxnTimeStamp = ({ + success, + timeStamp, +}: { + success: boolean; + timeStamp: string; +}) => { + return ( +
    +

    {timeStamp}

    +

    + status-icon +

    +
    + ); +}; + +export default TxnTimeStamp; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/error.tsx b/frontend/src/app/(routes)/transactions/history/[network]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/loading.tsx b/frontend/src/app/(routes)/transactions/history/[network]/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/transactions/history/[network]/page.tsx b/frontend/src/app/(routes)/transactions/history/[network]/page.tsx new file mode 100644 index 000000000..4a816f9f0 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/[network]/page.tsx @@ -0,0 +1,39 @@ +'use client'; + +import EmptyScreen from '@/components/common/EmptyScreen'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import { RootState } from '@/store/store'; +import React from 'react'; +import History from './components/History'; + +const Page = () => { + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector( + (state: RootState) => state.wallet.connected + ); + + const connectWallet = () => { + dispatch(setConnectWalletOpen(true)); + }; + + return ( +
    + {isWalletConnected ? ( + + ) : ( +
    + +
    + )} +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/transactions/history/error.tsx b/frontend/src/app/(routes)/transactions/history/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/transactions/history/loaders/TransactionLoading.tsx b/frontend/src/app/(routes)/transactions/history/loaders/TransactionLoading.tsx new file mode 100644 index 000000000..7dc77e601 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/loaders/TransactionLoading.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +const TransactionLoading = () => { + return ( +
    + +
    +
    +
    +
    +
    +
    +

    +
    +
    +
    +
    +
    +

    +
    +
    +
    +
    + {Array(4) + .fill(null) + .map((index) => ( +
    +
    +

    + +
    +
    +

    +
    +
    +
    + ))} +
    +
    + +
    + {Array(3) + .fill(null) + .map((index) => ( +
    +
    +

    +
    +
    +
    + ))} +
    +
    +
    + ); +}; + +export default TransactionLoading; + +const TransactionHeader = () => { + return ( +
    +
    +
    +
    +
    +
    +
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/transactions/history/loaders/TxnsLoading.tsx b/frontend/src/app/(routes)/transactions/history/loaders/TxnsLoading.tsx new file mode 100644 index 000000000..7ff869856 --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/loaders/TxnsLoading.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +const TxnsLoading = () => { + return ( +
    + {Array(3) + .fill(null) + .map((index) => ( +
    +
    +
    +

    +

    +
    +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ))} +
    + ); +}; + +export default TxnsLoading; diff --git a/frontend/src/app/(routes)/transactions/history/loading.tsx b/frontend/src/app/(routes)/transactions/history/loading.tsx new file mode 100644 index 000000000..f4c8108df --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/loading.tsx @@ -0,0 +1,10 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/frontend/src/app/(routes)/transactions/history/page.tsx b/frontend/src/app/(routes)/transactions/history/page.tsx new file mode 100644 index 000000000..28fd12e0c --- /dev/null +++ b/frontend/src/app/(routes)/transactions/history/page.tsx @@ -0,0 +1,40 @@ +'use client'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import PageHeader from '@/components/common/PageHeader'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import React from 'react'; +import SearchTransaction from './SearchTransaction'; + +const Page = () => { + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + + return ( +
    + +
    + {isWalletConnected ? ( + + ) : ( + + )} +
    +
    + ); +}; + +export default Page; diff --git a/frontend/src/app/(routes)/transfers/SendPage.tsx b/frontend/src/app/(routes)/transfers/SendPage.tsx deleted file mode 100644 index 1eb4d6f6c..000000000 --- a/frontend/src/app/(routes)/transfers/SendPage.tsx +++ /dev/null @@ -1,436 +0,0 @@ -import React, { useState } from 'react'; -import Image from 'next/image'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { CircularProgress, InputAdornment } from '@mui/material'; -import { useForm } from 'react-hook-form'; -import AllAssets from './components/AllAssets'; -import useGetTxInputs from '@/custom-hooks/useGetTxInputs'; -import { txBankSend } from '@/store/features/bank/bankSlice'; -import CustomTextField, { - CustomMultiLineTextField, -} from '@/components/CustomTextField'; -import props from './customTextFields.json'; -import CustomSubmitButton from '@/components/CustomButton'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import { txTransfer } from '@/store/features/ibc/ibcSlice'; -import { TxStatus } from '@/types/enums'; -import { setError } from '@/store/features/common/commonSlice'; -import NoAssets from '@/components/illustrations/NoAssets'; -import { capitalizeFirstLetter } from '@/utils/util'; -import useAssetsCardNumber from '@/custom-hooks/useAssetsCardNumber'; - -const SendPage = ({ sortedAssets }: { sortedAssets: ParsedAsset[] }) => { - const [selectedAsset, setSelectedAsset] = useState(); - const [slicedAssetsIndex, setSlicedAssetIndex] = useState(0); - const dispatch = useAppDispatch(); - const { txSendInputs, txTransferInputs } = useGetTxInputs(); - const { isNativeTransaction, getChainIDFromAddress, getChainInfo } = - useGetChainInfo(); - const sendTxStatus = useAppSelector((state) => state.bank.tx.status); - const ibcTxStatus = useAppSelector((state) => state.ibc.txStatus); - const balancesLoading = useAppSelector((state) => state.bank.balancesLoading); - const sendProps = props.send; - const [allAssetsDialogOpen, setAllAssetsDialogOpen]: [ - boolean, - React.Dispatch>, - ] = useState(false); - const feeAmount = selectedAsset - ? getChainInfo(selectedAsset.chainID).feeAmount - : 0; - - const amountRules = { - ...sendProps.amount, - validate: { - invalid: (value: string) => - !isNaN(Number(value)) || 'Please enter a valid amount', - zeroAmount: (value: string) => - Number(value) !== 0 || 'Amount should be greater than 0', - insufficient: (value: string) => - Number(selectedAsset?.balance) >= Number(value) + feeAmount || - 'Insufficient funds', - }, - }; - - const { - handleSubmit, - control, - formState: { errors }, - getValues, - setValue, - } = useForm({ - defaultValues: { - amount: 0, - address: '', - memo: '', - }, - }); - - const [amountOption, setAmountOption] = useState(''); - - const amountInputProps = { - sx: sendProps.amount.inputProps.sx, - endAdornment: selectedAsset ? ( -
    -
    - - -
    - - {selectedAsset?.displayDenom} - -
    - ) : null, - }; - - const [isIBC, setIsIBC] = useState(false); - - const onSelectAsset = (asset: ParsedAsset, index: number) => { - if (selectedAsset == asset) return; - checkIfIBCTransaction(asset); - setSelectedAsset(asset); - setSlicedAssetIndex(index); - }; - - const checkIfIBCTransaction = (asset = selectedAsset) => { - const address = getValues('address'); - - const destinationChainID = getChainIDFromAddress(address); - if (!!asset && !!destinationChainID && destinationChainID != asset?.chainID) - setIsIBC(true); - else setIsIBC(false); - }; - - const onSubmit = (data: { - amount: number | undefined; - address: string; - memo: string; - }) => { - if (!selectedAsset) { - dispatch( - setError({ - type: 'error', - message: `Please select an asset`, - }) - ); - return; - } - if (!data.amount) { - dispatch( - setError({ - type: 'error', - message: `Amount can't be zero`, - }) - ); - - return; - } - - if (isNativeTransaction(selectedAsset.chainID, data.address)) { - const txInputs = txSendInputs( - selectedAsset.chainID, - data.address, - data.amount, - data.memo, - selectedAsset.denom, - selectedAsset.decimals - ); - dispatch(txBankSend(txInputs)); - } else { - const destChainID = getChainIDFromAddress(data.address); - - if (!destChainID) { - dispatch( - setError({ - type: 'error', - message: 'Invalid Address', - }) - ); - return; - } - - const txInputs = txTransferInputs( - selectedAsset.chainID, - destChainID, - data.address, - data.amount, - selectedAsset.denom, - selectedAsset.decimals - ); - - dispatch(txTransfer(txInputs)); - } - }; - - return ( -
    - {sortedAssets.length ? ( -
    -
    -
    -
    Assets
    - {!sortedAssets.length ? ( -
    No Assets Found
    - ) : !selectedAsset ? ( -
    Please select an Asset
    - ) : null} -
    - - - -
    -
    -
    -
    -
    -
    -
    - Recipient Address -
    - {errors.address ? ( -
    - {errors.address?.message} -
    - ) : null} -
    - -
    -
    -
    -
    - Amount -
    - {!!errors.amount && ( -
    - {errors.amount?.message} -
    - )} -
    -
    { - if (!selectedAsset) { - setAllAssetsDialogOpen(true); - } - }} - > - -
    -
    -
    -
    -
    -
    - Memo -
    -
    -
    - -
    -
    -
    - {isIBC && ( -
    -
    - warning -
    - This looks like a cross chain Transaction. Avoid IBC - transfers to centralized exchanges. Your assets may be lost. -
    -
    -
    - )} - - -
    - ) : balancesLoading ? ( -
    - -
    - ) : ( - - )} -
    - ); -}; - -export default SendPage; - -const Cards = ({ - assets, - selectedAsset, - onSelectAsset, - slicedAssetsIndex, -}: { - assets: ParsedAsset[]; - slicedAssetsIndex: number; - selectedAsset: ParsedAsset | undefined; - onSelectAsset: (asset: ParsedAsset, index: number) => void; -}) => { - const cardsCount = useAssetsCardNumber(); - const indexes = () => { - if (slicedAssetsIndex < cardsCount || !selectedAsset) - return { startIndex: 0, endIndex: cardsCount }; - return { startIndex: slicedAssetsIndex, endIndex: slicedAssetsIndex + 1 }; - }; - - const { startIndex, endIndex } = indexes(); - - - return assets.length ? ( -
    - {assets.slice(startIndex, endIndex).map((asset, index) => ( -
    onSelectAsset(asset, index)} - > -
    -
    - {asset.chainName} - - {/* {asset.chainName} */} -
    -
    - {asset.balance} -
    - -
    - {asset.displayDenom} -
    -
    -
    -
    - on {capitalizeFirstLetter(asset.chainName)}{' '} - {asset.type === 'ibc' ? ' (IBC)' : ''} -
    -
    -
    - ))} -
    - ) : null; -}; diff --git a/frontend/src/app/(routes)/transfers/[...chainNames]/error.tsx b/frontend/src/app/(routes)/transfers/[...chainNames]/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/[...chainNames]/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/transfers/[...chainNames]/loading.tsx b/frontend/src/app/(routes)/transfers/[...chainNames]/loading.tsx new file mode 100644 index 000000000..0b08e2215 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/[...chainNames]/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/transfers/components/AllAssets.tsx b/frontend/src/app/(routes)/transfers/components/AllAssets.tsx deleted file mode 100644 index 802a3f61b..000000000 --- a/frontend/src/app/(routes)/transfers/components/AllAssets.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import DialogAllAssets from './DialogAllAssets'; - -const AllAssets = ({ - assets, - selectedAsset, - onSelectAsset, - dialogOpen, - setDialogOpen, -}: { - assets: ParsedAsset[]; - selectedAsset: ParsedAsset | undefined; - onSelectAsset: (asset: ParsedAsset, index: number) => void; - dialogOpen: boolean; - setDialogOpen: React.Dispatch>; -}) => { - const handleDialogClose = () => setDialogOpen(false); - - return ( -
    - {assets.length > 1 ? ( -
    -
    setDialogOpen(true)}> - View All -
    -
    - ) : null} - - -
    - ); -}; - -export default AllAssets; diff --git a/frontend/src/app/(routes)/transfers/components/DialogAllAssets.tsx b/frontend/src/app/(routes)/transfers/components/DialogAllAssets.tsx deleted file mode 100644 index dfed18adf..000000000 --- a/frontend/src/app/(routes)/transfers/components/DialogAllAssets.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import { Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import { customDialogPaper } from '../styles'; -import { capitalizeFirstLetter } from '@/utils/util'; - -const DialogAllAssets = ({ - dialogOpen, - assets, - selectedAsset, - onSelectAsset, - handleDialogClose, -}: { - dialogOpen: boolean; - assets: ParsedAsset[]; - selectedAsset: ParsedAsset | undefined; - onSelectAsset: (asset: ParsedAsset, index: number) => void; - handleDialogClose: () => void; -}) => { - return ( - - -
    -
    -
    { - handleDialogClose(); - }} - > - Close -
    -
    -
    -
    -

    - All Assets -

    -
    -
    - -
    - {assets.map((asset, index) => ( -
    { - onSelectAsset(asset, index); - handleDialogClose(); - }} - > -
    -
    - {asset.chainName} - - {/* {asset.chainName} */} -
    -
    - {asset.balance} -
    - -
    - {asset.displayDenom} -
    -
    -
    -
    - on {capitalizeFirstLetter(asset.chainName)}{' '} - {asset.type === 'ibc' ? ' (IBC)' : ''} -
    -
    -
    - ))} -
    -
    -
    -
    - ); -}; - -export default DialogAllAssets; diff --git a/frontend/src/app/(routes)/transfers/components/Messages.tsx b/frontend/src/app/(routes)/transfers/components/Messages.tsx index c04984bae..62c2b3a31 100644 --- a/frontend/src/app/(routes)/transfers/components/Messages.tsx +++ b/frontend/src/app/(routes)/transfers/components/Messages.tsx @@ -2,7 +2,7 @@ import { Pagination } from '@mui/material'; import Image from 'next/image'; import React, { useMemo, useState } from 'react'; import { paginationComponentStyles } from '../../staking/styles'; -import { formattedSerialize } from '@/txns/bank/send'; +import { formatSendMessage } from '@/txns/bank/send'; import { MULTI_TRANSFER_MSG_COUNT } from '../../../../utils/constants'; import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; @@ -24,13 +24,11 @@ const Messages = ({ return (
    -
    -
    -
    - Messages -
    +
    +
    +
    Messages
    { onDeleteAll(); setIndex(0); @@ -39,39 +37,27 @@ const Messages = ({ Clear All
    -
    - {msgs.length ? ( - msgs - .slice( - MULTI_TRANSFER_MSG_COUNT * index, - MULTI_TRANSFER_MSG_COUNT * index + MULTI_TRANSFER_MSG_COUNT - ) - .map((msg, offset) => ( -
    - - -
    -
    - )) - ) : ( -
    - no messages -
    - )} +
    + {msgs?.length + ? msgs + .slice( + MULTI_TRANSFER_MSG_COUNT * index, + MULTI_TRANSFER_MSG_COUNT * index + MULTI_TRANSFER_MSG_COUNT + ) + .map((msg, offset) => ( +
    + +
    + )) + : null}
    -
    -
    +
    +
    {pagesCount > 1 ? ( +
    - msg -
    - {formattedSerialize( +
    + {formatSendMessage( msg, originDenomInfo.decimals, originDenomInfo.originDenom @@ -121,10 +100,10 @@ const Message = ({
    cancel onDelete(index)} /> diff --git a/frontend/src/app/(routes)/transfers/components/MultiTransfer.tsx b/frontend/src/app/(routes)/transfers/components/MultiTransfer.tsx deleted file mode 100644 index 4bc2a08ff..000000000 --- a/frontend/src/app/(routes)/transfers/components/MultiTransfer.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import Messages from './Messages'; -import Summary from './Summary'; -import MultiTxUpload from './MultiTxUpload'; -import { useForm } from 'react-hook-form'; -import { CustomMultiLineTextField } from '@/components/CustomTextField'; -import props from '../customTextFields.json'; -import CustomSubmitButton from '@/components/CustomButton'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import useGetChainInfo from '../../../../custom-hooks/useGetChainInfo'; -import { multiTxns } from '@/store/features/bank/bankSlice'; -import { TxStatus } from '@/types/enums'; -import { setError } from '@/store/features/common/commonSlice'; -import { TransfersTab } from './TransfersPage'; - -const MultiTransfer = ({ - chainID, - tab, - handleTabChange, -}: { - chainID: string; - tab: TransfersTab; - handleTabChange: () => void; -}) => { - const [msgs, setMsgs] = useState([]); - const txPendingStatus = useAppSelector((state) => state.bank.tx.status); - - useEffect(() => { - if (txPendingStatus === TxStatus.IDLE) { - setMsgs([]); - } - }, [txPendingStatus]); - - const multiSendProps = props['multi-send']; - const dispatch = useAppDispatch(); - const { getChainInfo, getDenomInfo } = useGetChainInfo(); - - const { - handleSubmit, - control, - formState: { errors }, - } = useForm({ - defaultValues: { - memo: '', - }, - }); - - const addMsgs = (msgs: Msg[]) => { - setMsgs(msgs); - }; - - const onDelete = (index: number) => { - setMsgs((msgs) => { - return [...msgs.slice(0, index), ...msgs.slice(index + 1, msgs.length)]; - }); - }; - - const onDeleteAll = () => { - setMsgs([]); - }; - - const onSubmit = (data: { memo: string }) => { - if (txPendingStatus === TxStatus.PENDING) { - dispatch( - setError({ - type: 'error', - message: 'A transaction is still pending..', - }) - ); - return; - } - if (msgs.length === 0) { - dispatch( - setError({ - type: 'error', - message: 'No transactions found', - }) - ); - - return; - } - const denomInfo = getDenomInfo(chainID); - const txnInputs: MultiTxnsInputs = { - basicChainInfo: getChainInfo(chainID), - msgs, - memo: data.memo, - denom: denomInfo.minimalDenom, - feegranter: '', - }; - dispatch(multiTxns(txnInputs)); - }; - - return ( -
    -
    - -
    -
    -
    -
    - -
    -
    - Memo -
    - -
    - -
    -
    -
    - -
    -
    -
    -
    - ); -}; - -export default MultiTransfer; diff --git a/frontend/src/app/(routes)/transfers/components/MultiTxUpload.tsx b/frontend/src/app/(routes)/transfers/components/MultiTxUpload.tsx deleted file mode 100644 index 78223a576..000000000 --- a/frontend/src/app/(routes)/transfers/components/MultiTxUpload.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import Image from 'next/image'; -import { parseSendMsgsFromContent } from '@/utils/parseMsgs'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { setError } from '@/store/features/common/commonSlice'; -import { SEND_TEMPLATE } from '@/utils/constants'; - -const MultiTxUpload = ({ - chainID, - addMsgs, -}: { - chainID: string; - addMsgs: (msgs: Msg[]) => void; -}) => { - const dispatch = useAppDispatch(); - const address = useAppSelector( - (state) => state.wallet.networks[chainID].walletInfo.bech32Address - ); - - const onFileContents = (content: string | ArrayBuffer | null) => { - const [parsedTxns, error] = parseSendMsgsFromContent( - address, - '\n' + content - ); - if (error) { - dispatch( - setError({ - type: 'error', - message: error, - }) - ); - } else { - addMsgs(parsedTxns); - } - }; - - return ( -
    -
    -
    - File Upload -
    -
    -
    - Download Sample{' '} -
    -
    - Download { - window.open(SEND_TEMPLATE, '_blank', 'noopener,noreferrer'); - }} - /> -
    -
    -
    -
    { - const element = document.getElementById('multiTxns_file'); - if (element) element.click(); - }} - > -
    -
    - Upload file -
    - Upload CSV File, Each line must contain “Recipient Amount” -
    -
    -
    - { - if (!e?.target?.files) return; - const file = e.target.files[0]; - if (!file) { - return; - } - - const reader = new FileReader(); - reader.onload = (e) => { - if (!e.target) return null; - const contents = e.target.result; - onFileContents(contents); - }; - reader.onerror = (e) => { - dispatch( - setError({ - type: 'error', - message: '' + (e.target?.error || 'Something went wrong. '), - }) - ); - }; - reader.readAsText(file); - e.target.value = ''; - }} - /> -
    -
    -
    - ); -}; - -export default MultiTxUpload; diff --git a/frontend/src/app/(routes)/transfers/components/SingleTransfer.tsx b/frontend/src/app/(routes)/transfers/components/SingleTransfer.tsx deleted file mode 100644 index 8ea32df93..000000000 --- a/frontend/src/app/(routes)/transfers/components/SingleTransfer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import Summary from './Summary'; -import SendPage from '../SendPage'; -import { TransfersTab } from './TransfersPage'; - -const SingleTransfer = ({ - sortedAssets, - chainIDs, - tab, - handleTabChange, -}: { - sortedAssets: ParsedAsset[]; - chainIDs: string[]; - tab: TransfersTab; - handleTabChange: () => void; -}) => { - return ( -
    - -
    - -
    -
    - ); -}; - -export default SingleTransfer; diff --git a/frontend/src/app/(routes)/transfers/components/Summary.tsx b/frontend/src/app/(routes)/transfers/components/Summary.tsx deleted file mode 100644 index 4b81becb2..000000000 --- a/frontend/src/app/(routes)/transfers/components/Summary.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import Image from 'next/image'; -import React from 'react'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { TransfersTab } from './TransfersPage'; - -const Summary = ({ - chainIDs, - borderStyle, - tab, - handleTabChange, -}: { - chainIDs: string[]; - borderStyle: string; - tab: TransfersTab; - handleTabChange: () => void; -}) => { - const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); - let chainName = 'All Networks'; - let imageURL = '/all-networks-icon.png'; - let firstChainName = ''; - - Object.keys(nameToChainIDs).forEach((name) => { - if (nameToChainIDs[name] === chainIDs[0]) firstChainName = name; - }); - const chainImageURL = useAppSelector( - (state) => state.wallet.networks[chainIDs[0]]?.network?.logos?.menu || '' - ); - - if (chainIDs.length === 1) { - chainName = firstChainName; - imageURL = chainImageURL; - } - - return ( -
    -
    - {chainName} -
    - {chainName} -
    -
    -
    - -
    - {chainIDs.length === 1 && ( - - )} -
    - ); -}; - -export default Summary; diff --git a/frontend/src/app/(routes)/transfers/components/Transfers.tsx b/frontend/src/app/(routes)/transfers/components/Transfers.tsx index 73a93b465..c1c9dba68 100644 --- a/frontend/src/app/(routes)/transfers/components/Transfers.tsx +++ b/frontend/src/app/(routes)/transfers/components/Transfers.tsx @@ -8,7 +8,7 @@ import ChainNotFound from '@/components/ChainNotFound'; const Transfers = ({ chainNames }: { chainNames: string[] }) => { const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs + (state: RootState) => state.common.nameToChainIDs ); const chainIDs: string[] = []; diff --git a/frontend/src/app/(routes)/transfers/components/TransfersHistory.tsx b/frontend/src/app/(routes)/transfers/components/TransfersHistory.tsx deleted file mode 100644 index 7cd61875f..000000000 --- a/frontend/src/app/(routes)/transfers/components/TransfersHistory.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import TopNav from '@/components/TopNav'; -import { RecentTransactions } from '../../(overview)/overview-components/History'; -import { TRANSFERS_MSG_FILTERS } from '@/utils/constants'; -import { formatDollarAmount } from '@/utils/util'; -import useGetAssetsAmount from '@/custom-hooks/useGetAssetsAmount'; - -const TransfersHistory = ({ chainIDs }: { chainIDs: string[] }) => { - return ( -
    - - -
    -

    - Recent Transactions -

    -
    - -
    - ); -}; - -const Balance = ({ chainIDs }: { chainIDs: string[] }) => { - const [, available] = useGetAssetsAmount(chainIDs); - return ( -
    -
    -
    - Available Balance -
    - - {formatDollarAmount(available)} - -
    -
    - - -
    -
    - ); -}; - -export default TransfersHistory; \ No newline at end of file diff --git a/frontend/src/app/(routes)/transfers/components/TransfersPage.tsx b/frontend/src/app/(routes)/transfers/components/TransfersPage.tsx index bd761fffa..55d66dbff 100644 --- a/frontend/src/app/(routes)/transfers/components/TransfersPage.tsx +++ b/frontend/src/app/(routes)/transfers/components/TransfersPage.tsx @@ -1,67 +1,76 @@ -import React, { useState } from 'react'; -import MainTopNav from '@/components/MainTopNav'; -import TransfersHistory from './TransfersHistory'; -import { TRANSFERS_TAB2 } from '../../../../utils/constants'; -import { SINGLE_TAB_TEXT, TRANSFERS_TAB1 } from '@/utils/constants'; -import SingleTransfer from './SingleTransfer'; -import MultiTransfer from './MultiTransfer'; -import useInitBalances from '@/custom-hooks/useInitBalances'; -import { useAppDispatch } from '@/custom-hooks/StateHooks'; -import { setError } from '@/store/features/common/commonSlice'; +import React, { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; import useSortedAssets from '@/custom-hooks/useSortedAssets'; +import { useSearchParams } from 'next/navigation'; +import MultiSendPage from './multi-send/MultiSendPage'; +import IBCSwapPage from './ibc-swaps/IBCSwapPage'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import PageHeader from '@/components/common/PageHeader'; +import { TRANSFERS_TYPES } from '@/utils/constants'; +import SingleSend from './single-send/SingleSend'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; -export interface TransfersTab { - current: string; - to: string; -} const TransfersPage = ({ chainIDs }: { chainIDs: string[] }) => { + const [sortedAssets, authzSortedAssets] = useSortedAssets(chainIDs, { + showAvailable: true, + AuthzSkipIBC: true, + }); + const paramsTransferType = useSearchParams().get('type'); + + const [transferType, setTransferType] = useState('single'); + + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const dispatch = useAppDispatch(); - const [sortedAssets] = useSortedAssets(chainIDs, { showAvailable: true }); - const [tab, setTab] = useState(TRANSFERS_TAB1); - const changeTab = (tab: TransfersTab) => { - if (tab === TRANSFERS_TAB1) setTab(TRANSFERS_TAB2); - else setTab(TRANSFERS_TAB1); - }; - useInitBalances({ chainIDs }); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + const showAuthzAlert = useGetShowAuthzAlert(); - const handleTabChange = () => { - if (chainIDs.length > 1) { - dispatch( - setError({ - type: 'error', - message: 'Multi transfer is not available for All networks!', - }) - ); - return; - } - changeTab(tab); + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); }; - return ( -
    -
    - + useEffect(() => { + if (paramsTransferType?.length) { + setTransferType(paramsTransferType.toLowerCase()); + } else { + setTransferType('single'); + } + }, [paramsTransferType]); -
    - {tab.current === SINGLE_TAB_TEXT ? ( - - ) : ( - + + {isWalletConnected ? ( +
    + {transferType === 'single' ? ( + - )} + ) : null} + {transferType === 'multi-send' ? ( + + ) : null} + {transferType === 'ibc-swap' ? : null} +
    + ) : ( +
    +
    -
    - + )}
    ); }; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/AssetsList.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/AssetsList.tsx new file mode 100644 index 000000000..2cabc8f16 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/AssetsList.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import TextField from '@mui/material/TextField'; +import Autocomplete from '@mui/material/Autocomplete'; +import Avatar from '@mui/material/Avatar'; +import { CircularProgress, Paper } from '@mui/material'; +import { shortenName } from '@/utils/util'; +import { AssetConfig } from '@/types/swaps'; +import { customAutoCompleteStyles, customTextFieldStyles } from '../../styles'; +import NoOptions from '@/components/common/NoOptions'; + +export default function AssetsAutocomplete({ + options, + handleChange, + selectedAsset, + assetsLoading, + disabled, +}: { + options: AssetConfig[]; + handleChange: (option: AssetConfig | null) => void; + selectedAsset: AssetConfig | null; + assetsLoading: boolean; + disabled: boolean; +}) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderOption = (props: any, option: AssetConfig) => ( +
  • +
    + +
    + + {shortenName(option.symbol, 15)} + + + {shortenName(option.name, 20)} + +
    +
    +
  • + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderInput = (params: any) => ( + + {selectedAsset && ( + + )} + {params.InputProps.startAdornment} + + ), + }} + sx={{ + '& .MuiInputBase-input': { + color: '#fffffff0', + fontSize: '14px', + fontWeight: 300, + fontFamily: 'Libre Franklin', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: '#ffffff80', + }, + }} + /> + ); + + return ( + option.symbol} + renderOption={renderOption} + renderInput={renderInput} + noOptionsText={} + onChange={(_, newValue) => handleChange(newValue)} + value={selectedAsset} + disabled={disabled} + PaperComponent={({ children }) => ( + + {assetsLoading ? ( +
    + +
    + ) : ( + children + )} +
    + )} + sx={{ ...customTextFieldStyles, ...customAutoCompleteStyles }} + /> + ); +} diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/ChainsList.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/ChainsList.tsx new file mode 100644 index 000000000..96fa55639 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/ChainsList.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import TextField from '@mui/material/TextField'; +import Autocomplete from '@mui/material/Autocomplete'; +import Avatar from '@mui/material/Avatar'; +import { CircularProgress, Paper } from '@mui/material'; +import { shortenName } from '@/utils/util'; +import { ChainConfig } from '@/types/swaps'; +import { customAutoCompleteStyles, customTextFieldStyles } from '../../styles'; + +interface ChainOption { + label: string; + chainID: string; + logoURI: string; +} + +export default function ChainsList({ + options, + handleChange, + selectedChain, + dataLoading, + disabled, +}: { + options: ChainConfig[]; + handleChange: (option: ChainOption | null) => void; + selectedChain: ChainConfig | null; + dataLoading: boolean; + disabled: boolean; +}) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderOption = (props: any, option: ChainOption) => ( +
  • +
    + +
    + + {shortenName(option.label, 15)} + + + {shortenName(option.chainID, 15)} + +
    +
    +
  • + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderInput = (params: any) => ( + + {selectedChain && ( + + )} + {params.InputProps.startAdornment} + + ), + }} + sx={{ + '& .MuiInputBase-input': { + color: '#fffffff0', + fontSize: '14px', + fontWeight: 300, + fontFamily: 'Libre Franklin', + textTransform: 'capitalize', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: '#ffffff80', + }, + }} + /> + ); + + return ( +
    + option.label} + renderOption={renderOption} + renderInput={renderInput} + onChange={(_, newValue) => handleChange(newValue)} + value={selectedChain} + disabled={disabled} + PaperComponent={({ children }) => ( + + {dataLoading ? ( +
    + +
    + ) : ( + children + )} +
    + )} + sx={{ ...customTextFieldStyles, ...customAutoCompleteStyles }} + /> +
    + ); +} diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwap.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwap.tsx new file mode 100644 index 000000000..8febf9fed --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwap.tsx @@ -0,0 +1,931 @@ +import { + Avatar, + Box, + CircularProgress, + TextField, + Tooltip, +} from '@mui/material'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; +import AssetsList from './AssetsList'; +import useGetChains from '@/custom-hooks/useGetChains'; +import useGetAssets from '@/custom-hooks/useGetAssets'; +import useChain from '@/custom-hooks/useChain'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getBalances } from '@/store/features/bank/bankSlice'; +import useAccount from '@/custom-hooks/useAccount'; +import useSwaps from '@/custom-hooks/useSwaps'; +import { + resetTx, + resetTxDestSuccess, + setAmountIn, + setAmountOut, + setDestAsset, + setDestChain, + setFromAddress, + setSlippage, + setSourceAsset, + setSourceChain, + setToAddress, + txIBCSwap, +} from '@/store/features/swaps/swapsSlice'; +import ChainsList from './ChainsList'; +import { AssetConfig, ChainConfig } from '@/types/swaps'; +import { TxStatus } from '@/types/enums'; +import { RouteData } from '@0xsquid/sdk'; +import { fromBech32 } from '@cosmjs/encoding'; +import { shortenAddress } from '@/utils/util'; +import { setError } from '@/store/features/common/commonSlice'; +import RoutePreview from './RoutePreview'; +import { FLIP_ICON, ROUTE_ICON, SETTINGS_ICON } from '@/constants/image-names'; +import { + ALL_NETWORKS_GRADIENT, + ALL_NETWORKS_ICON, + SWAP_ROUTE_ERROR, +} from '@/utils/constants'; +import { customTextFieldStyles } from '../../styles'; +import Settings from './Settings'; +import IBCSwapLoading from './IBCSwapLoading'; + +const emptyBalance = { + amount: 0, + minimalDenom: '', + displayDenom: '', + decimals: 0, + parsedAmount: 0, +}; + +type HandleAmountChangeFunc = ( + e: React.ChangeEvent +) => void; +type QuickSelectAmountFunc = (value: string) => void; + +const IBCSwap = () => { + // To fetch all skip supported chains + const { chainsInfo, loading: chainsLoading } = useGetChains(); + + // To fetch all skip supported assets (chain - assets) + const { getTokensByChainID, srcAssetsLoading, destAssetLoading } = + useGetAssets(); + + // To fetch 4 rest endpoints from chain-registry + const { getChainEndpoints, getExplorerEndpoints } = useChain(); + + const { getSwapRoute, routeLoading, routeError } = useSwaps(); + const { getAccountAddress, getAvailableBalance } = useAccount(); + const [otherAddress, setOtherAddress] = useState(false); + const dispatch = useAppDispatch(); + const handleSendToAnotherAddress = () => { + setOtherAddress((prev) => !prev); + }; + + const selectedSourceChain = useAppSelector( + (state) => state.swaps.sourceChain + ); + const selectedSourceAsset = useAppSelector( + (state) => state.swaps.sourceAsset + ); + const selectedDestChain = useAppSelector((state) => state.swaps.destChain); + const selectedDestAsset = useAppSelector((state) => state.swaps.destAsset); + const amountIn = useAppSelector((state) => state.swaps.amountIn); + const amountOut = useAppSelector((state) => state.swaps.amountOut); + const toAddress = useAppSelector((state) => state.swaps.toAddress); + const fromAddress = useAppSelector((state) => state.swaps.fromAddress); + const txStatus = useAppSelector((state) => state.swaps.txStatus.status); + const sourceTxHash = useAppSelector((state) => state.swaps.txSuccess.txHash); + const slippage = useAppSelector((state) => state.swaps.slippage); + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + const balanceStatus = useAppSelector( + (state) => state.bank.balances?.[selectedSourceChain?.chainID || '']?.status + ); + const swapTxLoading = useAppSelector((state) => state.swaps.txStatus.status); + + const [selectedSourceChainAssets, setSelectedSourceChainAssets] = useState< + AssetConfig[] + >([]); + const [selectDestChainAssets, setSelectedDestChainAssets] = useState< + AssetConfig[] + >([]); + const [availableBalance, setAvailableBalance] = useState(emptyBalance); + const [userInputChange, setUserInputChange] = useState(true); + const [receiverAddress, setReceiverAddress] = useState(''); + const [selfReceiverAddress, setSelfReceiverAddress] = useState(''); + const [swapRoute, setSwapRoute] = useState(null); + const [addressValidationError, setAddressValidationError] = useState(''); + const [allInputsProvided, setAllInputsProvided] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + + const handleSelectSourceChain = async (option: ChainConfig | null) => { + dispatch(setFromAddress('')); + dispatch(setSourceChain(option)); + + setSelectedSourceChainAssets([]); + dispatch(setSourceAsset(null)); + + dispatch(setAmountIn('')); + dispatch(setAmountOut('')); + + if (!option) { + setAvailableBalance(emptyBalance); + } + + if (option?.chainID) { + const { address } = await getAccountAddress(option?.chainID || ''); + dispatch(setFromAddress(address)); + + // Select assets based on chainID + const assets = await getTokensByChainID(option?.chainID || '', true); + setSelectedSourceChainAssets(assets || []); + + setAvailableBalance(emptyBalance); + const { apis } = getChainEndpoints(option?.chainID || ''); + + // To get all asset balances of address using selected chain + if (address?.length) { + dispatch( + getBalances({ + address: address, + baseURL: apis[0], + baseURLs: apis, + chainID: option?.chainID, + }) + ); + } + } + }; + + const handleSelectSourceAsset = async (option: AssetConfig | null) => { + dispatch(setSourceAsset(option)); + dispatch(setAmountIn('')); + dispatch(setAmountOut('')); + if (option) { + const { balanceInfo } = await getAvailableBalance({ + chainID: selectedSourceChain?.chainID || '', + denom: option?.label || '', + }); + setAvailableBalance(balanceInfo); + } else { + setAvailableBalance(emptyBalance); + } + }; + + const handleSelectDestChain = async (option: ChainConfig | null) => { + dispatch(setToAddress('')); + dispatch(setDestAsset(null)); + dispatch(setAmountOut('')); + dispatch(setDestChain(option)); + if (option?.chainID) { + const { address } = await getAccountAddress(option.chainID); + setSelfReceiverAddress(address); + dispatch(setToAddress(address)); + const assets = await getTokensByChainID(option.chainID, false); + setSelectedDestChainAssets(assets || []); + } else { + setSelectedDestChainAssets([]); + } + }; + + const handleSelectDestAsset = (option: AssetConfig | null) => { + dispatch(setDestAsset(option)); + dispatch(setAmountOut('')); + }; + + const flipChains = async () => { + const tempSelectedSourceChain = selectedSourceChain; + dispatch(setSourceChain(selectedDestChain)); + dispatch(setDestChain(tempSelectedSourceChain)); + + const tempSelectedSourceAsset = selectedSourceAsset; + dispatch(setSourceAsset(selectedDestAsset)); + dispatch(setDestAsset(tempSelectedSourceAsset)); + + const tempFromAddress = fromAddress; + dispatch(setFromAddress(toAddress)); + dispatch(setToAddress(tempFromAddress)); + + handleRotate(); + if (selectedDestChain && selectedDestAsset) { + const { apis } = getChainEndpoints(selectedDestChain?.chainID || ''); + const { address } = await getAccountAddress( + selectedDestChain?.chainID || '' + ); + dispatch( + getBalances({ + address: address, + baseURL: apis[0], + baseURLs: apis, + chainID: selectedDestChain?.chainID || '', + }) + ); + + const { balanceInfo } = await getAvailableBalance({ + chainID: selectedDestChain?.chainID || '', + denom: selectedDestAsset?.label || '', + }); + setAvailableBalance(balanceInfo); + } else { + setAvailableBalance(emptyBalance); + } + }; + + const [isRotated, setIsRotated] = useState(false); + const disableSwapBtn = + txStatus === TxStatus.PENDING || + !allInputsProvided || + routeLoading || + !!routeError; + + const handleRotate = () => { + setIsRotated((prev) => !prev); + }; + + const fetchSwapRoute = async () => { + if ( + selectedDestAsset && + selectedDestChain && + selectedSourceAsset && + selectedSourceChain && + Number(amountIn) && + !(swapTxLoading === TxStatus.PENDING) + ) { + const amount = amountIn; + const decimals = selectedSourceAsset?.decimals; + const { resAmount, route } = await getSwapRoute({ + amount: Number(amount) * 10 ** (decimals || 1), + destChainID: selectedDestChain?.chainID || '', + destDenom: selectedDestAsset?.denom || '', + sourceChainID: selectedSourceChain?.chainID || '', + sourceDenom: selectedSourceAsset?.denom || '', + fromAddress: fromAddress, + toAddress: toAddress, + slippage: Number(slippage), + }); + setSwapRoute(route); + const resultDecimals = selectedDestAsset?.decimals; + const parsedDestAmount = parseFloat( + (Number(resAmount) / 10.0 ** (resultDecimals || 1)).toFixed(6) + ); + dispatch(setAmountOut(parsedDestAmount.toString())); + } else if (!Number(amountIn)) { + dispatch(setAmountOut('0')); + } + }; + + const handleAmountInChange = ( + e: React.ChangeEvent + ) => { + const input = e.target.value; + if (/^\d*\.?\d*$/.test(input)) { + if ((input.match(/\./g) || []).length <= 1) { + dispatch(setAmountIn(input)); + setUserInputChange(true); + } + } + }; + + const handleAddressChange = ( + e: React.ChangeEvent + ) => { + const value = e.target.value; + validateAddress(value); + dispatch(setToAddress(value)); + setReceiverAddress(value); + }; + + const handleSlippageChange = ( + e: React.ChangeEvent + ) => { + const input = e.target.value; + if (/^\d*\.?\d*$/.test(input)) { + if ((input.match(/\./g) || []).length <= 1) { + dispatch(setSlippage(input)); + setUserInputChange(true); + } + } + }; + + const validateAddress = (address: string) => { + if (address.length) { + try { + fromBech32(address); + setAddressValidationError(''); + return true; + } catch (error) { + setAddressValidationError('Invalid recipient address'); + return false; + } + } else { + setAddressValidationError('Please enter address'); + return false; + } + }; + + useEffect(() => { + if (userInputChange) { + fetchSwapRoute(); + setUserInputChange(false); + } + }, [amountIn]); + + useEffect(() => { + if (slippage) { + fetchSwapRoute(); + } + }, [slippage]); + + useEffect(() => { + if (selectedDestAsset) { + fetchSwapRoute(); + } else { + dispatch(setAmountOut('')); + } + }, [selectedDestAsset]); + + useEffect(() => { + if (receiverAddress && !addressValidationError) { + fetchSwapRoute(); + } + }, [toAddress]); + + const onTxSwap = async () => { + if (otherAddress && addressValidationError) { + dispatch( + setError({ + message: addressValidationError, + type: 'error', + }) + ); + return; + } else if (otherAddress && !receiverAddress.length) { + dispatch( + setError({ + message: 'Please enter the recipient address', + type: 'error', + }) + ); + return; + } + if (swapRoute && allInputsProvided) { + const { rpcs, apis } = getChainEndpoints( + selectedSourceChain?.chainID || '' + ); + const { explorerEndpoint } = getExplorerEndpoints( + selectedSourceChain?.chainID || '' + ); + dispatch( + txIBCSwap({ + rpcURLs: rpcs, + signerAddress: fromAddress, + sourceChainID: selectedSourceChain?.chainID || '', + destChainID: selectedDestChain?.chainID || '', + swapRoute: swapRoute, + explorerEndpoint, + baseURLs: apis, + }) + ); + } + }; + + const quickSelectAmount = (value: string) => { + if ( + availableBalance?.parsedAmount && + availableBalance?.displayDenom && + !(swapTxLoading === TxStatus.PENDING) + ) { + const amount = availableBalance.parsedAmount; + if (value === 'half') { + let halfAmount = Math.max(0, amount || 0) / 2; + halfAmount = +halfAmount.toFixed(6); + dispatch(setAmountIn(halfAmount.toString())); + setUserInputChange(true); + } else { + let maxAmount = Math.max(0, amount || 0); + maxAmount = +maxAmount.toFixed(6); + dispatch(setAmountIn(maxAmount.toString())); + setUserInputChange(true); + } + } + }; + + useEffect(() => { + dispatch(resetTx()); + dispatch(setSourceChain(null)); + dispatch(setSourceAsset(null)); + dispatch(setDestChain(null)); + dispatch(setDestAsset(null)); + dispatch(setAmountIn('')); + dispatch(setAmountOut('')); + dispatch(setToAddress('')); + dispatch(resetTx()); + dispatch(resetTxDestSuccess()); + }, []); + + const validateAllInputs = () => { + const chainsSelected = selectedSourceChain && selectedDestChain; + const assetsSelected = selectedSourceAsset && selectedDestAsset; + const srcAmount = Number(amountIn) ? true : false; + const validReceiverAddress = !otherAddress + ? true + : receiverAddress && !addressValidationError + ? true + : false; + + if (chainsSelected && assetsSelected && srcAmount && validReceiverAddress) { + setAllInputsProvided(true); + return; + } + setAllInputsProvided(false); + }; + + const selectNetworkAlert = () => { + dispatch( + setError({ + message: 'Please select a network', + type: 'error', + }) + ); + }; + + const connectSourceWallet = async () => { + if (selectedSourceChain) { + const { address } = await getAccountAddress( + selectedSourceChain?.chainID || '' + ); + dispatch(setFromAddress(address)); + } else { + selectNetworkAlert(); + } + }; + + const connectDestWallet = async () => { + if (selectedDestChain) { + const { address } = await getAccountAddress( + selectedDestChain?.chainID || '' + ); + dispatch(setToAddress(address)); + } else { + selectNetworkAlert(); + } + }; + + useEffect(() => { + validateAllInputs(); + }, [ + selectedDestAsset, + selectedDestChain, + selectedSourceAsset, + selectedSourceChain, + amountIn, + ]); + + useEffect(() => { + if (!otherAddress) { + dispatch(setToAddress(selfReceiverAddress)); + setReceiverAddress(''); + setAddressValidationError(''); + } + }, [otherAddress]); + + useEffect(() => { + if (!otherAddress && !addressValidationError) { + fetchSwapRoute(); + } + }, [otherAddress, toAddress]); + + useEffect(() => { + if (sourceTxHash?.length && balanceStatus === TxStatus.IDLE) { + const updateBalance = async () => { + const { balanceInfo } = await getAvailableBalance({ + chainID: selectedSourceChain?.chainID || '', + denom: selectedSourceAsset?.label || '', + }); + setAvailableBalance(balanceInfo); + }; + updateBalance(); + } + }, [balanceStatus, sourceTxHash]); + + const [showRoute, setShowRoute] = useState(false); + + return ( +
    +
    +
    + {settingsOpen ? ( + setSettingsOpen(false)} + handleSlippageChange={handleSlippageChange} + /> + ) : showRoute && swapRoute ? ( + setShowRoute(false)} + /> + ) : ( +
    +
    +
    +
    +
    Swap
    +
    + {swapRoute ? ( + + + + ) : null} + + + +
    +
    +
    + + {selectedSourceChain?.logoURI ? ( + + ) : ( +
    + +
    + )} + {fromAddress ? ( +
    + {shortenAddress(fromAddress, 20)} +
    + ) : ( + + )} +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    Available Balance
    + {balanceStatus === TxStatus.PENDING && + !availableBalance.parsedAmount && + !availableBalance.displayDenom ? ( + + ) : ( + <> + {/*
    {availableBalance.parsedAmount || 0}
    */} +
    + { + String(availableBalance.parsedAmount).split( + '.' + )[0] + } + {availableBalance.parsedAmount > 0 ? ( + + . + { + String( + availableBalance.parsedAmount + ).split('.')[1] + } + + ) : null} +
    +
    {availableBalance.displayDenom || null}
    + + )} +
    +
    +
    +
    +
    +
    + Swap +
    +
    + + {selectedDestChain?.logoURI ? ( + + ) : ( +
    + +
    + )} + {toAddress ? ( +
    + {shortenAddress(toAddress, 20)} +
    + ) : ( + + )} +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + {otherAddress + ? 'Receive on same wallet' + : 'Receive on another wallet'} +
    +
    + {otherAddress ? ( +
    + +
    + ) : null} +
    +
    +
    + {routeLoading ? ( +
    + Fetching Route + {' '} +
    + ) : ( +
    + {routeError ? ( +
    +
    {routeError}
    + {routeError === SWAP_ROUTE_ERROR ? ( +
    + Retry +
    + ) : null} +
    + ) : ( +
    + {swapRoute && + selectedSourceAsset && + selectedDestAsset ? ( +
    +
    {amountIn}
    +
    {selectedSourceAsset?.symbol}
    +
    =
    +
    {amountOut}
    +
    {selectedDestAsset?.symbol}
    +
    + ) : null} +
    + )} +
    + )} +
    +
    setSettingsOpen(true)} + className="text-b1 flex items-center gap-[2px] cursor-pointer" + > +
    + {slippage || swapRoute?.params?.slippage}% +
    +
    + Slippage +
    +
    +
    +
    +
    +
    +
    + +
    +
    + )} +
    +
    +
    + { + if (swapRoute) { + setShowRoute((prev) => !prev); + } + }} + /> +
    +
    + ); +}; + +export default IBCSwap; + +const AmountInputWrapper = ({ + quickSelectAmount, + handleAmountChange, + amount, +}: { + /* eslint-disable @typescript-eslint/no-explicit-any */ + quickSelectAmount: QuickSelectAmountFunc; + handleAmountChange: HandleAmountChangeFunc; + amount: string; +}) => { + return ( +
    +
    +
    + +
    +
    + + +
    +
    +
    + ); +}; + +const QuickSetAmountButton = ({ + value, + quickSelectAmount, +}: { + value: string; + quickSelectAmount: QuickSelectAmountFunc; +}) => { + return ( + + ); +}; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const AmountInputField = ({ + amount, + handleAmountChange, +}: { + amount: string; + handleAmountChange?: HandleAmountChangeFunc; +}) => { + const swapTxLoading = useAppSelector((state) => state.swaps.txStatus.status); + return ( + + ); +}; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwapLoading.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwapLoading.tsx new file mode 100644 index 000000000..4aa7a2c0f --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwapLoading.tsx @@ -0,0 +1,177 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React, { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { + CHECK_ICON_FILLED, + CROSS_ICON, + SWAP_ROUTE_ICON, +} from '@/constants/image-names'; +import NetworkSelected from './swap-loading/NetworkSelected'; +import NetworkName from './swap-loading/NetworkName'; +import EmptyNetwork from './swap-loading/EmptyNetwork'; +import EmptyNetworkName from './swap-loading/EmptyNetworkName'; +import SwapSummary from './swap-loading/SwapSummary'; +import { TxStatus } from '@/types/enums'; + +const IBCSwapLoading = ({ + toggleRoutePreview, +}: { + toggleRoutePreview: () => void; +}) => { + const selectedDestChain = useAppSelector((state) => state.swaps.destChain); + const selectedSourceChain = useAppSelector( + (state) => state.swaps.sourceChain + ); + + const [showTxSourceSuccess, setTxSourceSuccess] = useState(false); + const [showTxDestSuccess, setTxDestSuccess] = useState(false); + const [swapTxError, setSwapTxError] = useState(false); + const txLoadRes = useAppSelector((state) => state.swaps.txStatus.status); + const txError = useAppSelector((state) => state.swaps.txStatus.error); + const txHash = useAppSelector((state) => state.swaps.txSuccess.txHash); + const txDestStatus = useAppSelector((state) => state.swaps.txDestSuccess); + const selectedSourceAsset = useAppSelector( + (state) => state.swaps.sourceAsset + ); + const selectedDestAsset = useAppSelector((state) => state.swaps.destAsset); + + useEffect(() => { + if (txHash?.length) { + setTxSourceSuccess(true); + } else { + setTxSourceSuccess(false); + } + }, [txHash]); + + useEffect(() => { + if (txDestStatus.status.length) { + setTxDestSuccess(true); + } else { + setTxDestSuccess(false); + } + }, [txDestStatus]); + + useEffect(() => { + if (txError?.length) { + setSwapTxError(true); + setTimeout(() => setSwapTxError(false), 2000); + } + }, [txError]); + + return ( +
    +
    +
    + {selectedSourceChain ? ( +
    + + {selectedSourceAsset ? ( + + ) : null} +
    + ) : ( + + )} +
    + {showTxSourceSuccess && ( + Tick + )} +
    +
    + +
    Swap Route
    +
    +
    + {showTxDestSuccess && !swapTxError && ( + Success + )} + {swapTxError && !showTxDestSuccess && ( + Failed + )} +
    + {selectedDestChain ? ( +
    + + {selectedDestAsset ? ( + + ) : null} +
    + ) : ( + + )} +
    + +
    + {selectedSourceChain ? ( + + ) : ( + + )} + {selectedDestChain ? ( + + ) : ( + + )} +
    +
    +
    + {/*
    +
    + +
    Important
    +
    +
    IBC Swap
    +
    */} +
    +
    + +
    +
    +
    + ); +}; + +export default IBCSwapLoading; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwapPage.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwapPage.tsx new file mode 100644 index 000000000..ff80e094e --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/IBCSwapPage.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import IBCSwap from './IBCSwap'; + +const IBCSwapPage = () => { + return ; +}; + +export default IBCSwapPage; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/NetworkLogo.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/NetworkLogo.tsx new file mode 100644 index 000000000..8b4b0722e --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/NetworkLogo.tsx @@ -0,0 +1,64 @@ +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getFAC } from '@/utils/util'; +import React, { useEffect, useRef, useState } from 'react'; + +const NetworkLogo = ({ logo, chainID }: { logo: string; chainID: string }) => { + const { getNetworkTheme } = useGetChainInfo(); + const { primaryColor = '' } = getNetworkTheme(chainID); + const imgRef = useRef(null); + const [blurColor, setBlurColor] = useState('#4453DF'); + const [error, setError] = useState(false); + const fac = getFAC(); + useEffect(() => { + const getColor = async () => { + if (imgRef.current) { + try { + const color = await fac.getColorAsync(imgRef.current); + setBlurColor(color.hex); + } catch (error) { + setError(true); + } + } + }; + + getColor(); + + return () => { + fac.destroy(); + }; + }, [logo]); + return ( +
    + {error ? ( + <> +
    +
    + {''} +
    + + ) : ( + <> +
    +
    + {''} +
    + + )} +
    + ); +}; + +export default NetworkLogo; + diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/RoutePreview.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/RoutePreview.tsx new file mode 100644 index 000000000..002b4220e --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/RoutePreview.tsx @@ -0,0 +1,79 @@ +import useSwaps from '@/custom-hooks/useSwaps'; +import { RouteData } from '@0xsquid/sdk'; +import Image from 'next/image'; +import React from 'react'; +import CustomButton from '@/components/common/CustomButton'; +import SwapPath from './route-preview/SwapPath'; +import TransferPath from './route-preview/TransferPath'; +import ChainToken from './route-preview/ChainToken'; + +const RoutePreview = ({ + swapRoute, + onClose, +}: { + swapRoute: RouteData; + onClose: () => void; +}) => { + const { getSwapPathData } = useSwaps(); + const { fromChainData, toChainData, pathData } = getSwapPathData(swapRoute); + return ( +
    +
    +
    Route Preview
    + +
    +
    +
    + +
    +
    + +
    +
    + {pathData.map((path, index) => { + return ( + + {path.type === 'swap' ? ( + + ) : ( + + )} + + ); + })} +
    +
    + +
    +
    + +
    + ); +}; + +export default RoutePreview; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/Settings.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/Settings.tsx new file mode 100644 index 000000000..26affd278 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/Settings.tsx @@ -0,0 +1,102 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setSlippage } from '@/store/features/swaps/swapsSlice'; +import { TextField } from '@mui/material'; +import React, { useState } from 'react'; +import { customTextFieldStyles } from '../../styles'; +import CustomButton from '@/components/common/CustomButton'; + +const Settings = ({ + onClose, + handleSlippageChange, +}: { + onClose: () => void; + handleSlippageChange: HandleChangeEvent; +}) => { + const dispatch = useAppDispatch(); + const slippage = useAppSelector((state) => state.swaps.slippage); + const quickSelectSlippage = (value: string) => { + dispatch(setSlippage(value)); + }; + const [slippageError, setSlippageError] = useState(''); + const handleClose = () => { + if (!slippage?.length) { + setSlippageError('Slippage is required'); + return; + } + setSlippageError('') + onClose(); + }; + + return ( +
    +
    +
    +
    Settings
    + +
    +
    +
    +
    Slippage
    +
    + Slippage is how much price movement you can tolerate between the + time you send out a transaction and the time it's executed. +
    +
    +
    +
    + + + +
    +
    +
    Slippage
    + +
    + {slippageError} +
    +
    +
    +
    + +
    + ); +}; + +export default Settings; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/ChainToken.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/ChainToken.tsx new file mode 100644 index 000000000..0d2ef72ea --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/ChainToken.tsx @@ -0,0 +1,38 @@ +import Image from 'next/image'; +import React from 'react'; + +const ChainToken = ({ + amount, + chainName, + logo, + symbol, +}: { + logo: string; + amount: string; + symbol: string; + chainName: string; +}) => { + return ( +
    +
    + {chainName} +
    +
    +
    + {amount} {symbol} +
    +
    + On {chainName} +
    +
    +
    + ); +}; + +export default ChainToken; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/SwapPath.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/SwapPath.tsx new file mode 100644 index 000000000..8d570ff3f --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/SwapPath.tsx @@ -0,0 +1,37 @@ +import Image from 'next/image'; +import React from 'react'; + +const SwapPath = ({ + dex, + fromLogo, + fromSymbol, + toLogo, + toSymbol, +}: { + fromLogo: string; + fromSymbol: string; + toLogo: string; + toSymbol: string; + dex: string; +}) => { + return ( +
    +
    Swap
    +
    + + {fromSymbol} +
    +
    for
    +
    + + {toSymbol} +
    +
    + on + {dex} +
    +
    + ); +}; + +export default SwapPath; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/TransferPath.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/TransferPath.tsx new file mode 100644 index 000000000..f1dd19d5b --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/route-preview/TransferPath.tsx @@ -0,0 +1,40 @@ +import Image from 'next/image'; +import React from 'react'; + +const TransferPath = ({ + fromLogo, + fromName, + toLogo, + toName, + tokenLogo, + tokenSymbol, +}: { + tokenLogo: string; + tokenSymbol: string; + fromLogo: string; + fromName: string; + toLogo: string; + toName: string; +}) => { + return ( +
    +
    Transfer
    +
    + + {tokenSymbol} +
    +
    from
    +
    + + {fromName} +
    +
    to
    +
    + + {toName} +
    +
    + ); +}; + +export default TransferPath; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/EmptyNetwork.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/EmptyNetwork.tsx new file mode 100644 index 000000000..d2becabc9 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/EmptyNetwork.tsx @@ -0,0 +1,27 @@ +import { GLOBE_ICON } from '@/constants/image-names'; +import Image from 'next/image'; +import React from 'react'; + +const EmptyNetwork = () => { + return ( +
    +
    +
    + +
    +
    + ); +}; + +export default EmptyNetwork; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/EmptyNetworkName.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/EmptyNetworkName.tsx new file mode 100644 index 000000000..1283737db --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/EmptyNetworkName.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const EmptyNetworkName = ({ isSource }: { isSource: boolean }) => { + return ( +
    + {isSource ? 'Source' : 'Destination'} +
    + ); +}; + +export default EmptyNetworkName; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/NetworkName.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/NetworkName.tsx new file mode 100644 index 000000000..d6be83cbb --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/NetworkName.tsx @@ -0,0 +1,26 @@ +import useChain from '@/custom-hooks/useChain'; +import { capitalizeFirstLetter, shortenName } from '@/utils/util'; +import { Tooltip } from '@mui/material'; +import React from 'react'; + +const NetworkName = ({ + chainID, + isSource, +}: { + chainID: string; + isSource: boolean; +}) => { + const { getChainNameFromID } = useChain(); + const { chainName } = getChainNameFromID(chainID); + const networkName = + shortenName(chainName, 9) || (isSource ? 'Source' : 'Destination'); + return ( + +
    + {networkName} +
    +
    + ); +}; + +export default NetworkName; diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/NetworkSelected.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/NetworkSelected.tsx new file mode 100644 index 000000000..7b8ee043a --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/NetworkSelected.tsx @@ -0,0 +1,21 @@ +import { ChainConfig } from '@/types/swaps'; +import React from 'react' +import NetworkLogo from '../NetworkLogo'; + +const NetworkSelected = ({ + chainConfig, + isSource, + }: { + chainConfig: ChainConfig; + isSource: boolean; + }) => { + return ( +
    + +
    + ); + }; +export default NetworkSelected \ No newline at end of file diff --git a/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/SwapSummary.tsx b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/SwapSummary.tsx new file mode 100644 index 000000000..63ef92bcf --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/ibc-swaps/swap-loading/SwapSummary.tsx @@ -0,0 +1,37 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React from 'react'; + +const SwapSummary = () => { + const selectedSourceAsset = useAppSelector( + (state) => state.swaps.sourceAsset + ); + const selectedDestAsset = useAppSelector((state) => state.swaps.destAsset); + const amountIn = useAppSelector((state) => state.swaps.amountIn); + const amountOut = useAppSelector((state) => state.swaps.amountOut); + const isDataProvided = + selectedSourceAsset && + selectedDestAsset && + amountIn?.length && + amountOut?.length; + + return ( +
    + {isDataProvided ? ( + <> + You are swapping{' '} + + {Number(amountIn) ? amountIn : ''} {selectedSourceAsset?.symbol} + {' '} + to{' '} + + {Number(amountOut) ? amountOut : ''} {selectedDestAsset?.symbol} + + + ) : ( + <>Provide all the required fields to continue with the transaction. + )} +
    + ); +}; + +export default SwapSummary; diff --git a/frontend/src/app/(routes)/transfers/components/multi-send/AddMessages.tsx b/frontend/src/app/(routes)/transfers/components/multi-send/AddMessages.tsx new file mode 100644 index 000000000..de387a8db --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/multi-send/AddMessages.tsx @@ -0,0 +1,163 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import { parseSendMsgsFromContent } from '@/utils/parseMsgs'; +import { TextField } from '@mui/material'; +import React, { useState } from 'react'; +import { multiSendInputFieldStyles } from '../../styles'; +import { + MULTIOPS_SAMPLE_FILES, + MULTISEND_PLACEHOLDER, +} from '@/utils/constants'; +import Image from 'next/image'; +import Link from 'next/link'; + +const AddMessages = ({ + chainID, + addMsgs, + msgs, +}: { + chainID: string; + addMsgs: (msgs: Msg[]) => void; + msgs: Msg[]; +}) => { + const dispatch = useAppDispatch(); + const address = useAppSelector( + (state) => state.wallet?.networks?.[chainID]?.walletInfo?.bech32Address + ); + const [inputs, setInputs] = useState(''); + const handleInputChange = ( + e: React.ChangeEvent + ) => { + setInputs(e.target.value); + }; + + const onFileContents = (content: string | ArrayBuffer | null) => { + const [parsedTxns, error] = parseSendMsgsFromContent( + address, + '\n' + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + addMsgs(parsedTxns); + } + }; + + const addInputs = () => { + const [parsedTxns, error] = parseSendMsgsFromContent( + address, + '\n' + inputs + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + addMsgs(parsedTxns); + if (parsedTxns?.length) { + setInputs(''); + } else { + dispatch( + setError({ + type: 'error', + message: 'Invalid input', + }) + ); + } + } + }; + + return ( +
    +
    +
    + + +
    +
    { + const element = document.getElementById('multiTxns_file'); + if (element) element.click(); + }} + > +
    + +
    Upload CSV here
    +
    +
    +
    Download Sample
    + e.stopPropagation()} + className="text-[14px] underline underline-offset-[3px] font-bold" + > + here + +
    + { + if (!e?.target?.files) return; + const file = e.target.files[0]; + if (!file) { + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + if (!e.target) return null; + const contents = e.target.result; + onFileContents(contents); + }; + reader.onerror = (e) => { + dispatch( + setError({ + type: 'error', + message: '' + (e.target?.error || 'Something went wrong. '), + }) + ); + }; + reader.readAsText(file); + e.target.value = ''; + }} + /> +
    +
    +
    + ); +}; + +export default AddMessages; diff --git a/frontend/src/app/(routes)/transfers/components/multi-send/AmountSummary.tsx b/frontend/src/app/(routes)/transfers/components/multi-send/AmountSummary.tsx new file mode 100644 index 000000000..d63e541c2 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/multi-send/AmountSummary.tsx @@ -0,0 +1,25 @@ +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getTotalAmount } from '@/utils/denom'; +import React from 'react'; + +const AmountSummary = ({ msgs }: { msgs: Msg[] }) => { + const { getOriginDenomInfo } = useGetChainInfo(); + const originDenomInfo = getOriginDenomInfo( + msgs[0].value?.amount?.[0]?.denom || '' + ); + const totalAmount = getTotalAmount(originDenomInfo, msgs); + return ( +
    + You are sending + + {totalAmount} {originDenomInfo.originDenom} + + to + + {msgs.length} Addresses + +
    + ); +}; + +export default AmountSummary; diff --git a/frontend/src/app/(routes)/transfers/components/multi-send/MultiSend.tsx b/frontend/src/app/(routes)/transfers/components/multi-send/MultiSend.tsx new file mode 100644 index 000000000..8dbc9750a --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/multi-send/MultiSend.tsx @@ -0,0 +1,233 @@ +import React, { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import CustomSubmitButton from '@/components/CustomButton'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { multiTxns } from '@/store/features/bank/bankSlice'; +import { TxStatus } from '@/types/enums'; +import { + setChangeNetworkDialogOpen, + setError, +} from '@/store/features/common/commonSlice'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { Box } from '@mui/material'; +import { ALL_NETWORKS_GRADIENT, ALL_NETWORKS_ICON } from '@/utils/constants'; +import Image from 'next/image'; +import { shortenName } from '@/utils/util'; +import Messages from '../Messages'; +import MemoField from '../single-send/MemoField'; +import AmountSummary from './AmountSummary'; +import AddMessages from './AddMessages'; +import { get } from 'lodash'; +import TxnLoading from '../txn-loading/TxnLoading'; +import { getTotalAmount } from '@/utils/denom'; +import useGetFeegranter from '@/custom-hooks/useGetFeegranter'; +import { MAP_TXN_MSG_TYPES } from '@/utils/feegrant'; + +const MultiSend = ({ chainID }: { chainID: string }) => { + const dispatch = useAppDispatch(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const denomInfo = getDenomInfo(chainID); + const { getFeegranter } = useGetFeegranter(); + + const [msgs, setMsgs] = useState([]); + const [chainLogo, setChainLogo] = useState(ALL_NETWORKS_ICON); + const [chainGradient, setChainGradient] = useState(''); + + const txPendingStatus = useAppSelector((state) => state.bank.tx.status); + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork + ); + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + useEffect(() => { + if (txPendingStatus === TxStatus.IDLE) { + setMsgs([]); + } + }, [txPendingStatus]); + + const { handleSubmit, control } = useForm({ + defaultValues: { + memo: '', + }, + }); + + const addMsgs = (msgs: Msg[]) => { + setMsgs((prev) => [...prev, ...msgs]); + }; + + const onDelete = (index: number) => { + setMsgs((msgs) => { + return [...msgs.slice(0, index), ...msgs.slice(index + 1, msgs.length)]; + }); + }; + + const onDeleteAll = () => { + setMsgs([]); + }; + + const onSubmit = (data: { memo: string }) => { + if (txPendingStatus === TxStatus.PENDING) { + dispatch( + setError({ + type: 'error', + message: 'A transaction is still pending..', + }) + ); + return; + } + if (msgs.length === 0) { + dispatch( + setError({ + type: 'error', + message: 'No transactions found', + }) + ); + + return; + } + const txnInputs: MultiTxnsInputs = { + basicChainInfo: getChainInfo(chainID), + msgs, + memo: data.memo, + denom: denomInfo.minimalDenom, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['send']), + }; + dispatch(multiTxns(txnInputs)); + }; + + const changeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: false })); + }; + + const txnLoading = txPendingStatus === TxStatus.PENDING; + + useEffect(() => { + if (selectedNetwork.chainName && isWalletConnected) { + const chainID = nameToChainIDs[selectedNetwork.chainName]; + setChainLogo(allNetworks[chainID].logos.menu); + setChainGradient(allNetworks[chainID].config.theme.gradient); + } else { + setChainLogo(ALL_NETWORKS_ICON); + } + }, [selectedNetwork, isWalletConnected]); + + return ( +
    +
    +
    + +
    changeNetwork()} + className="flex items-center gap-2 cursor-pointer w-fit" + > + +
    + {shortenName(selectedNetwork.chainName, 15) || 'All Networks'} +
    + +
    +
    +
    +
    + +
    +
    Enter Memo
    + +
    + {msgs?.length ? ( +
    + + +
    + ) : null} + + +
    +
    +
    + +
    + ); +}; + +export default MultiSend; + +const MultiSendLoading = ({ + chainID, + msgs, +}: { + chainID: string; + msgs: Msg[]; +}) => { + const { getChainInfo } = useGetChainInfo(); + const { address: fromAddress, chainLogo } = getChainInfo(chainID); + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + const chainColor = get(allNetworks?.[chainID], 'config.theme.primaryColor'); + + const { getOriginDenomInfo } = useGetChainInfo(); + const originDenomInfo = msgs?.length + ? getOriginDenomInfo(msgs?.[0].value?.amount?.[0]?.denom || '') + : null; + const totalAmount = originDenomInfo + ? getTotalAmount(originDenomInfo, msgs) + : 0; + const firstAddress = msgs?.[0]?.value?.toAddress || ''; + + return ( +
    + +
    + {msgs?.length ? ( + + You are sending{' '} + {totalAmount ? ( + + {totalAmount} {originDenomInfo?.originDenom} + + ) : ( + tokens + )}{' '} + to {msgs.length} addresses + + ) : ( + + Your transaction summary appears here. + + )} +
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/transfers/components/multi-send/MultiSendPage.tsx b/frontend/src/app/(routes)/transfers/components/multi-send/MultiSendPage.tsx new file mode 100644 index 000000000..4ab100571 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/multi-send/MultiSendPage.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import MultiSend from './MultiSend'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import EmptyScreen from '@/components/common/EmptyScreen'; +import { setChangeNetworkDialogOpen } from '@/store/features/common/commonSlice'; + +const MultiSendPage = ({ chainID }: { chainID: string }) => { + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork.chainName + ); + const dispatch = useAppDispatch(); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const openChangeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: true })); + }; + + return ( + <> + {selectedNetwork && isWalletConnected ? ( + + ) : ( + + )} + + ); +}; + +export default MultiSendPage; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/AddressField.tsx b/frontend/src/app/(routes)/transfers/components/single-send/AddressField.tsx new file mode 100644 index 000000000..69d44b589 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/AddressField.tsx @@ -0,0 +1,43 @@ +import { TextField } from '@mui/material'; +import React from 'react'; +import { Controller } from 'react-hook-form'; +import { customTransferTextFieldStyles } from '../../styles'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const AddressField = ({ + control, + checkIfIBCTransaction, +}: { + control: any; + checkIfIBCTransaction: (asset?: ParsedAsset | null) => void; +}) => { + return ( + ( + { + checkIfIBCTransaction?.(); + }} + /> + )} + /> + ); +}; + +export default AddressField; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/AmountInputField.tsx b/frontend/src/app/(routes)/transfers/components/single-send/AmountInputField.tsx new file mode 100644 index 000000000..6f577bcf1 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/AmountInputField.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Controller } from 'react-hook-form'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const AmountInputField = ({ control }: { control: any }) => { + return ( + ( + + )} + /> + ); +}; + +export default AmountInputField; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/AmountInputWrapper.tsx b/frontend/src/app/(routes)/transfers/components/single-send/AmountInputWrapper.tsx new file mode 100644 index 000000000..96e97f5eb --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/AmountInputWrapper.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import AmountInputField from './AmountInputField'; + +type QuickSelectAmountFunc = (value: string) => void; + +const AmountInputWrapper = ({ + control, + quickSelectAmount, + selectedAsset, +}: { + /* eslint-disable @typescript-eslint/no-explicit-any */ + control: any; + quickSelectAmount: QuickSelectAmountFunc; + selectedAsset: ParsedAsset | null; +}) => { + return ( +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    Balance
    + {selectedAsset ? ( +
    + {String(selectedAsset.balance).split('.')[0]} + {selectedAsset.balance > 0 ? ( + + .{String(selectedAsset.balance).split('.')[1]} + + ) : null}{' '} + {selectedAsset.displayDenom} +
    + ) : ( + '-' + )} +
    +
    +
    + ); +}; +export default AmountInputWrapper; + +const QuickSetAmountButton = ({ + value, + quickSelectAmount, +}: { + value: string; + quickSelectAmount: QuickSelectAmountFunc; +}) => { + return ( + + ); +}; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/AssetsDropDown.tsx b/frontend/src/app/(routes)/transfers/components/single-send/AssetsDropDown.tsx new file mode 100644 index 000000000..46cf8565b --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/AssetsDropDown.tsx @@ -0,0 +1,189 @@ +/** @jsxImportSource @emotion/react */ + +import { + Autocomplete, + Avatar, + Paper, + TextField, + InputAdornment, +} from '@mui/material'; +import React from 'react'; +import { + customAutoCompleteStyles, + customTransferTextFieldStyles, +} from '../../styles'; +import NoOptions from '@/components/common/NoOptions'; +import CustomLoader from '@/components/common/CustomLoader'; +import { css } from '@emotion/react'; + +const listItemStyle = css` + &:hover { + background-color: #ffffff09 !important; + } +`; + +interface AssetsDropDownProps { + sortedAssets: ParsedAsset[]; + selectedAsset: ParsedAsset | null; + handleAssetChange: (option: ParsedAsset | null) => void; + loading: boolean; +} + +const AssetsDropDown: React.FC = ({ + sortedAssets = [], + selectedAsset, + handleAssetChange, + loading, +}) => { + const renderOption = ( + props: React.HTMLAttributes, + option: ParsedAsset + ) => ( +
  • +
    +
    +
    + +
    +
    +
    +
    + {String(option.balance).split('.')[0]} + {option.balance > 0 ? ( + + .{String(option.balance).split('.')[1]} + + ) : null} +
    +
    {option.displayDenom}
    +
    +
    + on {option.chainName} +
    +
    +
    +
    +
    +
  • + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderInput = (params: any) => ( + +
    +
    + +
    +
    +
    {selectedAsset.balance}
    +
    +
    +
    +
    + + ), + endAdornment: ( + +
    + {selectedAsset && ( +
    + on{' '} + {selectedAsset.chainName} +
    + )} + {params.InputProps.endAdornment} +
    +
    + ), + }} + sx={{ + '& .MuiInputBase-input': { + color: '#ffffffad', + fontSize: '14px', + fontWeight: 300, + fontFamily: 'Libre Franklin', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: '#ffffff80', + }, + }} + /> + ); + + return ( + option.displayDenom} + renderOption={renderOption} + renderInput={renderInput} + noOptionsText={} + onChange={(_, newValue) => handleAssetChange(newValue)} + value={selectedAsset || null} + PaperComponent={({ children }) => ( + + {sortedAssets?.length ? ( + children + ) : ( + <> + {loading ? ( +
    + +
    + ) : ( +
    + No Assets +
    + )} + + )} +
    + )} + sx={{ ...customTransferTextFieldStyles, ...customAutoCompleteStyles }} + /> + ); +}; + +export default AssetsDropDown; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/IBCSendAlert.tsx b/frontend/src/app/(routes)/transfers/components/single-send/IBCSendAlert.tsx new file mode 100644 index 000000000..e49ecaa1f --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/IBCSendAlert.tsx @@ -0,0 +1,33 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { IBC_SEND_ALERT } from '@/utils/constants'; +import Image from 'next/image'; +import React from 'react'; + +const IBCSendAlert = () => { + const showIBCSendAlert = useAppSelector( + (state) => state.bank.showIBCSendAlert + ); + return ( + <> + {showIBCSendAlert ? ( +
    + info-icon +

    + Important +

    +

    + {IBC_SEND_ALERT} +

    +
    + ) : null} + + ); +}; + +export default IBCSendAlert; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/MemoField.tsx b/frontend/src/app/(routes)/transfers/components/single-send/MemoField.tsx new file mode 100644 index 000000000..b613c664f --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/MemoField.tsx @@ -0,0 +1,26 @@ +import { TextField } from '@mui/material'; +import React from 'react'; +import { Controller } from 'react-hook-form'; +import { customTransferTextFieldStyles } from '../../styles'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const MemoField = ({ control }: { control: any }) => { + return ( + ( + + )} + /> + ); +}; + +export default MemoField; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/SingleSend.tsx b/frontend/src/app/(routes)/transfers/components/single-send/SingleSend.tsx new file mode 100644 index 000000000..dbb0bd73e --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/SingleSend.tsx @@ -0,0 +1,310 @@ +import React, { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import SingleSendForm from './SingleSendForm'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + setChangeNetworkDialogOpen, + setError, +} from '@/store/features/common/commonSlice'; +import useGetTxInputs from '@/custom-hooks/useGetTxInputs'; +import useAuthzExecHelper from '@/custom-hooks/useAuthzExecHelper'; +import { setIBCSendAlert, txBankSend } from '@/store/features/bank/bankSlice'; +import { txTransfer } from '@/store/features/ibc/ibcSlice'; +import Image from 'next/image'; +import { shortenAddress, shortenName } from '@/utils/util'; +import { Box } from '@mui/material'; +import { ALL_NETWORKS_GRADIENT, ALL_NETWORKS_ICON } from '@/utils/constants'; +import AssetsDropDown from './AssetsDropDown'; +import TxnLoading from '../txn-loading/TxnLoading'; +import { get } from 'lodash'; +import { TxStatus } from '@/types/enums'; + +const SingleSend = ({ sortedAssets }: { sortedAssets: ParsedAsset[] }) => { + const dispatch = useAppDispatch(); + const { isNativeTransaction, getChainIDFromAddress, getChainInfo } = + useGetChainInfo(); + const { txSendInputs, txTransferInputs, getVoteTxInputs } = useGetTxInputs(); + const { txAuthzSend } = useAuthzExecHelper(); + + const [selectedAsset, setSelectedAsset] = useState(null); + const [isIBC, setIsIBC] = useState(false); + const [chainLogo, setChainLogo] = useState(ALL_NETWORKS_ICON); + const [chainGradient, setChainGradient] = useState(''); + + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork + ); + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + const balancesLoading = useAppSelector((state) => state.bank.balancesLoading); + const sendTxStatus = useAppSelector((state) => state.bank.tx.status); + const ibcTxStatus = useAppSelector((state) => state.ibc.txStatus); + + const feeAmount = selectedAsset + ? getChainInfo(selectedAsset.chainID).feeAmount + : 0; + + const { handleSubmit, control, reset, getValues, setValue, watch } = useForm({ + defaultValues: { + amount: '', + address: '', + memo: '', + }, + }); + + const handleAssetChange = (option: ParsedAsset | null) => { + setSelectedAsset(option); + checkIfIBCTransaction(option); + }; + + const checkIfIBCTransaction = (asset = selectedAsset) => { + const address = getValues('address'); + + const destinationChainID = getChainIDFromAddress(address); + if ( + !!asset && + !!destinationChainID && + destinationChainID != asset?.chainID + ) { + setIsIBC(true); + dispatch(setIBCSendAlert(true)); + } else { + setIsIBC(false); + dispatch(setIBCSendAlert(false)); + } + }; + + const clearForm = () => { + reset(); + }; + + const onSubmit = (data: { + amount: number | undefined; + address: string; + memo: string; + }) => { + if (!selectedAsset) { + dispatch( + setError({ + type: 'error', + message: `Please select an asset`, + }) + ); + return; + } + if (!data.amount) { + dispatch( + setError({ + type: 'error', + message: `Amount can't be zero`, + }) + ); + + return; + } + + const { rpc } = getVoteTxInputs(selectedAsset.chainID); + if (isNativeTransaction(selectedAsset.chainID, data.address)) { + const txInputs = txSendInputs( + selectedAsset.chainID, + data.address, + data.amount, + data.memo, + selectedAsset.denom, + selectedAsset.decimals + ); + if (isAuthzMode) { + txAuthzSend({ + granter: authzAddress, + grantee: txInputs.from, + recipient: txInputs.to, + denom: txInputs.assetDenom, + amount: txInputs.amount, + chainID: txInputs.basicChainInfo.chainID, + memo: txInputs.memo, + }); + return; + } + txInputs.onTxSuccessCallBack = clearForm; + dispatch(txBankSend({ ...txInputs, rpc })); + } else { + const destChainID = getChainIDFromAddress(data.address); + + if (!destChainID) { + dispatch( + setError({ + type: 'error', + message: 'Invalid Address', + }) + ); + return; + } + + if (isAuthzMode) { + dispatch( + setError({ + type: 'error', + message: 'The IBC Transactions are not yet supported on Authz mode', + }) + ); + return; + } + + const txInputs = txTransferInputs( + selectedAsset.chainID, + destChainID, + data.address, + data.amount, + selectedAsset.denom, + selectedAsset.decimals + ); + + dispatch(txTransfer(txInputs)); + } + }; + + const changeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: false })); + }; + + const sendTxLoading = + sendTxStatus === TxStatus.PENDING || ibcTxStatus === TxStatus.PENDING; + + useEffect(() => { + if (selectedNetwork.chainName && isWalletConnected) { + const chainID = nameToChainIDs[selectedNetwork.chainName]; + setChainLogo(allNetworks[chainID].logos.menu); + setChainGradient(allNetworks[chainID].config.theme.gradient); + } else { + setChainLogo(ALL_NETWORKS_ICON); + } + }, [selectedNetwork]); + + return ( +
    +
    +
    + +
    changeNetwork()} + className="flex items-center gap-2 cursor-pointer w-fit" + > + +
    + {shortenName(selectedNetwork.chainName, 15) || 'All Networks'} +
    + +
    +
    +
    +
    +
    Select Asset
    + +
    + +
    +
    +
    + +
    + ); +}; + +export default SingleSend; + +const SingleSendLoading = ({ + chainID, + isIBC, + toAddress, + amount, + displayDenom, +}: { + chainID: string; + isIBC: boolean; + toAddress: string; + amount: string; + displayDenom: string; +}) => { + const { getChainInfo, getChainIDFromAddress } = useGetChainInfo(); + const destinationChainID = isIBC ? getChainIDFromAddress(toAddress) : chainID; + const { address: fromAddress, chainLogo: fromChainLogo } = + getChainInfo(chainID); + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + const { chainLogo: toChainLogo } = getChainInfo(destinationChainID); + const fromChainColor = get( + allNetworks?.[chainID], + 'config.theme.primaryColor' + ); + const toChainColor = get( + allNetworks?.[destinationChainID], + 'config.theme.primaryColor' + ); + const isDataProvided = amount?.length && chainID?.length && toAddress?.length; + return ( +
    + +
    + {isDataProvided ? ( + + {' '} + You are sending{' '} + + {amount} {displayDenom} + {' '} + to {shortenAddress(toAddress, 20)} + + ) : ( + + Your transaction summary appears here. + + )} +
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/SingleSendForm.tsx b/frontend/src/app/(routes)/transfers/components/single-send/SingleSendForm.tsx new file mode 100644 index 000000000..fe5db2bcf --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/SingleSendForm.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import AddressField from './AddressField'; +import AmountInputWrapper from './AmountInputWrapper'; +import MemoField from './MemoField'; +import { UseFormSetValue } from 'react-hook-form'; +import CustomSubmitButton from '@/components/CustomButton'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { TxStatus } from '@/types/enums'; +import { IBC_SEND_ALERT } from '@/utils/constants'; + +type OnSubmit = (data: { + amount: number | undefined; + address: string; + memo: string; +}) => void; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const SingleSendForm = ({ + control, + handleSubmit, + onSubmit, + feeAmount, + setValue, + selectedAsset, + isIBC, + checkIfIBCTransaction, +}: { + control: any; + handleSubmit: any; + onSubmit: OnSubmit; + feeAmount: number; + setValue: UseFormSetValue<{ + amount: string; + address: string; + memo: string; + }>; + selectedAsset: ParsedAsset | null; + isIBC: boolean; + checkIfIBCTransaction: (asset?: ParsedAsset | null) => void; +}) => { + const sendTxStatus = useAppSelector((state) => state.bank.tx.status); + const ibcTxStatus = useAppSelector((state) => state.ibc.txStatus); + + const quickSelectAmount = (value: string) => { + if (selectedAsset) { + const amount = selectedAsset.balance; + if (value === 'half') { + let halfAmount = Math.max(0, (amount || 0) - feeAmount) / 2; + halfAmount = +halfAmount.toFixed(6); + setValue('amount', halfAmount.toString()); + } else { + let maxAmount = Math.max(0, (amount || 0) - feeAmount); + maxAmount = +maxAmount.toFixed(6); + setValue('amount', maxAmount.toString()); + } + } + }; + return ( +
    +
    +
    +
    Enter recipient address
    + +
    + {isIBC ? ( +
    + {IBC_SEND_ALERT} +
    + ) : null} +
    +
    +
    +
    Enter Amount
    + {selectedAsset ? ( +
    +
    Balance
    +
    + {String(selectedAsset.balance).split('.')[0]} + {selectedAsset.balance > 0 ? ( + + .{String(selectedAsset.balance).split('.')[1]} + + ) : null}{' '} + {selectedAsset.displayDenom} +
    +
    + ) : null} +
    + +
    +
    +
    Enter Memo (Optional)
    + +
    + + + ); +}; + +export default SingleSendForm; diff --git a/frontend/src/app/(routes)/transfers/components/single-send/StyledNetworkLogo.tsx b/frontend/src/app/(routes)/transfers/components/single-send/StyledNetworkLogo.tsx new file mode 100644 index 000000000..96ca4e148 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/single-send/StyledNetworkLogo.tsx @@ -0,0 +1,68 @@ +import { getFAC } from '@/utils/util'; +import React, { useEffect, useRef, useState } from 'react'; + +const StyledNetworkLogo = ({ + logo, + primaryColor, + rotate, +}: { + logo: string; + primaryColor: string; + rotate?: boolean; +}) => { + const imgRef = useRef(null); + const [blurColor, setBlurColor] = useState('#4453DF'); + const [error, setError] = useState(false); + const fac = getFAC(); + useEffect(() => { + const getColor = async () => { + if (imgRef.current) { + try { + const color = await fac.getColorAsync(imgRef.current); + setBlurColor(color.hex); + } catch (error) { + setError(true); + } + } + }; + + getColor(); + + return () => { + fac.destroy(); + }; + }, [logo]); + return ( +
    + {error ? ( + <> +
    +
    + {''} +
    + + ) : ( + <> +
    +
    + {''} +
    + + )} +
    + ); +}; + +export default StyledNetworkLogo; diff --git a/frontend/src/app/(routes)/transfers/components/txn-loading/TxnLoading.tsx b/frontend/src/app/(routes)/transfers/components/txn-loading/TxnLoading.tsx new file mode 100644 index 000000000..64cdd081d --- /dev/null +++ b/frontend/src/app/(routes)/transfers/components/txn-loading/TxnLoading.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { shortenAddress } from '@/utils/util'; +import { GLOBE_ICON } from '@/constants/image-names'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { TxStatus } from '@/types/enums'; +import StyledNetworkLogo from '../single-send/StyledNetworkLogo'; + +interface TxnLoadingProps { + fromChainLogo: string; + fromChainColor: string; + fromAddress: string; + toChainLogo: string; + toChainColor: string; + toAddress: string; + msgsCount: number; + isSingle: boolean; +} + +const TxnLoading = (props: TxnLoadingProps) => { + const { + fromChainLogo, + fromChainColor, + fromAddress, + toChainLogo, + toChainColor, + toAddress, + msgsCount, + isSingle, + } = props; + const sendTxStatus = useAppSelector((state) => state.bank.tx.status); + const ibcTxStatus = useAppSelector((state) => state.ibc.txStatus); + + const sendTxLoading = + sendTxStatus === TxStatus.PENDING || ibcTxStatus === TxStatus.PENDING; + + return ( +
    +
    + {fromChainLogo ? ( +
    + +
    + ) : ( +
    + +
    + )} +
    + {fromAddress ? ( + {shortenAddress(fromAddress, 12)} + ) : ( + You + )} +
    +
    + {isSingle ? ( +
    + Tick +
    + ) : ( +
    +
    +
    + Tick + Tick + Tick +
    +
    +
    + )} + +
    + {toChainLogo ? ( +
    + +
    + ) : ( +
    + +
    + )} +
    + {toAddress ? ( + {shortenAddress(toAddress, 12)} + ) : ( + To + )} + {msgsCount > 1 ? ( + +{msgsCount - 1} + ) : null} +
    +
    +
    + ); +}; + +export default TxnLoading; diff --git a/frontend/src/app/(routes)/transfers/customTextFields.json b/frontend/src/app/(routes)/transfers/customTextFields.json deleted file mode 100644 index 9aba5c171..000000000 --- a/frontend/src/app/(routes)/transfers/customTextFields.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "send": { - "address": { - "name": "address", - "rules": { - "required": "Address is required" - }, - "textFieldClassName": "bg-[#FFFFFF0D] rounded-2xl", - "textFieldSize": "small", - "placeHolder": "", - "textFieldCustomMuiSx": { - "& .MuiTypography-body1": { - "color": "white", - "fontSize": "12px", - "fontWeight": 200 - }, - "& .MuiOutlinedInput-notchedOutline": { - "border": "none" - }, - "& .MuiOutlinedInput-root": { - "border": "1px solid transparent", - "borderRadius": "16px" - }, - "& .Mui-focused": { - "border": "1px solid #ffffff4a", - "borderRadius": "16px" - } - }, - "inputProps": { - "sx": { - "input": { - "minHeight": "30px", - "color": "white", - "fontSize": "16px", - "paddingX": 2, - "paddingY": 1 - } - } - } - }, - "amount": { - "name": "amount", - "rules": { - "required": "Amount is required", - "pattern": { - "value": "^[0-9]+(\\.[0-9]+)?$", - "message": "Please enter a valid number" - } - }, - "textFieldClassName": "bg-[#FFFFFF0D] rounded-2xl", - "textFieldSize": "small", - "placeHolder": "", - "textFieldCustomMuiSx": { - "& .MuiTypography-body1": { - "color": "rgba(255, 255, 255, 0.75)", - "fontSize": "12px", - "fontWeight": 400 - }, - "& .MuiOutlinedInput-notchedOutline": { - "border": "none" - }, - "& .MuiOutlinedInput-root": { - "border": "1px solid transparent", - "borderRadius": "16px" - }, - "& .Mui-focused": { - "border": "1px solid #ffffff4a", - "borderRadius": "16px" - } - }, - "inputProps": { - "sx": { - "input": { - "minHeight": "30px", - "color": "white", - "fontSize": "16px", - "paddingX": 2, - "paddingY": 1 - } - } - } - }, - "memo": { - "name": "memo", - "textFieldClassName": " rounded-2xl flex flex-col flex-1", - "rules": {}, - "textFieldSize": "small", - "placeHolder": "Enter memo here (optional)", - "textFieldCustomMuiSx": { - "& .MuiInputBase-input": { - "color": "white" - }, - "& .MuiInputLabel-root": { - "color": "white" - }, - "& .MuiOutlinedInput-notchedOutline": { - "border": "none" - }, - "& .MuiInputBase-root": { - "padding": "0" - } - }, - - "inputProps": { - "sx": { - "input": { - "minHeight": "30px", - "color": "white", - "fontSize": "16px", - "paddingX": 0, - "paddingY": 0 - } - } - } - } - }, - - "multi-send": { - "memo": { - "name": "memo", - "rules": {}, - "textFieldClassName": "bg-[#FFFFFF0D] rounded-2xl", - "textFieldSize": "lg", - "placeHolder": "Enter memo here (optional)", - "textFieldCustomMuiSx": { - "& .MuiInputBase-input": { - "color": "white" - }, - "& .MuiInputLabel-root": { - "color": "white" - }, - "& .MuiOutlinedInput-notchedOutline": { - "border": "none" - }, - "& .MuiOutlinedInput-root": { - "border": "1px solid transparent", - "borderRadius": "16px" - }, - "& .Mui-focused": { - "border": "1px solid #ffffff4a", - "borderRadius": "16px" - } - }, - "inputProps": { - "sx": { - "input": { - "color": "white", - "fontSize": "14px", - "padding": 2 - } - } - } - } - } -} diff --git a/frontend/src/app/(routes)/transfers/error.tsx b/frontend/src/app/(routes)/transfers/error.tsx new file mode 100644 index 000000000..b40d121b0 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/error.tsx @@ -0,0 +1,8 @@ +'use client'; + +import Error from '@/components/common/Error'; +import React from 'react'; + +const error = () => ; + +export default error; diff --git a/frontend/src/app/(routes)/transfers/loading.tsx b/frontend/src/app/(routes)/transfers/loading.tsx new file mode 100644 index 000000000..0b08e2215 --- /dev/null +++ b/frontend/src/app/(routes)/transfers/loading.tsx @@ -0,0 +1,8 @@ +'use client'; + +import PageLoading from '@/components/common/PageLoading'; +import React from 'react'; + +const loading = () => ; + +export default loading; diff --git a/frontend/src/app/(routes)/transfers/page.tsx b/frontend/src/app/(routes)/transfers/page.tsx index 9f7951191..e8775a9d8 100644 --- a/frontend/src/app/(routes)/transfers/page.tsx +++ b/frontend/src/app/(routes)/transfers/page.tsx @@ -6,7 +6,7 @@ import './transfers.css'; const Page = () => { const nameToChainsIDs = useAppSelector( - (state) => state.wallet.nameToChainIDs + (state) => state.common.nameToChainIDs ); const chainNames = Object.keys(nameToChainsIDs); return ; diff --git a/frontend/src/app/(routes)/transfers/styles.ts b/frontend/src/app/(routes)/transfers/styles.ts index 14af2c623..41a4f23bd 100644 --- a/frontend/src/app/(routes)/transfers/styles.ts +++ b/frontend/src/app/(routes)/transfers/styles.ts @@ -1,4 +1,117 @@ +import { merge } from 'lodash'; + export const customDialogPaper = { borderRadius: '24px', background: 'linear-gradient(90deg, #704290 0.11%, #241b61 70.28%)', }; + +export const multiSendInputFieldStyles = { + '& .MuiInputBase-input': { + color: 'white', + fontSize: '14px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '1px solid #ffffff10', + borderRadius: '16px', + }, + '& .Mui-focused': { + border: '1px solid #ffffff3a', + borderRadius: '16px', + }, +}; + +export const swapTextFieldStyles = { + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '1px solid transparent', + borderRadius: '12px', + color: 'white', + }, + '& .Mui-focused': { + border: '1px solid #ffffff4a', + borderRadius: '12px', + }, + '& .MuiInputAdornment-root': { + '& button': { + color: 'white', + }, + }, + '& .Mui-disabled': { + WebkitTextFillColor: '#ffffff !important', + }, +}; + +export const customTextFieldStyles = { + '& .MuiInputBase-input': { + color: '#fffffff0', + fontSize: '14px', + fontWeight: 200, + fontFamily: 'Libre Franklin', + lineHeight: '21px', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '0.25px solid #ffffff10', + borderRadius: '100px', + height: '40px', + }, + '& .Mui-focused': { + border: '0.25px solid #ffffff4a', + borderRadius: '100px', + }, +}; + +export const customTransferTextFieldStyles = merge({}, customTextFieldStyles, { + '& .MuiOutlinedInput-root': { + height: '32px', + '@media (min-width: 1500px)': { + height: '36px', + }, + }, +}); + +export const amountFieldStyles = { + '& .MuiInputBase-input': { + color: 'white', + fontSize: '40px', + height: '48px', + fontWeight: 700, + fontFamily: 'Libre Franklin', + padding: '0', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + height: '48px', + }, + '& .MuiOutlinedInput-root': { + border: 'none', + padding: '0', + }, + '& .Mui-focused': { + border: 'none', + borderRadius: '100px', + }, +}; + +export const customAutoCompleteStyles = { + '& .MuiAutocomplete-inputRoot': { + padding: '0px 16px', + fontFamily: 'Libre Franklin', + }, + '& .Mui-disabled': { + '-webkit-text-fill-color': '#fffffff0 !important', + }, +}; diff --git a/frontend/src/app/(routes)/transfers/transfers.css b/frontend/src/app/(routes)/transfers/transfers.css index 13c918a04..92be687f5 100644 --- a/frontend/src/app/(routes)/transfers/transfers.css +++ b/frontend/src/app/(routes)/transfers/transfers.css @@ -9,7 +9,7 @@ .selected { box-sizing: border-box; - background: linear-gradient(180deg, #4AA29C 0%, #8B3DA7 100%); + background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); } .coloured-container { @@ -32,19 +32,227 @@ } .amount-options { - @apply text-white text-sm not-italic font-normal leading-[normal] rounded-[100px]; - background: rgba(255, 255, 255, 0.10); + @apply text-sm not-italic font-normal leading-[normal] rounded-[100px]; + background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(2px); } .amount-options-default { - background: rgba(255, 255, 255, 0.10); + background: rgba(255, 255, 255, 0.1); } .amount-options-fill { background: - linear-gradient(180deg, + linear-gradient( + 180deg, rgba(74, 162, 156, 0.9) 0%, - rgba(139, 61, 167, 0.9) 100%), + rgba(139, 61, 167, 0.9) 100% + ), lightgray 50% / cover no-repeat; -} \ No newline at end of file +} + +.send-menu-item { + @apply cursor-pointer px-2 text-[18px] h-9 flex items-center pb-[14px] leading-[21.7px]; +} + +/* IBC Swap Styles */ + +.swap-btn { + @apply rounded-lg text-[12px] font-medium tracking-[0.48px] py-[10px] px-4 w-full flex justify-center items-center; +} + +.drop-down { + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); +} + +.multisend-input-box { + @apply flex justify-center items-center px-4 py-4 bg-[#FFFFFF1A] min-h-[152px] rounded-3xl w-full cursor-pointer; +} + +.multisend-toggle-btn-group { + @apply bg-[#FFFFFF1A] rounded-2xl flex h-[34px]; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25) inset; +} + +.multisend-btn { + @apply text-[#FFFFFF80] text-[12px] text-center px-4 min-w-[127px]; +} + +.multisend-btn-active { + @apply text-[#fffffff0] rounded-2xl; + background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); + box-shadow: 0px 4px 4px 0px #100; +} + +.single-send-box { + @apply bg-[#FFFFFF05] rounded-2xl w-full; +} + +.amount-input-field { + @apply bg-transparent w-full border-none focus:outline-none text-[28px] font-bold placeholder:text-[#FFFFFF33]; +} + +.select-network { + @apply h-[64px] desktop:h-[80px] py-6 px-6 flex items-center; + border-radius: 16px 16px 0px 0px; + background: linear-gradient( + 180deg, + rgba(136, 8, 8, 0.5) 0%, + rgba(18, 19, 28, 0.5) 100% + ); +} + +.upload-box { + @apply rounded-3xl px-6 py-[10.5px] flex items-center justify-between cursor-pointer; + border: 2px dashed #ffffff20; +} + +.network-image-container { + position: relative; + display: inline-block; +} + +.blur-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + filter: blur(15px); + z-index: 0; +} + +.circle-background { + position: relative; + width: 100px; + height: 100px; + border-radius: 50%; + background-color: black; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.network-image { + width: 32px; + height: 32px; + border-radius: 50%; +} + +.dotted-line { + flex-grow: 1; + height: 1px; + background: repeating-linear-gradient( + to right, + #4453df, + #7f5ced 6px, + transparent 4px, + transparent 12px + ); + margin: 0 10px; + display: flex; + justify-content: center; + align-items: center; +} + +.dotted-line-large { + flex-grow: 1; + height: 2px; + background: repeating-linear-gradient( + to right, + #4453df, + #7f5ced 6px, + transparent 10px, + transparent 20px + ); + margin: 0 10px; + display: flex; + justify-content: center; + align-items: center; +} + +.custom-scroll { + animation: scroll 2s linear infinite; +} + +.send-loading { + margin-bottom: 50px; +} + +.multi-send-loading { + @apply flex flex-col gap-3; + margin-bottom: 50px; +} + +@keyframes scroll { + from { + background-position: 0 0; + } + to { + background-position: 20px 0; + } +} + +.tick-mark { + position: absolute; + top: -10px; + width: 20px; + height: 20px; +} + +.custom-spin { + animation-name: custom-spin-animation; + animation-duration: 3000ms; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +@keyframes custom-spin-animation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.custom-opacity-animation { + animation: opacityAnimation 2s linear infinite; +} + +@keyframes opacityAnimation { + 0% { + opacity: 1; + } + 50% { + opacity: 0.3; + } + 100% { + opacity: 1; + } +} + +.globe-container { + display: flex; + justify-content: center; + align-items: center; +} + +.animate-rotate-x { + animation: rotateY 8s linear infinite; +} + +@keyframes rotateY { + from { + transform: rotateY(0deg); + } + to { + transform: rotateY(360deg); + } +} + +.txn-summary { + @apply px-6 py-3 rounded-2xl bg-[#FFFFFF14] text-[14px] space-y-2 text-[#ffffff80] italic text-center; +} diff --git a/frontend/src/app/(routes)/validator/[validator]/ValidatorProfile.tsx b/frontend/src/app/(routes)/validator/[validator]/ValidatorProfile.tsx new file mode 100644 index 000000000..fc68e6fbc --- /dev/null +++ b/frontend/src/app/(routes)/validator/[validator]/ValidatorProfile.tsx @@ -0,0 +1,83 @@ +'use client'; + +import useGetValidatorInfo from '@/custom-hooks/useGetValidatorInfo'; +import useInitAllValidator from '@/custom-hooks/useInitAllValidator'; +import React from 'react'; +import ValidatorHeader from './components/ValidatorHeader'; +import ValidatorsTable from './components/ValidatorsTable'; +import { VITWIT_VALIDATOR_NAMES } from '@/utils/constants'; +import SectionHeader from '@/components/common/SectionHeader'; +import { capitalizeFirstLetter } from '@/utils/util'; + +const ValidatorProfile = ({ moniker }: { moniker: string }) => { + useInitAllValidator(); + const { + getChainwiseValidatorInfo, + getOasisValidatorInfo, + getPolygonValidatorInfo, + getValidatorStats, + } = useGetValidatorInfo(); + const { + chainWiseValidatorData, + validatorDescription, + validatorIdentity, + validatorWebsite, + } = getChainwiseValidatorInfo({ moniker }); + + const validatorStatsResult = getValidatorStats({ + data: chainWiseValidatorData, + moniker: moniker, + }); + + const { avgCommission, activeNetworks, totalNetworks } = validatorStatsResult; + + let { totalDelegators, totalStaked } = validatorStatsResult; + + const { + totalStakedInUSD: totalPolygonStaked, + totalDelegators: totalPolygonDelegators, + } = getPolygonValidatorInfo(); + const { + totalStakedInUSD: totalOasisStaked, + totalDelegators: totalOasisDelegator, + } = getOasisValidatorInfo(); + + const isVitwitValidator = VITWIT_VALIDATOR_NAMES.includes( + moniker.toLowerCase() + ); + + if (isVitwitValidator) { + totalStaked += totalPolygonStaked || 0; + totalStaked += totalOasisStaked || 0; + totalDelegators += totalPolygonDelegators; + totalDelegators += totalOasisDelegator; + } + + return ( +
    + +
    + + +
    +
    + ); +}; + +export default ValidatorProfile; diff --git a/frontend/src/app/(routes)/validator/[validator]/components/NetworkItem.tsx b/frontend/src/app/(routes)/validator/[validator]/components/NetworkItem.tsx new file mode 100644 index 000000000..6991f3520 --- /dev/null +++ b/frontend/src/app/(routes)/validator/[validator]/components/NetworkItem.tsx @@ -0,0 +1,35 @@ +import Copy from '@/components/common/Copy'; +import { capitalizeFirstLetter, shortenName } from '@/utils/util'; +import { Tooltip } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const NetworkItem = ({ + logo, + networkName, + operatorAddress, +}: { + networkName: string; + logo: string; + operatorAddress: string; +}) => { + return ( +
    + {networkName} + +

    + {shortenName(capitalizeFirstLetter(networkName), 10)} +

    +
    + +
    + ); +}; + +export default NetworkItem; diff --git a/frontend/src/app/(routes)/validator/[validator]/components/TableHeader.tsx b/frontend/src/app/(routes)/validator/[validator]/components/TableHeader.tsx new file mode 100644 index 000000000..7a0b35080 --- /dev/null +++ b/frontend/src/app/(routes)/validator/[validator]/components/TableHeader.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +const TableHeader = ({ title }: { title: string }) => { + return ( + +
    + {title} +
    + + ); +}; + +export default TableHeader; diff --git a/frontend/src/app/(routes)/validator/[validator]/components/ValidatorHeader.tsx b/frontend/src/app/(routes)/validator/[validator]/components/ValidatorHeader.tsx new file mode 100644 index 000000000..6b366e231 --- /dev/null +++ b/frontend/src/app/(routes)/validator/[validator]/components/ValidatorHeader.tsx @@ -0,0 +1,119 @@ +import ValidatorLogo from '@/app/(routes)/staking/components/ValidatorLogo'; +import { REDIRECT_ICON } from '@/constants/image-names'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { + VITWIT, + VITWIT_VALIDATOR_DESCRIPTION, + WITVAL, +} from '@/utils/constants'; +import { capitalizeFirstLetter, formatValidatorStatsValue } from '@/utils/util'; +import Image from 'next/image'; +import Link from 'next/link'; +import React from 'react'; + +interface ValidatorHeaderProps { + name: string; + website: string; + identity: string; + description: string; + totalStaked: number; + totalDelegators: number; + avgCommission: number; + totalNetworks: number; + activeNetworks: number; +} + +const ValidatorHeader = (props: ValidatorHeaderProps) => { + const { + activeNetworks, + avgCommission, + description, + identity, + name, + totalDelegators, + totalNetworks, + totalStaked, + website, + } = props; + const isWitval = name.toLowerCase() === WITVAL; + const totalStakedAmount = formatValidatorStatsValue(totalStaked, 0); + const totalDelegatorsCount = formatValidatorStatsValue(totalDelegators, 0); + const parsedAvgCommission = formatValidatorStatsValue(avgCommission, 2); + const validatorsLoadingCount = useAppSelector( + (state) => state.staking.validatorsLoading + ); + const isLoading = validatorsLoadingCount > 0; + return ( +
    +
    +
    +
    + +
    + {isWitval ? VITWIT : capitalizeFirstLetter(name)} +
    + {!isLoading ? ( + + + + ) : null} +
    +
    + {isWitval + ? VITWIT_VALIDATOR_DESCRIPTION + : description || 'No description available'} +
    +
    +
    +
    +
    + + + + + +
    +
    + ); +}; + +export default ValidatorHeader; + +const StatsCard = ({ + name, + value, + isLoading, +}: { + name: string; + value: string; + isLoading: boolean; +}) => { + return ( +
    +
    + {name} +
    +
    {value}
    +
    + ); +}; diff --git a/frontend/src/app/(routes)/validator/[validator]/components/ValidatorItem.tsx b/frontend/src/app/(routes)/validator/[validator]/components/ValidatorItem.tsx new file mode 100644 index 000000000..7825891f9 --- /dev/null +++ b/frontend/src/app/(routes)/validator/[validator]/components/ValidatorItem.tsx @@ -0,0 +1,76 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getTotalDelegationsCount } from '@/store/features/staking/stakeSlice'; +import { ValidatorProfileInfo } from '@/types/staking'; +import { formatCommission, formatValidatorStatsValue } from '@/utils/util'; +import React, { useEffect } from 'react'; +import NetworkItem from './NetworkItem'; +import useGetAllChainsInfo from '@/custom-hooks/useGetAllChainsInfo'; +import { Tooltip } from '@mui/material'; +import Link from 'next/link'; + +const ValidatorItem = ({ + validatorInfo, +}: { + validatorInfo: ValidatorProfileInfo; +}) => { + const { chainID, commission, totalStakedInUSD, tokens, operatorAddress } = + validatorInfo; + const { getAllChainInfo } = useGetAllChainsInfo(); + const { chainName, chainLogo, restURLs } = getAllChainInfo(chainID); + const dispatch = useAppDispatch(); + + const totalDelegators = useAppSelector( + (state) => + state.staking.chains[chainID].validatorProfiles?.[operatorAddress] + ?.totalDelegators + ); + const votingPower = formatValidatorStatsValue(tokens, 0); + const totalStaked = formatValidatorStatsValue(totalStakedInUSD, 0); + const totalDelegatorsCount = formatValidatorStatsValue(totalDelegators, 0); + + const connected = useAppSelector((state) => state.wallet.connected); + + useEffect(() => { + if (operatorAddress?.length) { + dispatch( + getTotalDelegationsCount({ + baseURLs: restURLs, + chainID, + operatorAddress, + }) + ); + } + }, [operatorAddress]); + + return ( + + + + + {votingPower} + {totalDelegatorsCount !== '0' ? totalDelegatorsCount : '-'} + {formatCommission(commission)} + {'$ ' + totalStaked} + + {connected ? ( + + Stake + + ) : ( + + + + )} + + + ); +}; + +export default ValidatorItem; diff --git a/frontend/src/app/(routes)/validator/[validator]/components/ValidatorsLoading.tsx b/frontend/src/app/(routes)/validator/[validator]/components/ValidatorsLoading.tsx new file mode 100644 index 000000000..dd0af081a --- /dev/null +++ b/frontend/src/app/(routes)/validator/[validator]/components/ValidatorsLoading.tsx @@ -0,0 +1,20 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React from 'react'; + +const ValidatorsLoading = () => { + const validatorsLoadingCount = useAppSelector( + (state) => state.staking.validatorsLoading + ); + return ( + <> + {validatorsLoadingCount > 0 ? ( +
    +
    +
    +
    + ) : null} + + ); +}; + +export default ValidatorsLoading; diff --git a/frontend/src/app/(routes)/validator/[validator]/components/ValidatorsTable.tsx b/frontend/src/app/(routes)/validator/[validator]/components/ValidatorsTable.tsx new file mode 100644 index 000000000..9fe04e3b0 --- /dev/null +++ b/frontend/src/app/(routes)/validator/[validator]/components/ValidatorsTable.tsx @@ -0,0 +1,129 @@ +import { ValidatorProfileInfo } from '@/types/staking'; +import React from 'react'; +import ValidatorItem from './ValidatorItem'; +import NetworkItem from './NetworkItem'; +import useGetValidatorInfo from '@/custom-hooks/useGetValidatorInfo'; +import { OASIS_CONFIG, POLYGON_CONFIG } from '@/utils/constants'; +import { formatCommission, formatValidatorStatsValue } from '@/utils/util'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import Link from 'next/link'; +import { Tooltip } from '@mui/material'; +import TableHeader from './TableHeader'; +import ValidatorsLoading from './ValidatorsLoading'; + +const ValidatorsTable = ({ + data, + isWitval, +}: { + data: Record; + isWitval: boolean; +}) => { + const columnTitles = [ + 'Network', + 'Voting Power', + 'Total Delegators', + 'Commission', + 'Total Staked Assets', + 'Actions', + ]; + + const sortedKeys = Object.keys(data).sort((a, b) => { + return ( + parseInt(data[b].totalStakedInUSD) - parseInt(data[a].totalStakedInUSD) + ); + }); + + const sortedObject: Record = {}; + sortedKeys.forEach((key) => { + sortedObject[key] = data[key]; + }); + + return ( +
    +
    +
    +
    + + + + {columnTitles.map((title) => ( + + ))} + + + + {isWitval ? ( + <> + + + + ) : null} + {Object.keys(sortedObject).map((chainID) => { + return ( + + ); + })} + +
    + +
    +
    +
    +
    + ); +}; + +export default ValidatorsTable; + +const NonCosmosValidators = ({ networkName }: { networkName: string }) => { + const { getPolygonValidatorInfo, getOasisValidatorInfo } = + useGetValidatorInfo(); + const { + commission, + totalDelegators, + totalStakedInUSD, + totalStakedTokens, + operatorAddress, + } = + networkName === 'polygon' + ? getPolygonValidatorInfo() + : getOasisValidatorInfo(); + const totalStaked = formatValidatorStatsValue(totalStakedInUSD.toString(), 0); + const votingPower = formatValidatorStatsValue( + totalStakedTokens.toString(), + 0 + ); + const { logo, witval } = + networkName === 'polygon' ? POLYGON_CONFIG : OASIS_CONFIG; + const connected = useAppSelector((state) => state.wallet.connected); + + return ( + + + + + {votingPower || '-'} + {totalDelegators !== 0 ? totalDelegators.toLocaleString() : '-'} + {formatCommission(Number(commission))} + {totalStaked !== '0' ? '$ ' + totalStaked : '$ -'} + + {connected ? ( + + + + ) : ( + + + + )} + + + ); +}; diff --git a/frontend/src/app/(routes)/validator/[validator]/page.tsx b/frontend/src/app/(routes)/validator/[validator]/page.tsx new file mode 100644 index 000000000..b84074ae2 --- /dev/null +++ b/frontend/src/app/(routes)/validator/[validator]/page.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import '../validator-profile.css'; +import '../../staking/staking.css'; +import { VITWIT_NEW_MONIKER, VITWIT_VALIDATOR_NAMES } from '@/utils/constants'; +import ValidatorProfile from './ValidatorProfile'; + +const page = ({ params }: { params: { validator: string } }) => { + const decodedMonikerName = decodeURIComponent(params.validator); + // If the moniker name is vitwit or vitwit (previously witval) or witval use new moniker name + const isVitwitValidator = VITWIT_VALIDATOR_NAMES.includes( + decodedMonikerName.toLowerCase() + ); + const monikerName = isVitwitValidator + ? decodeURIComponent(VITWIT_NEW_MONIKER) + : decodedMonikerName.toLocaleLowerCase(); + return ; +}; + +export default page; diff --git a/frontend/src/app/(routes)/validator/validator-profile.css b/frontend/src/app/(routes)/validator/validator-profile.css new file mode 100644 index 000000000..730fd341f --- /dev/null +++ b/frontend/src/app/(routes)/validator/validator-profile.css @@ -0,0 +1,23 @@ +.validators-table { + @apply text-[#fffffff0] flex flex-col flex-1; +} + +.validators-table th { + @apply pb-4; +} + +.validators-table td { + @apply text-[14px] text-[#fffffff0] py-4 px-6; +} + +.validators-table tr { + @apply mb-2; +} + +.validator-stats-card { + @apply bg-[#FFFFFF05] p-4 flex flex-col gap-2 items-center justify-center flex-1 rounded-2xl; +} + +.validator-description { + @apply text-[#FFFFFF80] font-extralight text-[14px] leading-8; +} diff --git a/frontend/src/app/fixed-layout.css b/frontend/src/app/fixed-layout.css new file mode 100644 index 000000000..0f93dbcfe --- /dev/null +++ b/frontend/src/app/fixed-layout.css @@ -0,0 +1,105 @@ +.main { + --main-bg-color: #09090a; + background: radial-gradient( + 80% 48% at 50% 50%, + #4e2d954d 0%, + rgba(9, 9, 10, 0.3) 100% + ) + fixed; +} + +.fixed-layout { + @apply w-full h-screen flex flex-col; +} + +.main { + @apply bg-[var(--main-bg-color)] flex flex-1 w-full justify-center; +} + +.main-container { + @apply w-full max-w-[1512px] flex; +} + +.sidebar { + @apply overflow-hidden fixed px-6 w-[240px] border-r-[1px] border-[#1c1c20] py-10 flex flex-col justify-between; +} + +.fixed-top { + @apply flex-none; +} + +.scrollable-content { + @apply overflow-y-auto flex-1 my-10; +} + +.fixed-bottom { + @apply flex-none; +} + +.network-icon-bg { + @apply rounded-full flex justify-center items-center; +} + +.menu-item-selected { + @apply font-semibold; + background: #ffffff14; +} + +.dynamic-section { + @apply ml-[240px] w-full; + /* background: radial-gradient( + 50% 50% at 50% 50%, + rgba(68, 83, 223, 0.2) 0%, + rgba(9, 9, 10, 0.2) 100% + ); */ +} + +.menu-item { + @apply w-full flex gap-2 !h-8 items-center pl-3 pr-1 rounded-full hover:bg-[#FFFFFF0A]; +} + +.menu-item-name { + @apply text-[14px] leading-[21px]; +} + +.top-bar { + @apply bg-[#0f0f11] flex justify-center items-center min-h-[60px] fixed top-0 left-0 right-0 z-[999]; + box-shadow: 0px 4px 4px 0px rgba(17, 0, 0, 0.388); +} + +.top-bar nav { + @apply px-6 w-full max-w-[1512px] flex justify-between items-center; +} + +.profile-section { + @apply w-[472px] h-full px-6 pt-10 pb-[60px]; +} + +.connect-wallet-popup { + @apply max-w-[1000px] w-full p-4 space-y-10; +} + +.select-network-popup { + @apply w-[1000px] max-w-[1000px] pt-4 pb-6 px-10 space-y-10; +} + +.search-network-field { + @apply py-4 px-6 bg-[#FFFFFF05] rounded-full flex flex-1; +} + +.search-network-input { + @apply w-full border-none cursor-text focus:outline-none bg-transparent placeholder:text-[#FFFFFF30] placeholder:font-normal text-[#ffffff]; +} + +.network-item { + @apply p-2 flex items-center gap-2 border-[0.25px] border-[#ffffff21] rounded-2xl hover:bg-[#ffffff14] hover:border-transparent; +} + +.network-item .network-name { + @apply text-[14px] leading-normal opacity-100 font-normal text-[#fffffff0]; +} + +.logout-btn { + border-image: none !important; + background: #d921011a !important; +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 3198d3735..164a38936 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1,381 +1,228 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Libre+Franklin:ital,wght@0,100..900;1,100..900&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; +:root { + --primary-text-color: #fffffff0; + --secondary-text-color: #ffffff80; + --main-bg: #09090a; +} + ::-webkit-scrollbar { width: 0; - /* Remove scrollbar space */ background: transparent; - /* Optional: just make scrollbar invisible */ } -/* Optional: show position indicator in red */ -::-webkit-scrollbar-thumb { - background: #393737; +*, +*::before, +*::after { + @apply box-border; } -.disable-draggable { - user-drag: none; - -webkit-user-drag: none; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; +body { + color: var(--primary-text-color); + font-family: 'Libre Franklin', sans-serif !important; + font-optical-sizing: auto !important; + background: var(--main-bg); + @apply text-customTextColor; } - -.main { - @apply leading-[normal] m-0 text-white; - background-color: #0b071d; - font-family: 'Inter', sans-serif; +.text-bg { + background: linear-gradient(270deg, #fff -67.89%, #999 99.95%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.right-section { - @apply flex flex-col p-6 bg-[#0e0b26] min-w-[500px] min-h-screen max-h-screen overflow-y-scroll; - box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); +.table-border-line { + @apply h-[1px] self-stretch border-b border-[#1C1C20]; } - -.page-padding { - @apply px-10 py-6; +.horizontal-line { + @apply h-[1px] self-stretch bg-[#1C1C20]; } - -@media screen and (max-width: 1450px) { - .right-section { - @apply flex flex-col p-6 bg-[#0e0b26] min-w-[450px] min-h-screen max-h-screen overflow-y-scroll; - box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); - } - - .page-padding { - @apply px-5 py-3; - } +.primary-btn { + @apply border-[1px] text-[14px] rounded-full px-4 py-[10.5px] leading-[21px] !h-8 flex justify-center items-center text-[#fffffff0] hover:bg-[#ffffff14]; + background: linear-gradient( + 180deg, + rgba(68, 83, 223, 0.1) 12.5%, + rgba(127, 92, 237, 0.1) 87.5% + ); + border-image: url("data:image/svg+xml,%3csvg width='149' height='42' viewBox='0 0 149 42' fill='none' xmlns='http://www.w3.org/2000/svg'%3e %3crect x='0.5' y='0.5' width='148' height='41' rx='20.5' stroke='url(%23paint0_linear_2064_7866)'/%3e %3cdefs%3e %3clinearGradient id='paint0_linear_2064_7866' x1='74.5' y1='1' x2='74.5' y2='41' gradientUnits='userSpaceOnUse'%3e %3cstop stop-color='%234453DF'/%3e %3cstop offset='1' stop-color='%237F5CED'/%3e %3c/linearGradient%3e %3c/defs%3e %3c/svg%3e") + 20 / 40px stretch; } - -*, -::before, -::after { - border-width: 0; +.delete-btn { + @apply border-[1px] border-[#D92101] bg-[#D921011A] text-[14px] rounded-full px-4 py-[10.5px] leading-[21px] !h-8 flex justify-center items-center text-[#fffffff0] hover:bg-[#D92101]; } -.blur-effect { - backdrop-filter: blur(2px); +.btn-border-primary { + background: linear-gradient( + 180deg, + rgba(68, 83, 223, 0.1) 12.5%, + rgba(127, 92, 237, 0.1) 87.5% + ); + border-image: url("data:image/svg+xml,%3csvg width='149' height='42' viewBox='0 0 149 42' fill='none' xmlns='http://www.w3.org/2000/svg'%3e %3crect x='0.5' y='0.5' width='148' height='41' rx='20.5' stroke='url(%23paint0_linear_2064_7866)'/%3e %3cdefs%3e %3clinearGradient id='paint0_linear_2064_7866' x1='74.5' y1='1' x2='74.5' y2='41' gradientUnits='userSpaceOnUse'%3e %3cstop stop-color='%234453DF'/%3e %3cstop offset='1' stop-color='%237F5CED'/%3e %3c/linearGradient%3e %3c/defs%3e %3c/svg%3e") + 20 / 40px stretch; } -@layer base { - .txt-xs { - @apply text-[12px] leading-normal; - } - - .txt-sm { - @apply text-[14px] leading-normal; - } - - .txt-md { - @apply text-[16px] leading-normal; - } - - .txt-lg { - @apply text-[20px] leading-normal; - } - - .txt-xl { - @apply text-[24px] leading-normal; - } +.secondary-text { + @apply text-[var(--secondary-text-color)] font-extralight text-[14px]; } -@layer components { - .text-capitalize { - text-transform: capitalize; - } - - .primary-gradient { - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); - } - - .flex-center-center { - @apply flex justify-center items-center; - } +.secondary-btn { + @apply text-[var(--secondary-text-color)] font-extralight text-[14px] underline underline-offset-[2px] cursor-pointer; } -/* Home page styles */ -@layer components { - .connect-wallet { - @apply px-[120px] flex flex-col justify-center h-screen; - font-family: 'Space Grotesk' !important; - background: - url('/cosmos-background.png'), - lightgray 50% / cover; - } - - .space-ship-image-1 { - @apply absolute top-[72px] left-[272px]; - } - - .space-ship-image-2 { - @apply absolute top-[180px] left-[282px]; - } - - .connect-wallet-header { - @apply pt-6 absolute top-0; - } - - .home-title { - @apply relative flex gap-[140px] text-white text-[150px] font-bold leading-[175px] tracking-[6px]; - } - - .home-title img { - @apply absolute left-[170px] bottom-[-20px]; - } - - .home-title h1 { - @apply z-[10]; - } - - .home-title-caption h2 { - @apply text-white text-[48px] font-light leading-[90px] tracking-[1.92px]; - } - - .connect-wallet-btn { - @apply text-white px-[54px] py-6 rounded-full border-[4px] border-[#612155] text-[24px] font-bold leading-8 tracking-[1.92px]; - } - - .primary-action-btn { - @apply w-[152px] h-[44px] rounded-2xl text-white text-[16px] font-medium leading-3; - background: - linear-gradient( - 180deg, - rgba(74, 162, 156, 0.9) 0%, - rgba(139, 61, 167, 0.9) 100% - ), - lightgray 50% / cover no-repeat; - } - - .custom-btn { - @apply rounded-2xl text-white text-[16px] font-medium leading-3; - background: - linear-gradient( - 180deg, - rgba(74, 162, 156, 0.9) 0%, - rgba(139, 61, 167, 0.9) 100% - ), - lightgray 50% / cover no-repeat; - } - - .primary-custom-btn { - @apply rounded-2xl text-white text-base not-italic font-medium leading-5 tracking-[0.64px] h-10 flex items-center justify-center px-10; - background: - linear-gradient( - 180deg, - rgba(74, 162, 156, 0.9) 0%, - rgba(139, 61, 167, 0.9) 100% - ), - lightgray 50% / cover no-repeat; - } - - .primary-custom-btn-disabled { - @apply bg-[#FFFFFF1A] text-[#FFFFFF1A] cursor-not-allowed rounded-2xl text-base not-italic font-medium leading-5 tracking-[0.64px] h-10 flex items-center justify-center px-10; - background: #ffffff1a; - } - - .secondary-custom-btn { - @apply rounded-lg text-white text-xs not-italic font-medium leading-5 tracking-[0.48px] h-8 flex items-center justify-center px-3; - background: - linear-gradient( - 180deg, - rgba(74, 162, 156, 0.9) 0%, - rgba(139, 61, 167, 0.9) 100% - ), - lightgray 50% / cover no-repeat; - } - - .recent-txn-item-icon { - @apply rounded-lg; - background: linear-gradient(180deg, #0b071d 0%, #0e0b26 100%); - box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25) inset; - } - - .chip { - @apply rounded-2xl px-2 py-1 text-[12px]; - } - - .fill { - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); - } - - .formatted-text-1 { - @apply text-sm font-normal leading-[14px] max-w-[293px] truncate; - } - - .overflowed-text { - @apply text-sm font-normal leading-[14px]; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .disabled { - opacity: 0.3; - cursor: not-allowed; - } - - .recent-txn-item-icon { - @apply rounded-lg; - background: linear-gradient(180deg, #0b071d 0%, #0e0b26 100%); - box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25) inset; - } - - .fill { - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); - } +.divider-line { + @apply w-full bg-[#FFFFFF80] h-[0.25px] opacity-20; } -/* Connect Wallet styles */ -@layer components { - .custom-box { - @apply w-[1102px] h-[300px] rounded-2xl; - justify-content: center; - background-color: #20172f; - padding: 24px; - display: flex; - } - - .add-wallet-header { - @apply flex justify-between items-center relative; - } - - .add-wallet-header .dialog-close-icon { - @apply absolute right-0; - } - - .dialog-close-icon { - @apply cursor-pointer; - } - - .add-wallet-header h2 { - @apply flex-col font-bold text-white text-[20px]; - margin: auto; - } - - .add-wallet-dialog-content { - @apply flex w-full justify-between mt-10; - padding: 0 180px; - } +.flex-center { + @apply flex justify-center items-center; +} - .wallet { - @apply w-[172px] h-[140px] flex flex-col justify-center items-center cursor-pointer rounded-md hover:bg-[#3B3148]; - } +.form-label-text { + @apply text-[var(--secondary-text-color)] font-extralight text-[12px] leading-[24px]; +} - .wallet-name { - @apply font-bold text-white items-center text-center leading-3 text-[20px] h-10 flex; - } +.dots-flashing:after { + content: ' .'; + animation: dots 1s steps(5, end) infinite; +} - .logout-box { - @apply flex w-[788px] h-[376px] flex-col justify-center items-start gap-10 relative backdrop-blur-[2px] px-6 py-10 rounded-2xl; - background: #20172f; - } +.dots-loader:after { + content: ' ...'; + animation: dots 1s steps(5, end) infinite; } -/* Sidebar styles */ -@layer components { - .sidebar-logo { - @apply w-[55px] h-[30px] shrink-0; - } +.btn-small { + @apply border-[1px] text-[14px] rounded-full px-4 py-1 leading-[19px] h-[25px] flex justify-center items-center; + background: linear-gradient( + 180deg, + rgba(68, 83, 223, 0.1) 12.5%, + rgba(127, 92, 237, 0.1) 87.5% + ); + border-image: url("data:image/svg+xml,%3csvg width='149' height='42' viewBox='0 0 149 42' fill='none' xmlns='http://www.w3.org/2000/svg'%3e %3crect x='0.5' y='0.5' width='148' height='41' rx='20.5' stroke='url(%23paint0_linear_2064_7866)'/%3e %3cdefs%3e %3clinearGradient id='paint0_linear_2064_7866' x1='74.5' y1='1' x2='74.5' y2='41' gradientUnits='userSpaceOnUse'%3e %3cstop stop-color='%234453DF'/%3e %3cstop offset='1' stop-color='%237F5CED'/%3e %3c/linearGradient%3e %3c/defs%3e %3c/svg%3e") + 20 / 40px stretch; +} - .sidebar { - @apply flex w-[87px] flex-col justify-between items-center shrink-0 px-4 py-8 h-screen; - background: #0e0b26; - box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); - } +.flex-center-center { + @apply flex justify-center items-center; +} - .sidebar-menu-item { - @apply cursor-pointer w-12 h-12 flex items-center justify-center rounded-lg hover:bg-[#221F38]; +@keyframes dots { + 0%, + 20% { + color: rgba(0, 0, 0, 0); + text-shadow: + 0.25em 0 0 rgba(0, 0, 0, 0), + 0.5em 0 0 rgba(0, 0, 0, 0); } - .sidebar-menu-item-selected { - background-image: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); + 40% { + color: white; + text-shadow: + 0.25em 0 0 rgba(0, 0, 0, 0), + 0.5em 0 0 rgba(0, 0, 0, 0); } - .main { - @apply flex; + 60% { + text-shadow: + 0.25em 0 0 white, + 0.5em 0 0 rgba(0, 0, 0, 0); } - /* Side ads styles */ - .ad-close { - @apply absolute right-3 top-1 cursor-pointer rounded-full bg-[#ffffff1a]; + 80%, + 100% { + text-shadow: + 0.25em 0 0 white, + 0.5em 0 0 white; } } -/* Select Network styles */ - -.select-network, -.add-network { - @apply text-white rounded-3xl; +.empty-screen-title { + @apply text-[18px] font-bold; + background: linear-gradient(270deg, #fff -67.89%, #999 99.95%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.add-network-button { - @apply text-white rounded-lg px-4 py-[6px] text-[12px] font-medium leading-[20px] tracking-[0.48px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.empty-screen-description { + @apply text-[14px] font-extralight tracking-[1.6px] leading-[21px]; + background: linear-gradient(270deg, #fff -67.89%, #999 99.95%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.networks-list { - @apply grid grid-cols-5 gap-6; +.text-h1 { + @apply text-[20px] font-bold text-[#fffffff0]; } -.network-item { - @apply p-4 rounded-2xl h-16 flex gap-2 items-center cursor-pointer bg-[#FFFFFF0D] hover:bg-[#ffffff18]; +.text-h2 { + @apply text-[18px] text-[#fffffff0]; } -.network-item-selected { - @apply rounded-2xl; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.text-b1 { + @apply text-[14px] leading-[21px] text-[#fffffff0]; } -.wallet-address { - @apply flex items-center gap-2 opacity-80 text-white text-center text-sm font-normal leading-[normal] p-2 rounded-lg w-[176px] h-[36px]; - background: rgba(255, 255, 255, 0.1); +.text-b1-light { + @apply text-[14px] text-[var(--secondary-text-color)] font-extralight; } -.all-networks { - @apply flex items-center gap-2 text-white text-center text-sm font-normal leading-[normal] py-2 rounded-lg h-[36px]; +.text-small { + @apply text-[12px] text-[var(--secondary-text-color)]; } -.message { - @apply bg-[#FFFFFF0D] p-4 rounded-2xl mx-10 mb-6 text-[14px] font-bold text-center truncate; +.text-small-light { + @apply text-[12px] text-[var(--secondary-text-color)] font-extralight; } -.common-copy { - @apply flex items-center gap-2 opacity-80 p-2 rounded-lg; - background: rgba(255, 255, 255, 0.1); +.text-btn { + @apply text-[var(--secondary-text-color)] font-extralight text-[14px] underline underline-offset-[2px]; } -.divider-line { - @apply h-[1px] w-full my-4 bg-[#ffffff35] opacity-50; +.txn-status-data { + @apply flex items-center justify-center gap-2 px-6 py-[10.5px] bg-[#FFFFFF05] rounded-full w-full; } -/* Add new network styles */ - -.add-network { - @apply w-[890px] min-h-[485px]; +.more-msgs { + @apply bg-[#FFFFFF14] flex justify-center items-center text-[12px] h-6 min-w-6 rounded-2xl p-2; } - -.file-upload-box { - @apply flex justify-center items-center px-4 py-10 bg-[#FFFFFF1A] min-h-[152px] rounded-3xl w-full cursor-pointer; +.search-bar { + @apply flex h-14 justify-between items-center gap-2 w-full px-6 py-2 rounded-[100px]; + background: rgba(255, 255, 255, 0.02); } - -.add-network-json-sample-link { - @apply underline underline-offset-2 cursor-pointer; +input[type='checkbox'] { + accent-color: #2ba472; + display: block; } - -.show-more-errors { - @apply underline underline-offset-2 cursor-pointer text-[12px]; +.dashboard-card { + @apply flex flex-col items-center gap-2 p-4 rounded-2xl; + background: rgba(255, 255, 255, 0.02); } - -.gradient-bg { - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.more-popup-grid { + @apply backdrop-blur-[15px] flex w-[200px] flex-col items-start rounded-2xl; + background: rgba(255, 255, 255, 0.08); +} +.search-text { + @apply w-full border-none cursor-text focus:outline-none bg-transparent placeholder:text-[#FFFFFF30] placeholder:font-normal text-[#ffffff]; +} +.footer-bg { + @apply flex flex-col items-start gap-6 w-full px-10 py-24; + background: rgba(255, 255, 255, 0.02); +} +.profile-grid, +.txn-data { + @apply flex flex-col justify-center items-center gap-2 p-4 rounded-2xl; + background: rgba(255, 255, 255, 0.02); } -.add-network-button-2 { - @apply text-center px-10 py-[10px] text-[16px] leading-[20px] rounded-2xl w-[144px] font-medium; +.delete-network-button { + @apply w-full !border-[#D92101] !bg-[#D921011A]; + border-image: none !important; + background: #d921011a !important; } .custom-radio-button { @@ -389,215 +236,56 @@ .custom-radio-button-checked { @apply h-[6px] w-[6px] bg-white rounded-full; } - -.chain-exist-error { - @apply text-red-600 font-bold; +.selected-filters { + @apply flex justify-center items-center gap-4 px-4 py-[10.5px] rounded-lg border-[0.25px] hover:bg-[#ffffff14] hover:border-transparent; } - -/* Transaction success Popup styles */ -.transaction-box { - @apply flex flex-col justify-center items-center gap-6 opacity-95 backdrop-blur-[2px] rounded-3xl; - background: linear-gradient(90deg, #704290 0.11%, #241b61 70.28%); +.selected-msgs { + @apply flex justify-center items-center gap-4 px-4 py-2 rounded-lg border-[0.25px] hover:bg-[#ffffff14] hover:border-transparent; } -.cross { - /* @apply flex justify-end items-center self-stretch gap-2.5 pl-6 pr-10 py-4; */ - @apply flex justify-end items-center gap-2.5 self-stretch pl-6 pr-10 pt-10 pb-6; +.shimmer { + @apply bg-[#252525] animate-pulse; } -.transaction-inner-grid { - @apply flex flex-col justify-center items-start gap-4 self-stretch opacity-80 px-4 py-2 rounded-2xl; - background: rgba(255, 255, 255, 0.05); +.shimmer-line { + @apply bg-[#252525] animate-pulse rounded h-5; } -.button { - @apply flex justify-center items-center gap-6 px-10 py-2.5 rounded-2xl text-white text-base font-medium leading-5 tracking-[0.64px]; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +/* TransactionHistory */ +.search-Txn-input { + @apply w-full border-none cursor-text focus:outline-none bg-transparent placeholder:text-[#FFFFFF30] placeholder:font-normal text-[#ffffff] flex-1 text-[14px]; } - -.popup-text { - @apply text-white text-sm leading-[normal]; +.search-Txn-field { + @apply py-4 px-6 bg-[#FFFFFF05] rounded-full flex w-full; } - -/* WalletPopup */ - -.wallet-box { - @apply flex flex-col justify-center items-center gap-6 opacity-95 backdrop-blur-[2px] rounded-3xl w-[889px]; - /* background: linear-gradient(90deg, #704290 0.11%, #241b61 70.28%); */ +.v-line { + @apply w-[0.25px] h-2 bg-[#FFFFFF80] opacity-20; +} +.txn-card { + @apply flex justify-between gap-2 px-6 py-4 rounded-2xl; background: linear-gradient( - 178deg, - #241b61 1.71%, - #69448d 98.35%, - #69448d 98.35% + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% ); } - -.connect-wallet-box { - @apply flex-col gap-6 pl-10 pr-10; -} - -.wallet-grid { - background: rgba(255, 255, 255, 0.05); - @apply flex flex-col items-center gap-4 p-4 cursor-pointer rounded-2xl hover:bg-[#585287] px-8 py-4 w-full; +.txn-permission-card { + @apply flex justify-center items-center gap-2 px-4 py-2 rounded-lg h-8; + background: rgba(255, 255, 255, 0.06); } -.selected-wallet { - @apply opacity-100; +.txn-history-card { + @apply flex flex-col justify-center items-center gap-2 p-4 rounded-2xl; background: linear-gradient( - 180deg, - rgba(74, 162, 156, 0.4) 0%, - rgba(139, 61, 167, 0.4) 100% + 45deg, + rgb(255 255 255 / 3%) 0%, + rgb(153 153 153 / 25%) 100% ); } - -/* Landingpage */ -.powered-by { - color: white; - font-size: 16px; - font-weight: bold; - animation: scrollText 10s linear infinite; -} - -.powered-by-background { - @apply flex w-full grow-0 justify-center items-center gap-2.5 p-2.5 fixed bottom-0 opacity-100 h-12; - background: rgba(255, 255, 255, 0.05); -} -.text { - @apply text-white text-base not-italic font-light leading-7 tracking-[0.96px]; -} -.landingpage-background { - @apply flex flex-col min-h-screen; - background: linear-gradient(107deg, #1f184e 1.65%, #8b3da7 100%); -} -.landingpage-button { - @apply flex justify-center items-center gap-6 px-6 py-4 rounded-[100px] w-[226px] cursor-pointer; - background: linear-gradient(180deg, #4aa29c 0%, #8b3da7 100%); +.count-type-card { + @apply flex flex-col items-start gap-10 w-full px-6 py-4 rounded-2xl; + background: rgba(255, 255, 255, 0.02); } -@media (max-width: 640px) { - .landingpage-button { - @apply w-full sm:py-3 sm:px-6; - } -} - -@media (min-width: 641px) and (max-width: 1024px) { - .landingpage-button { - @apply lg:py-4 lg:px-8; - } -} - -.dots-flashing:after { - content: ' .'; - animation: dots 1s steps(5, end) infinite; -} - -@keyframes dots { - 0%, - 20% { - color: rgba(0, 0, 0, 0); - text-shadow: - 0.25em 0 0 rgba(0, 0, 0, 0), - 0.5em 0 0 rgba(0, 0, 0, 0); - } - 40% { - color: white; - text-shadow: - 0.25em 0 0 rgba(0, 0, 0, 0), - 0.5em 0 0 rgba(0, 0, 0, 0); - } - 60% { - text-shadow: - 0.25em 0 0 white, - 0.5em 0 0 rgba(0, 0, 0, 0); - } - 80%, - 100% { - text-shadow: - 0.25em 0 0 white, - 0.5em 0 0 white; - } -} - -.loader { - animation: rotate 1s infinite; - height: 50px; - width: 50px; -} - -.loader:before, -.loader:after { - border-radius: 50%; - content: ''; - display: block; - height: 20px; - width: 20px; -} -.loader:before { - animation: ball1 1s infinite; - background-color: #cb2025; - box-shadow: 30px 0 0 #f8b334; - margin-bottom: 10px; -} -.loader:after { - animation: ball2 1s infinite; - background-color: #00a096; - box-shadow: 30px 0 0 #97bf0d; -} - -@keyframes rotate { - 0% { - -webkit-transform: rotate(0deg) scale(0.8); - -moz-transform: rotate(0deg) scale(0.8); - } - 50% { - -webkit-transform: rotate(360deg) scale(1.2); - -moz-transform: rotate(360deg) scale(1.2); - } - 100% { - -webkit-transform: rotate(720deg) scale(0.8); - -moz-transform: rotate(720deg) scale(0.8); - } -} - -@keyframes ball1 { - 0% { - box-shadow: 30px 0 0 #f8b334; - } - 50% { - box-shadow: 0 0 0 #f8b334; - margin-bottom: 0; - -webkit-transform: translate(15px, 15px); - -moz-transform: translate(15px, 15px); - } - 100% { - box-shadow: 30px 0 0 #f8b334; - margin-bottom: 10px; - } -} - -@keyframes ball2 { - 0% { - box-shadow: 30px 0 0 #97bf0d; - } - 50% { - box-shadow: 0 0 0 #97bf0d; - margin-top: -20px; - -webkit-transform: translate(15px, 15px); - -moz-transform: translate(15px, 15px); - } - 100% { - box-shadow: 30px 0 0 #97bf0d; - margin-top: 0; - } -} - -.landingpage-container { - position: relative; - overflow: hidden; -} - -@media only screen and (min-width: 1024px) { - .landingpage-container { - position: absolute; - } - .landingpage-background { - } +.count-type-card-extend { + @apply flex flex-col items-start gap-4 w-full rounded-2xl; + background: rgba(255, 255, 255, 0.02); } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index e116c2908..677d42e0d 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,17 +1,34 @@ import './globals.css'; import type { Metadata } from 'next'; -import { Inter } from 'next/font/google'; - -import { Landingpage } from '@/components/LandingPage'; import { StoreProvider } from '@/store/StoreProvider'; -import SideBar from '@/components/SideBar'; import SnackBar from '@/components/SnackBar'; +import Script from 'next/script'; +import { OpenGraph } from 'next/dist/lib/metadata/types/opengraph-types'; +import dynamic from 'next/dynamic'; +import Loading from '@/components/main-layout/Loading'; + +const TRACKING_ID = 'G-RTXGXXDNNS'; -const inter = Inter({ subsets: ['latin'] }); +const FixedLayout = dynamic( + () => import('@/components/main-layout/FixedLayout'), + { ssr: false, loading: () => } +); + +const openGraph: OpenGraph = { + title: 'Interchain interface', + description: + 'Resolute is an advanced spacecraft designed to travel through the multiverse, connecting Cosmos sovereign chains.', + url: 'https://resolute.vitwit.com', + type: 'website', +}; export const metadata: Metadata = { title: 'Resolute', - description: 'resolute', + description: + 'Interchain interface, Resolute is an advanced spacecraft designed to travel through the multiverse, connecting Cosmos sovereign chains.', + keywords: + 'resolute, interchain interface, cosmos, osmosis, regen, akash, celestia, dymension, authz, feegrant, groups, staking, send, ibc send, multisig', + openGraph, }; export default function RootLayout({ @@ -21,17 +38,25 @@ export default function RootLayout({ }) { return ( - + {
    - - {children} - + {children}
    } + ); diff --git a/frontend/src/components/ChainNotFound.tsx b/frontend/src/components/ChainNotFound.tsx index 62864d1f9..4f36b6ed6 100644 --- a/frontend/src/components/ChainNotFound.tsx +++ b/frontend/src/components/ChainNotFound.tsx @@ -3,7 +3,7 @@ import messages from '@/utils/messages.json'; const ChainNotFound = () => { return ( -
    +
    {messages.chainNotFound}
    ); diff --git a/frontend/src/components/CommonCopy.tsx b/frontend/src/components/CommonCopy.tsx index 0f51ac55f..ceb995c38 100644 --- a/frontend/src/components/CommonCopy.tsx +++ b/frontend/src/components/CommonCopy.tsx @@ -4,7 +4,15 @@ import Image from 'next/image'; import { useAppDispatch } from '@/custom-hooks/StateHooks'; import { setError } from '@/store/features/common/commonSlice'; -const CommonCopy = ({ message, style }: { message: string; style: string }) => { +const CommonCopy = ({ + message, + style, + plainIcon, +}: { + message: string; + style: string; + plainIcon?: boolean; +}) => { const dispatch = useAppDispatch(); return (
    @@ -21,7 +29,7 @@ const CommonCopy = ({ message, style }: { message: string; style: string }) => { ); e.stopPropagation(); }} - src="/copy.svg" + src={plainIcon ? '/copy-icon-plain.svg' : '/copy.svg'} width={24} height={24} alt="copy" diff --git a/frontend/src/components/CopyToClipboard.tsx b/frontend/src/components/CopyToClipboard.tsx deleted file mode 100644 index 254d135db..000000000 --- a/frontend/src/components/CopyToClipboard.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Chip } from '@mui/material'; -import React from 'react'; -import { copyToClipboard } from '@/utils/copyToClipboard'; -import Image from 'next/image'; - -export const CopyToClipboard = ({ - message, - formattedMessage, -}: { - message: string; - formattedMessage: string; -}) => { - return ( - - } - onDelete={() => { - copyToClipboard(message); - }} - /> - ); -}; diff --git a/frontend/src/components/CustomButton.tsx b/frontend/src/components/CustomButton.tsx index 9a2c8d8d0..5da7f3b37 100644 --- a/frontend/src/components/CustomButton.tsx +++ b/frontend/src/components/CustomButton.tsx @@ -1,54 +1,41 @@ -import { CircularProgress } from '@mui/material'; +import { isMetaMaskWallet } from '@/utils/localStorage'; +import { CircularProgress, Tooltip } from '@mui/material'; import React from 'react'; const CustomSubmitButton = ({ pendingStatus, - circularProgressSize, - buttonStyle, - buttonContent, + isIBC, }: { pendingStatus: boolean; - circularProgressSize: number; - buttonStyle: string; - buttonContent: string; + isIBC?: boolean; }) => { + const isMetaMask = isMetaMaskWallet(); + return (
    - + + +
    ); }; -interface propsToAccept { - pendingStatus: boolean; - circularProgressSize: number; - buttonStyle: string; - buttonContent: string; - onClick: () => void; -} - -export const CustomButton: React.FC = ({ - pendingStatus, - circularProgressSize, - buttonStyle, - buttonContent, - onClick, -}: propsToAccept) => { - return ( - - ); -}; - export default CustomSubmitButton; diff --git a/frontend/src/components/DialogAddNetwork.tsx b/frontend/src/components/DialogAddNetwork.tsx deleted file mode 100644 index 834d3f980..000000000 --- a/frontend/src/components/DialogAddNetwork.tsx +++ /dev/null @@ -1,327 +0,0 @@ -'use client'; -import { Dialog, DialogContent, IconButton, Tooltip } from '@mui/material'; -import Image from 'next/image'; -import React, { ChangeEvent, useEffect, useState } from 'react'; -import ClearIcon from '@mui/icons-material/Clear'; -import networkConfigFormat from '@/utils/networkConfigSchema.json'; -import { ValidationError, validate } from 'jsonschema'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { get } from 'lodash'; -import { establishWalletConnection } from '@/store/features/wallet/walletSlice'; -import { ADD_NETWORK_TEMPLATE_URL } from '@/utils/constants'; -import { networks } from '@/utils/chainsInfo'; -import { getLocalNetworks, setLocalNetwork } from '@/utils/localStorage'; -import { TxStatus } from '@/types/enums'; -import { setError } from '@/store/features/common/commonSlice'; -import { CHAIN_ID_EXIST_ERROR, CHAIN_NAME_EXIST_ERROR } from '@/utils/errors'; -import { convertKeysToCamelCase } from '@/utils/util'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -const DialogAddNetwork = ({ - open, - handleClose, -}: { - open: boolean; - handleClose: () => void; -}) => { - const [requestNetwork, setRequestNetwork] = useState(false); - const [uploadedFileName, setUploadedFileName] = useState(''); - const [chainIDExist, setChainIDExist] = useState(false); - const [chainNameExist, setChainNameExist] = useState(false); - const [showErrors, setShowErrors] = useState(false); - const [validationErrors, setValidationErrors] = useState( - [] - ); - const [networkConfig, setNetworkConfig] = useState({}); - - const nameToChainIDs: Record = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - const connectWalletStatus = useAppSelector( - (state: RootState) => state.wallet.status - ); - const dispatch = useAppDispatch(); - - const handleFileChange = (e: ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) { - return; - } - const reader = new FileReader(); - reader.onload = (e) => { - const contents = e.target?.result as string; - onFileContents(contents); - setUploadedFileName(file.name); - }; - reader.onerror = (e) => { - alert(e); - }; - reader.readAsText(file); - e.target.value = ''; - }; - - const onFileContents = (content: string): void => { - try { - const parsedData = JSON.parse(content); - const res = validate(parsedData, networkConfigFormat); - setValidationErrors(res.errors); - if (!get(res, 'errors.length')) { - setChainNameExist( - chainNameExists(get(parsedData, 'config.chain_name')) - ); - setChainIDExist(chainIDExists(get(parsedData, 'config.chain_id'))); - setNetworkConfig(parsedData); - } else { - setNetworkConfig({}); - } - } catch (e) { - setNetworkConfig({}); - console.log(e); - } - }; - - const chainNameExists = (chainName: string) => { - const chainNamesList = Object.keys(nameToChainIDs); - if (chainNamesList.includes(chainName.toLowerCase())) { - return true; - } - return false; - }; - - const chainIDExists = (chainID: string) => { - const chainNamesList = Object.keys(nameToChainIDs); - for (const chain in chainNamesList) { - if ( - nameToChainIDs[chainNamesList[chain]].toLowerCase() === - chainID.toLowerCase() - ) { - return true; - } - } - return false; - }; - - const addNetwork = () => { - const chainID = get(networkConfig, 'config.chain_id'); - if (!chainIDExist && !chainNameExist && chainID) { - const networkConfigFormatted = convertKeysToCamelCase(networkConfig); - setLocalNetwork(networkConfigFormatted, chainID); - dispatch( - establishWalletConnection({ - walletName: 'keplr', - networks: [...networks, ...getLocalNetworks()], - }) - ); - } else { - setNetworkConfig({}); - setError({ - type: 'error', - message: 'Invalid JSON file', - }); - } - }; - - const handleAddNetworkType = (value: boolean) => { - setRequestNetwork(value); - }; - - useEffect(() => { - if (connectWalletStatus === TxStatus.IDLE) { - handleClose(); - } - }, [connectWalletStatus]); - - return ( - - -
    -
    -
    { - handleClose(); - }} - > - Close -
    -
    -
    -
    - Add Network -
    -
    -

    - Add Network -

    -
    -
    handleAddNetworkType(false)} - > -
    - {!requestNetwork ? ( -
    - ) : null} -
    -
    Local Network
    -
    -
    handleAddNetworkType(true)} - > -
    - {requestNetwork ? ( -
    - ) : null} -
    -
    Request Network
    -
    -
    - {requestNetwork ? ( - <> -
    - Coming soon... -
    - - ) : ( - <> -
    -
    { - document.getElementById('multisig_file')!.click(); - }} - > -
    - {uploadedFileName ? ( - <> -
    - {uploadedFileName}{' '} - - { - setUploadedFileName(''); - setChainIDExist(false); - setChainNameExist(false); - setShowErrors(false); - e.stopPropagation(); - }} - > - - - -
    - - ) : ( - <> - Upload file -
    Upload file here
    - - )} -
    - -
    - -
    - {uploadedFileName && validationErrors?.length ? ( -
    -
    -
    - Invalid json file -
    -
    - setShowErrors((showErrors) => !showErrors) - } - > - show more -
    -
    - {showErrors && - validationErrors?.map((item, index) => ( -
  • {item.stack}
  • - ))} -
    - ) : ( -
    - {chainNameExist ? ( -
  • - {CHAIN_NAME_EXIST_ERROR} -
  • - ) : ( - <> - )} - {chainIDExist ? ( -
  • - {CHAIN_ID_EXIST_ERROR} -
  • - ) : ( - <> - )} -
    - )} -
    - -
    - - )} -
    -
    -
    -
    -
    - ); -}; - -export default DialogAddNetwork; diff --git a/frontend/src/components/IBCSwapTxStatus.tsx b/frontend/src/components/IBCSwapTxStatus.tsx new file mode 100644 index 000000000..4ece6f32a --- /dev/null +++ b/frontend/src/components/IBCSwapTxStatus.tsx @@ -0,0 +1,203 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + resetTx, + resetTxDestSuccess, + setAmountIn, + setAmountOut, + setDestAsset, + setDestChain, + setSourceAsset, + setSourceChain, + setToAddress, +} from '@/store/features/swaps/swapsSlice'; +import { TxStatus } from '@/types/enums'; +import { + Alert, + AlertTitle, + CircularProgress, + IconButton, + Snackbar, +} from '@mui/material'; +import Link from 'next/link'; +import React, { useEffect, useState } from 'react'; +import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined'; +import CloseIcon from '@mui/icons-material/Close'; +import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded'; +import { cleanURL } from '@/utils/util'; + +const IBCSwapTxStatus = () => { + const dispatch = useAppDispatch(); + const [showTxSourceSuccess, setTxSourceSuccess] = useState(false); + const [showTxDestSuccess, setTxDestSuccess] = useState(false); + const txLoadRes = useAppSelector((state) => state.swaps.txStatus.status); + const txHash = useAppSelector((state) => state.swaps.txSuccess.txHash); + const explorerUrl = useAppSelector((state) => state.swaps.explorerEndpoint); + const txDestStatus = useAppSelector((state) => state.swaps.txDestSuccess); + + const resetIBCSwap = () => { + dispatch(setSourceChain(null)); + dispatch(setSourceAsset(null)); + dispatch(setDestChain(null)); + dispatch(setDestAsset(null)); + dispatch(setAmountIn('')); + dispatch(setAmountOut('')); + dispatch(setToAddress('')); + setTxSourceSuccess(false); + setTxDestSuccess(false); + dispatch(resetTx()); + dispatch(resetTxDestSuccess()); + }; + + useEffect(() => { + let timer: NodeJS.Timeout; + + if (showTxSourceSuccess && showTxDestSuccess) { + timer = setTimeout(() => { + resetIBCSwap(); + }, 2000); + } + + return () => clearTimeout(timer); + }, [showTxSourceSuccess, showTxDestSuccess]); + + useEffect(() => { + if (txHash?.length) { + setTxSourceSuccess(true); + } else { + setTxSourceSuccess(false); + } + }, [txHash]); + + useEffect(() => { + if (txDestStatus.status.length) { + setTxDestSuccess(true); + } else { + setTxDestSuccess(false); + } + }, [txDestStatus]); + + useEffect(() => { + dispatch(resetTx()); + dispatch(resetTxDestSuccess()); + }, []); + + return ( +
    + + , + }} + onClose={() => { + dispatch(resetTx()); + }} + severity="info" + sx={{ + width: '100%', + backgroundColor: '#09090a', + borderRadius: '8px', + border: '1px solid #ffffffD0', + }} + action={ + { + dispatch(resetTx()); + }} + sx={{ color: '#fff' }} + > + + + } + > + + Transaction Pending... + + {showTxSourceSuccess ? ( + <> + +
    + +
    + Transaction Broadcasted on Source Chain +
    +
    +
    + + View on explorer + + + ) : null} +
    +
    + + + + ), + }} + onClose={() => { + dispatch(resetTxDestSuccess()); + }} + severity="info" + sx={{ + width: '100%', + backgroundColor: '#09090a', + borderRadius: '8px', + border: '1px solid #ffffffD0', + }} + action={ + { + dispatch(resetTxDestSuccess()); + }} + sx={{ color: '#fff' }} + > + + + } + > + + {txDestStatus.msg || ''} + + + View on explorer + + + +
    + ); +}; + +export default IBCSwapTxStatus; diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx deleted file mode 100644 index 194f89750..000000000 --- a/frontend/src/components/LandingPage.tsx +++ /dev/null @@ -1,158 +0,0 @@ -'use client'; -import React, { useEffect, useState } from 'react'; -import { networks } from '../utils/chainsInfo'; -import Image from 'next/image'; -import { - getLocalNetworks, - getWalletName, - isConnected, - removeAllAuthTokens, -} from '../utils/localStorage'; -import { - establishWalletConnection, - unsetIsLoading, -} from '../store/features/wallet/walletSlice'; -import { RootState } from '../store/store'; -import { getAllTokensPrice } from '@/store/features/common/commonSlice'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import WalletPopup from './WalletPopup'; -import CustomParticles from './Particles'; -import Loading from './Loading'; - -export const Landingpage = ({ children }: { children: React.ReactNode }) => { - const dispatch = useAppDispatch(); - const connected = useAppSelector( - (state: RootState) => state.wallet.connected - ); - const isLoading = useAppSelector((state) => state.wallet.isLoading); - const [connectWalletDialogOpen, setConnectWalletDialogOpen] = - useState(false); - const handleClose = () => { - setConnectWalletDialogOpen( - (connectWalletDialogOpen) => !connectWalletDialogOpen - ); - }; - - const selectWallet = (walletName: string) => { - tryConnectWallet(walletName); - handleClose(); - }; - - const tryConnectWallet = (walletName: string) => { - dispatch( - establishWalletConnection({ - walletName, - networks: [...networks, ...getLocalNetworks()], - }) - ); - }; - - useEffect(() => { - const walletName = getWalletName(); - if (isConnected()) { - tryConnectWallet(walletName); - } else { - dispatch(unsetIsLoading()); - } - - const accountChangeListener = () => { - setTimeout(() => tryConnectWallet(walletName), 1000); - removeAllAuthTokens(); - window.location.reload(); - }; - - window.addEventListener( - `${walletName}_keystorechange`, - accountChangeListener - ); - - dispatch(getAllTokensPrice()); - - return () => { - window.removeEventListener( - `${walletName}_keystorechange`, - accountChangeListener - ); - }; - }, []); - - if (isLoading) { - return ; - } - - return connected ? ( - <>{children} - ) : ( -
    -
    - -
    - -
    -
    -
    -
    - - Vitwit-Logo -
    -
    -
    -
    -
    - Resolute -
    -
    -
    -
    - Interchain Interface -
    -
    - Resolute is an advanced spacecraft designed to travel - through the multiverse, connecting Cosmos sovereign - chains. -
    -
    -
    setConnectWalletDialogOpen(true)} - > -

    - Connect Wallet -

    -
    -
    -
    - - -
    - - landing page image -
    - -
    -
    -
    -
    Powered by Vitwit
    -
    -
    -
    -
    -
    - ); -}; diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx deleted file mode 100644 index 0f101b345..000000000 --- a/frontend/src/components/Loading.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -const Loading = () => { - return ( -
    -
    - -
    -
    - ); -}; - -export default Loading; diff --git a/frontend/src/components/MainTopNav.tsx b/frontend/src/components/MainTopNav.tsx deleted file mode 100644 index a1d8566fa..000000000 --- a/frontend/src/components/MainTopNav.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -const MainTopNav = ({ title }: { title: string }) => { - return ( -
    -

    {title}

    -
    - ); -}; - -export default MainTopNav; diff --git a/frontend/src/components/NewTxnMsg.tsx b/frontend/src/components/NewTxnMsg.tsx new file mode 100644 index 000000000..462d8aed3 --- /dev/null +++ b/frontend/src/components/NewTxnMsg.tsx @@ -0,0 +1,270 @@ +import { + DELEGATE_TYPE_URL, + DEPOSIT_TYPE_URL, + IBC_SEND_TYPE_URL, + MAP_TXN_TYPES, + MSG_AUTHZ_EXEC, + MSG_AUTHZ_GRANT, + MSG_AUTHZ_REVOKE, + MSG_GRANT_ALLOWANCE, + MSG_REVOKE_ALLOWANCE, + REDELEGATE_TYPE_URL, + SEND_TYPE_URL, + UNDELEGATE_TYPE_URL, + VOTE_OPTIONS, + VOTE_TYPE_URL, +} from '@/utils/constants'; +import { + getTypeURLName, + parseAmount, + shortenAddress, + shortenString, +} from '@/utils/util'; +import { get } from 'lodash'; +import React from 'react'; + +const NewTxnMsg = ({ + msgs, + currency, + failed, +}: { + msgs: NewMsg[]; + currency: Currency; + failed: boolean; +}) => { + const status = failed ? 'failed' : 'successfully'; + + if (!msgs.length) { + return null; + } + + const msgType = msgs[0]?.typeUrl || get(msgs, '[0][@type]', ''); + const txTypeText = msgs?.length + ? failed + ? 'while ' + MAP_TXN_TYPES[msgType]?.[1] + ' to' + : MAP_TXN_TYPES[msgType]?.[0] + ' to' + : ''; + return ( + <> + {msgs?.length ? ( +
    + {msgType === SEND_TYPE_URL ? ( +
    +
    + {parseAmount(msgs[0]?.amount, currency)}{' '} + {status} {txTypeText} +
    + +
    + + {shortenAddress(msgs[0]?.to_address || '', 15) || '-'} + +
    +
    + +
    + ) : null} + {msgType === DELEGATE_TYPE_URL ? ( +
    +
    + {parseAmount([msgs[0]?.amount], currency)}{' '} + {status} {txTypeText} +
    + +
    + + {shortenString(msgs[0]?.validator_address, 24) || '-'} + +
    +
    + +
    + ) : null} + {msgType === UNDELEGATE_TYPE_URL ? ( +
    +
    + {parseAmount([msgs[0]?.amount], currency)}{' '} + {status}{' '} + + {failed + ? 'while ' + MAP_TXN_TYPES[msgType][1] + ' from' + : MAP_TXN_TYPES[msgType][0] + 'from'} + +
    + +
    + + {shortenString(msgs[0]?.validator_address || '', 24) || '-'} + +
    +
    + +
    + ) : null} + {msgType === REDELEGATE_TYPE_URL ? ( +
    +
    + {parseAmount([msgs[0]?.amount], currency)}{' '} + {status}{' '} + + {failed + ? 'while ' + MAP_TXN_TYPES[msgType][1] + : MAP_TXN_TYPES[msgType][0]} + +
    + +
    + ) : null} + {msgType === VOTE_TYPE_URL ? ( +
    +
    + {status}{' '} + + {failed + ? 'while ' + MAP_TXN_TYPES[msgType][1] + : MAP_TXN_TYPES[msgType][0]} + + {VOTE_OPTIONS[get(msgs, '[0].option', 0) - 1]} + on + + proposal #{parseInt(get(msgs, '[0].proposal_id', ''))} + +
    + +
    + ) : null} + {msgType === DEPOSIT_TYPE_URL ? ( +
    +
    + {parseAmount(msgs[0]?.amount, currency)}{' '} + {status}{' '} + + {failed + ? 'while ' + MAP_TXN_TYPES[msgType][1] + : MAP_TXN_TYPES[msgType][0]} + + on proposal #{parseInt(msgs[0]?.proposal_id)} +
    + +
    + ) : null} + {msgType === IBC_SEND_TYPE_URL ? ( +
    +
    + {parseAmount([msgs[0]?.token], currency)}{' '} + {status} {txTypeText} +
    + +
    + + {shortenAddress(msgs[0]?.receiver || '', 15) || '-'} + +
    +
    + +
    + ) : null} + {msgType === MSG_AUTHZ_GRANT ? ( +
    +
    + {status} granted to{' '} +
    + +
    + + {shortenAddress(get(msgs, '[0].grantee', '') || '', 15) || + '-'} + +
    +
    + +
    + ) : null} + {msgType === MSG_AUTHZ_REVOKE ? ( +
    +
    + {status}{' '} +
    + +
    + + {' '} + {'revoked authz from '} + {shortenAddress(get(msgs, '[0].grantee', '') || '', 15) || + '-'} + +
    +
    + +
    + ) : null} + {msgType === MSG_AUTHZ_EXEC ? ( +
    +
    + {status} Executed{' '} +
    + +
    + + {get(msgs, '[0].msgs', []).map((m, mindex) => ( +
    + Authz {getTypeURLName(get(m, '@type'))} +
    + ))} +
    +
    +
    + +
    + ) : null} + {msgType === MSG_GRANT_ALLOWANCE ? ( +
    +
    + {status} granted allowance + to{' '} +
    + +
    + + {shortenAddress(get(msgs, '[0].grantee', '') || '', 15) || + '-'} + +
    +
    + +
    + ) : null} + {msgType === MSG_REVOKE_ALLOWANCE ? ( +
    +
    + {status} revoked allowance + from{' '} +
    + +
    + + {shortenAddress(get(msgs, '[0].grantee', '') || '', 15) || + '-'} + +
    +
    + +
    + ) : null} +
    + ) : null} + + ); +}; + +export default NewTxnMsg; + +const MoreMessages = ({ msgs }: { msgs: NewMsg[] }) => { + return ( + <> + {msgs.length > 1 ? ( + +{msgs.length - 1} + ) : null} + + ); +}; diff --git a/frontend/src/components/Particles.tsx b/frontend/src/components/Particles.tsx deleted file mode 100644 index 6cd3b1425..000000000 --- a/frontend/src/components/Particles.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useCallback } from 'react'; -import Particles from 'react-particles'; -import { Engine } from 'tsparticles-engine'; -import { loadSlim } from 'tsparticles-slim'; - -const CustomParticles = () => { - const particlesInit = useCallback(async (engine: Engine) => { - await loadSlim(engine); - }, []); - - return ( - - ); -}; - -export default CustomParticles; diff --git a/frontend/src/components/SelectNetwork.tsx b/frontend/src/components/SelectNetwork.tsx deleted file mode 100644 index 74a4c984b..000000000 --- a/frontend/src/components/SelectNetwork.tsx +++ /dev/null @@ -1,294 +0,0 @@ -'use client'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { - setError, - setSelectedNetwork, -} from '@/store/features/common/commonSlice'; -import { RootState } from '@/store/store'; -import { Avatar, Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; -import DialogAddNetwork from './DialogAddNetwork'; -import { resetConnectWalletStatus } from '@/store/features/wallet/walletSlice'; -import { allNetworksLink, changeNetworkRoute } from '@/utils/util'; -import { copyToClipboard } from '@/utils/copyToClipboard'; -import { ALL_NETWORKS_ICON, CHANGE_NETWORK_ICON } from '@/utils/constants'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -const SelectNetwork = ({ message }: { message?: string }) => { - const pathName = usePathname(); - const dispatch = useAppDispatch(); - - const [open, setOpen] = useState(false); - const [addNetworkDialogOpen, setAddNetworkDialogOpen] = - useState(false); - const [chainLogo, setChainLogo] = useState(ALL_NETWORKS_ICON); - const [walletAddress, setWalletAddress] = useState(''); - - const selectedNetwork = useAppSelector( - (state: RootState) => state.common.selectedNetwork - ); - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const nameToChainIDs = useAppSelector( - (state: RootState) => state.wallet.nameToChainIDs - ); - - const handleClose = () => { - setOpen(false); - }; - const handleCloseAddNetworkDialog = () => { - setAddNetworkDialogOpen(false); - }; - const openAddNetworkDialog = () => { - setAddNetworkDialogOpen(true); - }; - - useEffect(() => { - const pathParts = pathName.split('/') || []; - if (pathParts.length >= 3) { - dispatch(setSelectedNetwork({ chainName: pathParts[2].toLowerCase() })); - } else { - dispatch(setSelectedNetwork({ chainName: '' })); - } - }, [pathName]); - - useEffect(() => { - if (selectedNetwork.chainName) { - const chainID = nameToChainIDs[selectedNetwork.chainName]; - - setChainLogo(networks[chainID].network.logos.menu); - setWalletAddress(networks[chainID].walletInfo.bech32Address); - } else { - setChainLogo(ALL_NETWORKS_ICON); - setWalletAddress(''); - } - }, [selectedNetwork]); - - useEffect(() => { - if (message?.length) { - setOpen(true); - } - }, [message]); - - return ( -
    -
    setOpen(true)} - > - All Networks - {walletAddress ? ( -
    - {walletAddress} - { - copyToClipboard(walletAddress); - dispatch( - setError({ - type: 'success', - message: 'Copied', - }) - ); - e.stopPropagation(); - }} - src="/copy.svg" - width={24} - height={24} - alt="copy" - /> -
    - ) : ( - <> -
    - All Networks -
    - - )} -
    - Select Network -
    -
    - - -
    - ); -}; - -export default SelectNetwork; - -const DialogSelectNetwork = ({ - open, - handleClose, - selectedNetworkName, - openAddNetworkDialog, - message, -}: { - open: boolean; - handleClose: () => void; - selectedNetworkName: string; - openAddNetworkDialog: () => void; - message?: string; -}) => { - const networks = useAppSelector((state: RootState) => state.wallet.networks); - const chainIDs = Object.keys(networks); - const pathName = usePathname(); - const dispatch = useAppDispatch(); - const pathParts = pathName.split('/'); - return ( - handleClose()} - maxWidth="lg" - PaperProps={{ - sx: dialogBoxPaperPropStyles, - }} - > - -
    -
    -
    { - handleClose(); - }} - > - Close -
    -
    - {message?.length ?
    {message}
    : null} -
    -
    -

    - All Networks -

    -
    - -
    -
    - { - dispatch(setSelectedNetwork({ chainName: '' })); - }} - className={ - selectedNetworkName.length - ? 'network-item' - : 'network-item network-item-selected' - } - > -
    - All Networks -

    - - Select All Networks - -

    -
    - -
    -
    - {chainIDs.map((chainID, index) => ( - - ))} -
    -
    -
    -
    -
    - ); -}; - -const NetworkItem = ({ - chainName, - logo, - pathName, - selectedNetworkName, - handleClose, -}: { - chainName: string; - logo: string; - pathName: string; - selectedNetworkName: string; - handleClose: () => void; -}) => { - const dispatch = useAppDispatch(); - - const shortenName = (name: string, maxLength: number): string => - name.length > maxLength ? `${name.substring(0, maxLength)}...` : name; - - const isSelected = (): boolean => { - return selectedNetworkName.toLowerCase() === chainName.toLowerCase(); - }; - return ( - { - dispatch(setSelectedNetwork({ chainName: chainName.toLowerCase() })); - handleClose(); - }} - className={ - isSelected() ? 'network-item network-item-selected' : 'network-item' - } - > - -

    - - {shortenName(chainName, 15)} - -

    - - ); -}; diff --git a/frontend/src/components/SideBar.tsx b/frontend/src/components/SideBar.tsx deleted file mode 100644 index 1f555f191..000000000 --- a/frontend/src/components/SideBar.tsx +++ /dev/null @@ -1,127 +0,0 @@ -'use client'; -import Image from 'next/image'; -import React from 'react'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { getSelectedPartFromURL } from '../utils/util'; -import { useAppSelector } from '@/custom-hooks/StateHooks'; -import { RootState } from '@/store/store'; -import { tabLink } from '../utils/util'; -import { Tooltip } from '@mui/material'; -import TransactionSuccessPopup from './TransactionSuccessPopup'; - -import { - GITHUB_ISSUES_PAGE_LINK, - HELP_ICON, - REPORT_ICON, - SIDENAV_MENU_ITEMS, - TELEGRAM_LINK, - TWITTER_ICON, - TWITTER_LINK, -} from '@/utils/constants'; - -const SideBar = ({ children }: { children: React.ReactNode }) => { - const pathName = usePathname(); - const pathParts = pathName.split('/'); - const selectedPart = getSelectedPartFromURL(pathParts).toLowerCase(); - - return ( -
    - - -
    - - Resolute - -
    - {SIDENAV_MENU_ITEMS.map((item) => ( - - ))} -
    -
    - - -
    - Resolute -
    - -
    - - -
    - Resolute -
    - -
    - - -
    - Resolute -
    - -
    -
    -
    -
    {children}
    -
    - ); -}; - -export default SideBar; - -const MenuItem = ({ - pathName, - itemName, - icon, - activeIcon, - link, -}: { - pathName: string; - itemName: string; - icon: string; - activeIcon: string; - link: string; -}) => { - const path = pathName === 'overview' ? '/' : `/${pathName}`; - const selectedNetwork = useAppSelector( - (state: RootState) => state.common.selectedNetwork.chainName - ); - - return ( - - -
    -
    - {itemName} -
    -
    -
    - - ); -}; diff --git a/frontend/src/components/SnackBar.tsx b/frontend/src/components/SnackBar.tsx index 5ce2fbcc6..5bd6ac90d 100644 --- a/frontend/src/components/SnackBar.tsx +++ b/frontend/src/components/SnackBar.tsx @@ -1,9 +1,9 @@ -'use client' +'use client'; import { useAppSelector } from '@/custom-hooks/StateHooks'; import { RootState } from '@/store/store'; -import { ALERT_HIDE_DURATION } from '@/utils/constants'; -import { Alert, Snackbar } from '@mui/material'; +import { ALERT_HIDE_DURATION, ALERT_TYPE_MAP } from '@/utils/constants'; +import { Alert, AlertColor, Snackbar } from '@mui/material'; import React, { useEffect, useState } from 'react'; const SnackBar = () => { @@ -22,6 +22,7 @@ const SnackBar = () => { setSnackOpen(false); } }, [errState]); + return ( <> {errState?.message?.length > 0 && ( @@ -38,7 +39,7 @@ const SnackBar = () => { {errState.message} diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx deleted file mode 100644 index e319d0ff5..000000000 --- a/frontend/src/components/TopNav.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import Profile from '@/app/(routes)/(overview)/overview-components/Profile'; -import React from 'react'; -import SelectNetwork from './SelectNetwork'; - -const TopNav = ({ message }: { message?: string }) => { - return ( -
    - - -
    - ); -}; - -export default TopNav; diff --git a/frontend/src/components/TransactionSuccessPopup.tsx b/frontend/src/components/TransactionSuccessPopup.tsx deleted file mode 100644 index e045b0a67..000000000 --- a/frontend/src/components/TransactionSuccessPopup.tsx +++ /dev/null @@ -1,197 +0,0 @@ -'use client'; -import '@/app/txn.css'; - -import { Box, Dialog, DialogContent } from '@mui/material'; -import React, { useEffect, useMemo, useState } from 'react'; -import Image from 'next/image'; -import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { TXN_FAILED_ICON, TXN_SUCCESS_ICON } from '@/utils/constants'; -import { copyToClipboard } from '@/utils/copyToClipboard'; -import { setError } from '@/store/features/common/commonSlice'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; -import Link from 'next/link'; -import { getTxnURL } from '@/utils/util'; -import TxnMessage from './TxnMessage'; -import { parseBalance } from '@/utils/denom'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -const TransactionSuccessPopup = () => { - const tx = useAppSelector((state) => state.common.txSuccess.tx); - - const [isOpen, setIsOpen] = useState(false); - const dispatch = useAppDispatch(); - const { getChainInfo, getDenomInfo } = useGetChainInfo(); - const { explorerTxHashEndpoint = '' } = tx?.chainID - ? getChainInfo(tx.chainID) - : {}; - const { - decimals = 0, - displayDenom = '', - minimalDenom = '', - } = tx?.chainID ? getDenomInfo(tx?.chainID) : {}; - const currency = useMemo( - () => ({ - coinMinimalDenom: minimalDenom, - coinDecimals: decimals, - coinDenom: displayDenom, - }), - [minimalDenom, decimals, displayDenom] - ); - - const handleClose = () => { - setIsOpen(false); - }; - - useEffect(() => { - if (tx) { - setIsOpen(true); - } else { - setIsOpen(false); - } - }, [tx]); - - return isOpen ? ( - - - -
    -
    -
    - Transaction Successful -
    -
    - {tx?.code === 0 ? ( - - Transaction Successful ! - - ) : ( - Transaction Failed ! - )} -
    - -
    -
    - -
    - -
    -
    -
    -
    -
    Transaction Hash
    -
    -
    - - {tx?.transactionHash || '-'} - - { - copyToClipboard(tx?.transactionHash || '-'); - dispatch( - setError({ - type: 'success', - message: 'Copied', - }) - ); - e.stopPropagation(); - }} - src="/copy-icon-plain.svg" - width={24} - height={24} - alt="copy" - /> -
    -
    -
    -
    -
    Fees
    -
    - {tx?.fee?.[0] - ? parseBalance( - tx?.fee, - currency.coinDecimals, - currency.coinMinimalDenom - ) - : '-'}{' '} - {currency.coinDenom} -
    -
    -
    -
    Memo
    -
    - {tx?.memo || '-'} -
    -
    -
    -
    - - - View - -
    -
    -
    -
    -
    -
    - ) : null; -}; - -export default TransactionSuccessPopup; diff --git a/frontend/src/components/TxnMessage.tsx b/frontend/src/components/TxnMessage.tsx index 62407536f..6400e38cc 100644 --- a/frontend/src/components/TxnMessage.tsx +++ b/frontend/src/components/TxnMessage.tsx @@ -9,12 +9,12 @@ import { VOTE_OPTIONS, VOTE_TYPE_URL, } from '@/utils/constants'; -import { capitalizeFirstLetter, parseAmount } from '@/utils/util'; +import { + capitalizeFirstLetter, + parseAmount, + shortenAddress, +} from '@/utils/util'; import React from 'react'; -import Image from 'next/image'; -import { copyToClipboard } from '@/utils/copyToClipboard'; -import { useAppDispatch } from '@/custom-hooks/StateHooks'; -import { setError } from '@/store/features/common/commonSlice'; const TxnMessage = ({ msgs, @@ -25,7 +25,6 @@ const TxnMessage = ({ currency: Currency; failed: boolean; }) => { - const dispatch = useAppDispatch(); const status = failed ? 'failed' : 'successfully'; const msgType = msgs[0]?.typeUrl; @@ -47,30 +46,11 @@ const TxnMessage = ({
    - {msgs[0]?.value?.toAddress || '-'} + {shortenAddress(msgs[0]?.value?.toAddress || '', 15) || '-'} - { - copyToClipboard(msgs[0]?.value?.toAddress || '-'); - dispatch( - setError({ - type: 'success', - message: 'Copied', - }) - ); - e.stopPropagation(); - }} - src="/copy-icon-plain.svg" - width={24} - height={24} - alt="copy" - />
    - {msgs.length > 1 ? ( - +{msgs.length - 1} - ) : null} +
    ) : null} {msgs[0]?.typeUrl === DELEGATE_TYPE_URL ? ( @@ -82,27 +62,14 @@ const TxnMessage = ({
    - {msgs[0]?.value?.validatorAddress || '-'} + {shortenAddress( + msgs[0]?.value?.validatorAddress || '', + 15 + ) || '-'} - { - copyToClipboard(msgs[0]?.value?.validatorAddress || '-'); - dispatch( - setError({ - type: 'success', - message: 'Copied', - }) - ); - e.stopPropagation(); - }} - src="/copy-icon-plain.svg" - width={24} - height={24} - alt="copy" - />
    +
    ) : null} {msgs[0]?.typeUrl === UNDELEGATE_TYPE_URL ? ( @@ -119,27 +86,14 @@ const TxnMessage = ({
    - {msgs[0]?.value?.validatorAddress || '-'} + {shortenAddress( + msgs[0]?.value?.validatorAddress || '', + 15 + ) || '-'} - { - copyToClipboard(msgs[0]?.value?.validatorAddress || '-'); - dispatch( - setError({ - type: 'success', - message: 'Copied', - }) - ); - e.stopPropagation(); - }} - src="/copy-icon-plain.svg" - width={24} - height={24} - alt="copy" - />
    +
    ) : null} {msgs[0]?.typeUrl === REDELEGATE_TYPE_URL ? ( @@ -150,9 +104,10 @@ const TxnMessage = ({ {failed ? 'while ' + MAP_TXN_TYPES[msgs[0]?.typeUrl][1] - : MAP_TXN_TYPES[msgs[0]?.typeUrl][1]} + : MAP_TXN_TYPES[msgs[0]?.typeUrl][0]}
    +
    ) : null} {msgs[0]?.typeUrl === VOTE_TYPE_URL ? ( @@ -168,6 +123,7 @@ const TxnMessage = ({ on proposal #{parseInt(msgs[0]?.value.proposalId)}
    +
    ) : null} {msgs[0]?.typeUrl === DEPOSIT_TYPE_URL ? ( @@ -182,6 +138,7 @@ const TxnMessage = ({ on proposal #{parseInt(msgs[0]?.value.proposalId)}
    +
    ) : null} {msgs[0]?.typeUrl === IBC_SEND_TYPE_URL ? ( @@ -193,27 +150,11 @@ const TxnMessage = ({
    - {msgs[0]?.value?.receiver || '-'} + {shortenAddress(msgs[0]?.value?.receiver || '', 15) || '-'} - { - copyToClipboard(msgs[0]?.value?.receiver || '-'); - dispatch( - setError({ - type: 'success', - message: 'Copied', - }) - ); - e.stopPropagation(); - }} - src="/copy-icon-plain.svg" - width={24} - height={24} - alt="copy" - />
    +
    ) : null}
    @@ -223,3 +164,13 @@ const TxnMessage = ({ }; export default TxnMessage; + +const MoreMessages = ({ msgs }: { msgs: Msg[] }) => { + return ( + <> + {msgs.length > 1 ? ( + +{msgs.length - 1} + ) : null} + + ); +}; diff --git a/frontend/src/components/WalletPopup.tsx b/frontend/src/components/WalletPopup.tsx deleted file mode 100644 index 4bf602a57..000000000 --- a/frontend/src/components/WalletPopup.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { DialogContent, Dialog } from '@mui/material'; -import Image from 'next/image'; -import React, { useState } from 'react'; -import { supportedWallets } from '@/utils/contants'; -import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; - -const WalletPopup = ({ - isOpen, - onClose, - selectWallet, -}: { - isOpen: boolean; - onClose: () => void; - selectWallet: (walletName: string) => void; -}) => { - const [selectedWallet, setSelectedWallet] = useState(null); - - const handleWalletClick = (walletName: string) => { - setSelectedWallet(walletName); - selectWallet(walletName); // Pass the walletName directly - }; - - return ( - - -
    -
    - Close -
    -
    -
    -
    - Connect Wallet -
    -
    - {supportedWallets.map((wallet) => ( -
    - handleWalletClick(wallet.name.toLocaleLowerCase()) - } - key={wallet.name} - > -
    - {wallet.name} -
    -
    - {wallet.name} Wallet -
    -
    - ))} -
    -
    -
    -
    -
    -
    -
    - ); -}; - -export default WalletPopup; diff --git a/frontend/src/components/common/AuthzButton.tsx b/frontend/src/components/common/AuthzButton.tsx new file mode 100644 index 000000000..112b8e44d --- /dev/null +++ b/frontend/src/components/common/AuthzButton.tsx @@ -0,0 +1,44 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import React, { useState } from 'react'; +import Image from 'next/image'; +import { TOGGLE_OFF, TOGGLE_ON } from '@/constants/image-names'; +import useAuthzGrants from '@/custom-hooks/useAuthzGrants'; +import DialogAuthzGrants from './DialogAuthzGrants'; + +const AuthzButton = ({ disabled }: { disabled: boolean }) => { + const isAuthzEnabled = useAppSelector( + (state) => state.authz.authzModeEnabled + ); + + const { disableAuthzMode } = useAuthzGrants(); + + const [grantsDialogOpen, setGrantsDialogOpen] = useState(false); + const toggleDialogOpen = () => { + setGrantsDialogOpen((prev) => !prev); + }; + return ( + <> + + {grantsDialogOpen && ( + + )} + + ); +}; + +export default AuthzButton; diff --git a/frontend/src/components/common/ConnectWallet.tsx b/frontend/src/components/common/ConnectWallet.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/common/Copy.tsx b/frontend/src/components/common/Copy.tsx new file mode 100644 index 000000000..b1b2e4702 --- /dev/null +++ b/frontend/src/components/common/Copy.tsx @@ -0,0 +1,62 @@ +/** + * Copy component displays an icon that allows the user to copy content to the clipboard. + * @module Copy + */ + +import { copyToClipboard } from '@/utils/copyToClipboard'; +import { Tooltip } from '@mui/material'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; + +/** + * Copy component displays an icon that allows the user to copy content to the clipboard. + * @param {Object} props - The props object. + * @param {string} props.content - The content to be copied to the clipboard. + * @returns {React.ReactNode} - React element representing the Copy component. + */ + +const Copy = ({ + content, + height = 20, + width = 20, +}: { + content: string; + height?: number; + width?: number; +}) => { + const [copied, setCopied] = useState(false); + + const handleCopy = (e: React.MouseEvent) => { + copyToClipboard(content); + setCopied(true); + e.stopPropagation(); + e.preventDefault(); + }; + + useEffect(() => { + let timer: NodeJS.Timeout; + if (copied) { + timer = setTimeout(() => { + setCopied(false); + }, 2000); + } + return () => clearTimeout(timer); + }, [copied]); + + return ( +
    + + copy + +
    + ); +}; + +export default Copy; diff --git a/frontend/src/components/common/CustomButton.tsx b/frontend/src/components/common/CustomButton.tsx new file mode 100644 index 000000000..500ba337d --- /dev/null +++ b/frontend/src/components/common/CustomButton.tsx @@ -0,0 +1,53 @@ +/** + * CustomButton component represents a button with customizable text, styles, and loading state. + * @module CustomButton + */ + +import { CircularProgress } from '@mui/material'; +import React from 'react'; + +interface CustomButtonProps { + btnText: string; + btnStyles?: string; + btnLoading?: boolean; + btnDisabled?: boolean; + /* eslint-disable @typescript-eslint/no-explicit-any */ + btnOnClick?: any; + btnType?: 'submit' | 'button'; + isDelete?: boolean; + form?: string; +} + +const CustomButton = ({ + btnStyles, + btnText, + btnLoading, + btnDisabled, + btnOnClick, + btnType, + isDelete, + form, +}: CustomButtonProps) => { + return ( + + ); +}; + +export default CustomButton; diff --git a/frontend/src/components/common/CustomDialog.tsx b/frontend/src/components/common/CustomDialog.tsx new file mode 100644 index 000000000..87699414b --- /dev/null +++ b/frontend/src/components/common/CustomDialog.tsx @@ -0,0 +1,92 @@ +/** + * CustomDialog component displays a dialog with a custom layout. + * @module CustomDialog + */ + +import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import { Dialog, DialogContent } from '@mui/material'; +import React from 'react'; +import Image from 'next/image'; + +interface CustomDialogProps { + children: React.ReactNode; + open: boolean; + onClose: () => void; + title: string; + styles?: string; + description?: string; + img?: string; + showDivider?: boolean; +} + +/** + * CustomDialog component displays a dialog with a custom layout. + * @param {Object} props - The props object. + * @param {React.ReactNode} props.children - The content of the dialog. + * @param {boolean} props.open - Whether the dialog is open or closed. + * @param {function} props.onClose - Callback function to close the dialog. + * @param {string} props.title - The title of the dialog. + * @param {string} props.description - The description of the dialog. + * @param {string} [props.styles] - (Optional) Additional tailwindcss styles for the dialog container. + * @param {string} props.showDivider - (Optional) Whether to display a horizontal line after the title and description. + * @returns {React.ReactNode} - React element representing the CustomDialog component. + */ + +const CustomDialog = ({ + children, + onClose, + open, + img, + title, + styles, + description, + showDivider, +}: CustomDialogProps) => { + return ( + + +
    +
    + +
    +
    +
    + {img ? ( +
    + Network-logo +
    + ) : null} +
    {title}
    +
    +
    + {description} +
    + {showDivider ? ( +
    +
    +
    + ) : null} +
    +
    {children}
    +
    +
    +
    +
    + ); +}; + +export default CustomDialog; diff --git a/frontend/src/components/common/CustomLoader.tsx b/frontend/src/components/common/CustomLoader.tsx new file mode 100644 index 000000000..a24baa626 --- /dev/null +++ b/frontend/src/components/common/CustomLoader.tsx @@ -0,0 +1,30 @@ +import { CircularProgress } from '@mui/material'; +import React from 'react'; + +interface CustomLoaderProps { + size?: number; + loadingText?: string; + textStyles?: string; +} + +const SIZE = 24; + +const CustomLoader = ({ + size = SIZE, + loadingText, + textStyles, +}: CustomLoaderProps) => { + return ( +
    + + {loadingText ? ( + + {loadingText} + + + ) : null} +
    + ); +}; + +export default CustomLoader; diff --git a/frontend/src/components/common/DialogAuthzGrants.tsx b/frontend/src/components/common/DialogAuthzGrants.tsx new file mode 100644 index 000000000..e5596eb2d --- /dev/null +++ b/frontend/src/components/common/DialogAuthzGrants.tsx @@ -0,0 +1,158 @@ +import React, { useState } from 'react'; +import CustomDialog from './CustomDialog'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { Tooltip } from '@mui/material'; +import { capitalizeFirstLetter, convertToSpacedName } from '@/utils/util'; +import Image from 'next/image'; +import { DROP_DOWN_ICON_FILLED } from '@/constants/image-names'; +import { enableAuthzMode } from '@/store/features/authz/authzSlice'; +import { setAuthzMode } from '@/utils/localStorage'; +import useAuthzGrants, { ChainAuthz } from '@/custom-hooks/useAuthzGrants'; +import useInitAuthz from '@/custom-hooks/useInitAuthz'; +import { exitFeegrantMode } from '@/store/features/feegrant/feegrantSlice'; +import { getMsgNameFromAuthz } from '@/utils/authorizations'; + +interface DialogAuthzGrantsProps { + open: boolean; + onClose: () => void; +} + +const DialogAuthzGrants: React.FC = (props) => { + const { open, onClose } = props; + const dispatch = useAppDispatch(); + const { getCosmosAddress } = useGetChainInfo(); + const cosmosAddress = getCosmosAddress(); + + const [viewMore, setViewMore] = useState(false); + const [viewingGrant, setViewingGrant] = useState(''); + const grantsToMeLoading = useAppSelector( + (state) => state.authz.getGrantsToMeLoading > 0 + ); + + const { getInterChainGrants } = useAuthzGrants(); + const grants = getInterChainGrants(); + + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const chainIDs = Object.keys(nameToChainIDs).map( + (chainName) => nameToChainIDs[chainName] + ); + useInitAuthz({ chainIDs, shouldFetch: true }); + + return ( + +
    + {!grantsToMeLoading && !grants.length ? ( +
    +

    - No permissions found -

    +
    + ) : null} + + {grants.map((grant) => ( +
    +
    +
    {grant.cosmosAddress}
    +
    + + +
    +
    + {viewMore && viewingGrant === grant.cosmosAddress ? ( + + ) : null} +
    + ))} + + {grantsToMeLoading ? ( +
    +

    + Please wait, trying to fetch your permissions + +

    +
    + ) : null} +
    +
    + ); +}; + +export default DialogAuthzGrants; + +const AuthzGrants = ({ grants }: { grants: ChainAuthz[] }) => { + return ( +
    +

    Allowed Messages

    +
    + {grants.map((chainGrants) => ( + + ))} +
    +
    + ); +}; + +const ChainGrants = ({ chainAuthz }: { chainAuthz: ChainAuthz }) => { + const { chainID, grant } = chainAuthz; + const mgsName = getMsgNameFromAuthz(grant); + const { getChainInfo } = useGetChainInfo(); + const { chainLogo, chainName } = getChainInfo(chainID); + + return ( +
    +
    + + {chainID} + +
    {convertToSpacedName(mgsName)}
    +
    +
    + ); +}; diff --git a/frontend/src/components/common/DialogConfirmDelete.tsx b/frontend/src/components/common/DialogConfirmDelete.tsx new file mode 100644 index 000000000..4b7e2c0a6 --- /dev/null +++ b/frontend/src/components/common/DialogConfirmDelete.tsx @@ -0,0 +1,75 @@ +import CustomButton from '@/components/common/CustomButton'; +import { DELETE_ILLUSTRATION } from '@/constants/image-names'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const DialogConfirmDelete = ({ + open, + onClose, + onDelete, + title, + description, + loading, +}: { + open: boolean; + onClose: () => void; + onDelete: () => void; + title: string; + description: string; + loading: boolean; +}) => { + return ( + + +
    + +
    +
    + Delete +
    +
    {title}
    +
    {description}
    +
    +
    + +
    +
    +
    +
    + ); +}; + +export default DialogConfirmDelete; diff --git a/frontend/src/components/common/DialogFeegrants.tsx b/frontend/src/components/common/DialogFeegrants.tsx new file mode 100644 index 000000000..e0d76f569 --- /dev/null +++ b/frontend/src/components/common/DialogFeegrants.tsx @@ -0,0 +1,158 @@ +import useFeeGrants, { ChainAllowance } from '@/custom-hooks/useFeeGrants'; +import React, { useState } from 'react'; +import CustomDialog from './CustomDialog'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import useInitFeegrant from '@/custom-hooks/useInitFeegrant'; +import { getMsgNamesFromAllowance } from '@/utils/feegrant'; +import { Tooltip } from '@mui/material'; +import { capitalizeFirstLetter, convertToSpacedName } from '@/utils/util'; +import Image from 'next/image'; +import { DROP_DOWN_ICON_FILLED } from '@/constants/image-names'; +import { enableFeegrantMode } from '@/store/features/feegrant/feegrantSlice'; +import { exitAuthzMode } from '@/store/features/authz/authzSlice'; +import { setFeegrantMode } from '@/utils/localStorage'; + +interface DialogFeegrantsProps { + open: boolean; + onClose: () => void; +} + +const DialogFeegrants: React.FC = (props) => { + const { open, onClose } = props; + const dispatch = useAppDispatch(); + const { getCosmosAddress } = useGetChainInfo(); + const cosmosAddress = getCosmosAddress(); + + const [viewMore, setViewMore] = useState(false); + const grantsToMeLoading = useAppSelector( + (state) => state.feegrant.getGrantsToMeLoading > 0 + ); + + const { getInterChainGrants } = useFeeGrants(); + const grants = getInterChainGrants(); + + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const chainIDs = Object.keys(nameToChainIDs).map( + (chainName) => nameToChainIDs[chainName] + ); + useInitFeegrant({ chainIDs, shouldFetch: true }); + + const toggleViewMore = () => { + setViewMore((prev) => !prev); + }; + + return ( + +
    + {!grantsToMeLoading && !grants.length ? ( +
    +

    - No allowances found -

    +
    + ) : null} + + {grants.map((grant) => ( +
    +
    +
    {grant.cosmosAddress}
    +
    + + +
    +
    + {viewMore ? : null} +
    + ))} + + {grantsToMeLoading ? ( +
    +

    + Please wait, trying to fetch your allowances + +

    +
    + ) : null} +
    +
    + ); +}; + +export default DialogFeegrants; + +const FeegrantAllowances = ({ grants }: { grants: ChainAllowance[] }) => { + return ( +
    +

    Allowed Messages

    +
    + {grants.map((chainGrants) => ( + + ))} +
    +
    + ); +}; + +const ChainGrants = ({ + chainAllowance, +}: { + chainAllowance: ChainAllowance; +}) => { + const { chainID, grant } = chainAllowance; + const mgsNames = getMsgNamesFromAllowance(grant); + const { getChainInfo } = useGetChainInfo(); + const { chainLogo, chainName } = getChainInfo(chainID); + + return ( +
    + {mgsNames.map((msg) => ( +
    + + {chainID} + +
    {convertToSpacedName(msg)}
    +
    + ))} +
    + ); +}; diff --git a/frontend/src/components/common/DialogLoader.tsx b/frontend/src/components/common/DialogLoader.tsx new file mode 100644 index 000000000..386fa892f --- /dev/null +++ b/frontend/src/components/common/DialogLoader.tsx @@ -0,0 +1,39 @@ +import { CircularProgress, Dialog, DialogContent } from '@mui/material'; +import React from 'react'; + +const DialogLoader = ({ + open, + loadingText, +}: { + open: boolean; + loadingText?: string; +}) => { + return ( + + +
    + +
    + + {loadingText || 'Loading'} + + +
    +
    +
    +
    + ); +}; + +export default DialogLoader; diff --git a/frontend/src/components/common/EmptyScreen.tsx b/frontend/src/components/common/EmptyScreen.tsx new file mode 100644 index 000000000..e798c06ec --- /dev/null +++ b/frontend/src/components/common/EmptyScreen.tsx @@ -0,0 +1,75 @@ +import { EMPTY_ILLUSTRATION } from '@/constants/image-names'; +import { CircularProgress } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const defaultImageWidth = 914; +const defaultImageHeight = 480; + +interface EmptyScreenProps { + title: string; + description: string; + width?: number; + height?: number; + hasActionBtn?: boolean; + btnText?: string; + btnStyles?: string; + btnLoading?: boolean; + btnDisabled?: boolean; + /* eslint-disable @typescript-eslint/no-explicit-any */ + btnOnClick?: any; + bgImage?: string; +} + +const EmptyScreen = ({ + title, + description, + width = defaultImageWidth, + height = defaultImageHeight, + hasActionBtn, + btnStyles, + btnText, + btnLoading, + btnDisabled, + btnOnClick, + bgImage = EMPTY_ILLUSTRATION, +}: EmptyScreenProps) => { + return ( +
    +
    +
    + +
    {title}
    +
    +
    {description}
    +
    + {hasActionBtn ? ( + + ) : null} +
    + ); +}; + +export default EmptyScreen; diff --git a/frontend/src/components/common/Error.tsx b/frontend/src/components/common/Error.tsx new file mode 100644 index 000000000..544b7af94 --- /dev/null +++ b/frontend/src/components/common/Error.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const Error = () => { + return ( +
    +
    {'Something went wrong :('}
    +
    + ); +}; + +export default Error; diff --git a/frontend/src/components/common/FeegrantButton.tsx b/frontend/src/components/common/FeegrantButton.tsx new file mode 100644 index 000000000..6f2c1304e --- /dev/null +++ b/frontend/src/components/common/FeegrantButton.tsx @@ -0,0 +1,43 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useFeeGrants from '@/custom-hooks/useFeeGrants'; +import React, { useState } from 'react'; +import DialogFeegrants from './DialogFeegrants'; +import Image from 'next/image'; +import { TOGGLE_OFF, TOGGLE_ON } from '@/constants/image-names'; + +const FeegrantButton = () => { + const isFeegrantEnabled = useAppSelector( + (state) => state.feegrant.feegrantModeEnabled + ); + + const { disableFeegrantMode } = useFeeGrants(); + + const [grantsDialogOpen, setGrantsDialogOpen] = useState(false); + const toggleDialogOpen = () => { + setGrantsDialogOpen((prev) => !prev); + }; + return ( + <> + + {grantsDialogOpen && ( + + )} + + ); +}; + +export default FeegrantButton; diff --git a/frontend/src/components/common/Footer.tsx b/frontend/src/components/common/Footer.tsx new file mode 100644 index 000000000..79e36fbfe --- /dev/null +++ b/frontend/src/components/common/Footer.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import Image from 'next/image'; + +const Footer = () => { + return ( +
    +
    + {/* resolute-logo */} +

    RESOLUTE

    +

    Powered by Vitwit

    +
    + +
    + Vitwit is a leading Cosmos dev agency and validator company. Proudly + serving as one of the core contributors to the Cosmos SDK. +
    + +
    + ); +}; + +export default Footer; diff --git a/frontend/src/components/common/LetterAvatar.tsx b/frontend/src/components/common/LetterAvatar.tsx new file mode 100644 index 000000000..c02995fb6 --- /dev/null +++ b/frontend/src/components/common/LetterAvatar.tsx @@ -0,0 +1,61 @@ +import { Avatar } from '@mui/material'; +import React from 'react'; + +function stringToColor(string: string) { + let hash = 0; + let i; + + for (i = 0; i < string.length; i += 1) { + hash = string.charCodeAt(i) + ((hash << 5) - hash); + } + + let color = '#'; + + for (i = 0; i < 3; i += 1) { + const value = (hash >> (i * 8)) & 0xff; + color += `00${value.toString(16)}`.slice(-2); + } + + return color; +} + +function stringAvatar(name: string, width: string, height: string) { + const spaceIndex = name.indexOf(' '); + const firstName = spaceIndex !== -1 ? name.split(' ')[0] : name; + + const firstInitial = firstName[0].toLowerCase(); + + const secondInitial = + spaceIndex !== -1 && name.split(' ')[1] + ? name.split(' ')[1][0].toLowerCase() + : ''; + + return { + sx: { + bgcolor: stringToColor(name), + width, + height, + fontSize: '16px', + color: 'black', + fontWeight: 700, + fontStyle: 'italic', + }, + children: `${firstInitial}${secondInitial}`, + }; +} + +const LetterAvatar = ({ + name, + height = '24px', + width = '24px', +}: { + name: string; + width?: string; + height?: string; +}) => { + return name?.length ? ( + + ) : null; +}; + +export default LetterAvatar; diff --git a/frontend/src/components/common/NoData.tsx b/frontend/src/components/common/NoData.tsx new file mode 100644 index 000000000..7cd736876 --- /dev/null +++ b/frontend/src/components/common/NoData.tsx @@ -0,0 +1,27 @@ +import { NO_DATA_ILLUSTRATION } from '@/constants/image-names'; +import Image from 'next/image'; +import React from 'react'; + +const NoData = ({ + height, + width, + message, +}: { + width: number; + height: number; + message: string; +}) => { + return ( +
    + No Data +
    {message}
    +
    + ); +}; + +export default NoData; diff --git a/frontend/src/components/common/NoOptions.tsx b/frontend/src/components/common/NoOptions.tsx new file mode 100644 index 000000000..e9e2b4ca8 --- /dev/null +++ b/frontend/src/components/common/NoOptions.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const NoOptions = ({ text }: { text: string }) => { + return
    {text}
    ; +}; + +export default NoOptions; diff --git a/frontend/src/components/common/NumberFormat.tsx b/frontend/src/components/common/NumberFormat.tsx new file mode 100644 index 000000000..578f36915 --- /dev/null +++ b/frontend/src/components/common/NumberFormat.tsx @@ -0,0 +1,33 @@ + +import { formatNumber } from '@/utils/util' +import React from 'react' + +function NumberFormat({ value, type, token = '', cls }: { value: string, type: string, token?: string, cls: string }) { + let parsedAmount = '0'; + let formattedToken = token; + + // Remove commas and split the value by space + const [numberPart, possibleToken] = value.replace(/,/g, "").split(' '); + + // Parse the number and set the token if provided + const numberValue = Number(numberPart); + if (possibleToken) formattedToken = possibleToken; + + // Determine the parsedAmount + if (numberValue === 0) { + parsedAmount = '0'; + } else if (numberValue < 0.01) { + parsedAmount = '< 0.01'; + } else { + parsedAmount = formatNumber(numberValue); + } + + return ( + + {type === 'token' ? `${parsedAmount} ${formattedToken}` : `${parsedAmount === '< 0.01'? '< $0.01': '$'+parsedAmount}`} + + ); +} + + +export default NumberFormat \ No newline at end of file diff --git a/frontend/src/components/common/PageHeader.tsx b/frontend/src/components/common/PageHeader.tsx new file mode 100644 index 000000000..19018c246 --- /dev/null +++ b/frontend/src/components/common/PageHeader.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const PageHeader = ({ + title, + description, +}: { + title: string; + description: string; +}) => { + return ( +
    +
    +
    {title}
    +
    +
    {description}
    +
    +
    +
    +
    + ); +}; + +export default PageHeader; diff --git a/frontend/src/components/common/PageLoading.tsx b/frontend/src/components/common/PageLoading.tsx new file mode 100644 index 000000000..f25cc34f0 --- /dev/null +++ b/frontend/src/components/common/PageLoading.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { CircularProgress } from '@mui/material'; + +const PageLoading = () => { + return ( +
    +
    + +
    +
    + ); +}; + +export default PageLoading; diff --git a/frontend/src/components/common/SectionHeader.tsx b/frontend/src/components/common/SectionHeader.tsx new file mode 100644 index 000000000..1d1ad2c9e --- /dev/null +++ b/frontend/src/components/common/SectionHeader.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const SectionHeader = ({ + title, + description, +}: { + title: string; + description: string; +}) => { + return ( +
    +
    +
    {title}
    +
    {description}
    +
    +
    +
    + ); +}; + +export default SectionHeader; diff --git a/frontend/src/components/common/ToggleSwitch.tsx b/frontend/src/components/common/ToggleSwitch.tsx new file mode 100644 index 000000000..44302b41a --- /dev/null +++ b/frontend/src/components/common/ToggleSwitch.tsx @@ -0,0 +1,33 @@ +import { TOGGLE_OFF, TOGGLE_ON } from '@/constants/image-names'; +import Image from 'next/image'; +import React from 'react'; + +interface ToggleSwitchProps { + checked: boolean; + onChange: (checked: boolean) => void; + text?: string; + width?: number; + height?: number; +} + +const ToggleSwitch = (props: ToggleSwitchProps) => { + const { checked, onChange, text, height = 14.8, width = 20 } = props; + + return ( + + ); +}; + +export default ToggleSwitch; diff --git a/frontend/src/components/illustrations/NoAssets.tsx b/frontend/src/components/illustrations/NoAssets.tsx index a98318949..2237bb323 100644 --- a/frontend/src/components/illustrations/NoAssets.tsx +++ b/frontend/src/components/illustrations/NoAssets.tsx @@ -4,17 +4,15 @@ import messages from '@/utils/messages.json'; const NoAssets = () => { return ( -
    +
    no assets -
    - {messages.noAssets} -
    +
    {messages.noAssets}
    ); }; diff --git a/frontend/src/components/illustrations/NoTransactions.tsx b/frontend/src/components/illustrations/NoTransactions.tsx index 71e0d2c7c..1b1ab1a3e 100644 --- a/frontend/src/components/illustrations/NoTransactions.tsx +++ b/frontend/src/components/illustrations/NoTransactions.tsx @@ -12,7 +12,7 @@ const NoTransactions = () => { height={200} alt="no transactions" /> -
    +
    {messages.noTransactions}
    diff --git a/frontend/src/components/illustrations/NotSupported.tsx b/frontend/src/components/illustrations/NotSupported.tsx new file mode 100644 index 000000000..1679d8fed --- /dev/null +++ b/frontend/src/components/illustrations/NotSupported.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Image from 'next/image'; + +const NotSupported = ({ feature }: { feature: string }) => { + return ( +
    + no assets +
    + {feature} is not supported in authz mode yet +
    +
    + ); +}; + +export default NotSupported; diff --git a/frontend/src/components/illustrations/WithoutConnectionIllustration.tsx b/frontend/src/components/illustrations/WithoutConnectionIllustration.tsx new file mode 100644 index 000000000..808fad7fa --- /dev/null +++ b/frontend/src/components/illustrations/WithoutConnectionIllustration.tsx @@ -0,0 +1,43 @@ +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import Image from 'next/image'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; + +const WithoutConnectionIllustration = () => { + const dispatch = useAppDispatch(); + const connectWallet = () => { + dispatch(setConnectWalletOpen(true)); + }; + + return ( +
    + {/*
    +
    Staking
    +
    + Connect your wallet now to access all the modules on resolute{' '} +
    +
    +
    */} +
    +
    + Dashboard-Image +
    +

    Connect your Wallet

    +

    + Connect your wallet to access your account on Resolute +

    +
    +
    + +
    +
    + ); +}; + +export default WithoutConnectionIllustration; diff --git a/frontend/src/components/illustrations/withConnectionIllustration.tsx b/frontend/src/components/illustrations/withConnectionIllustration.tsx new file mode 100644 index 000000000..c18b23a74 --- /dev/null +++ b/frontend/src/components/illustrations/withConnectionIllustration.tsx @@ -0,0 +1,24 @@ +import Image from 'next/image'; + +const WithConnectionIllustration = ({ message }: { message: string }) => { + return ( +
    +
    + Illustration +

    {message}

    + {/*
    +

    {message}

    +
    */} +
    +
    + ); +}; + +export default WithConnectionIllustration; diff --git a/frontend/src/components/main-layout/AuthzGrantsAlert.tsx b/frontend/src/components/main-layout/AuthzGrantsAlert.tsx new file mode 100644 index 000000000..d7abfcf96 --- /dev/null +++ b/frontend/src/components/main-layout/AuthzGrantsAlert.tsx @@ -0,0 +1,165 @@ +import { CANCEL_ICON_SOLID } from '@/constants/image-names'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useAuthzGrants from '@/custom-hooks/useAuthzGrants'; +import { setAuthzAlert } from '@/store/features/authz/authzSlice'; +import { getAuthzAlertData, setAuthzAlertData } from '@/utils/localStorage'; +import Image from 'next/image'; +import Link from 'next/link'; +import React, { useState } from 'react'; +import { Dialog, DialogContent, Checkbox } from '@mui/material'; +import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import CustomButton from '../common/CustomButton'; + +const AuthzGrantsAlert = () => { + const { getSendAuthzGrants } = useAuthzGrants(); + const [dialogOpen, setDialogOpen] = useState(false); + + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const chainIDs = Object.keys(nameToChainIDs).map( + (chainName) => nameToChainIDs[chainName] + ); + const sendGrantsData = getSendAuthzGrants(chainIDs); + const dispatch = useAppDispatch(); + + const showAuthzGrantsAlert = useAppSelector( + (state) => state.authz.authzAlert.display + ); + + const handleCloseAlert = () => { + setDialogOpen(true); + }; + + const handleDialogClose = (closePermanently: boolean) => { + setDialogOpen(false); + dispatch(setAuthzAlert(false)); + if (closePermanently) setAuthzAlertData(false); + }; + + if ( + showAuthzGrantsAlert && + (sendGrantsData.ibcTransfer > 0 || sendGrantsData.send > 0) && + getAuthzAlertData() && + !isAuthzMode + ) + return ( +
    +
    +
    + info-icon +

    Important

    {' '} +

    + You have granted + + {sendGrantsData.send > 0 ? ' Send' : ''} + {sendGrantsData.send > 0 && sendGrantsData.ibcTransfer > 0 + ? ',' + : ''} + {sendGrantsData.ibcTransfer > 0 ? ' IBC Transfer' : ''} + {' '} + access to{' '} + + {' '} + {sendGrantsData.send > sendGrantsData.ibcTransfer + ? sendGrantsData.send + : sendGrantsData.ibcTransfer} + {' '} + {sendGrantsData.send > 1 || sendGrantsData.ibcTransfer > 1 + ? 'accounts.' + : 'account.'}{' '} + + Click here + {' '} + to review and Revoke the access if it's not required +

    +
    + +
    + {dialogOpen && ( + setDialogOpen(false)} + /> + )} +
    + ); + + return null; +}; + +export default AuthzGrantsAlert; + +const DialogCloseAlert = ({ + onClose, + open, + onCloseDialog, +}: { + onClose: (closePermanently: boolean) => void; + open: boolean; + onCloseDialog: () => void; +}) => { + const [dontShowAgain, setDontShowAgain] = useState(false); + + const handleClose = () => { + onClose(dontShowAgain); + }; + + return ( + + +
    +
    + +
    + +
    +
    +
    +
    +
    + ); +}; diff --git a/frontend/src/components/main-layout/ConnectWallet.tsx b/frontend/src/components/main-layout/ConnectWallet.tsx new file mode 100644 index 000000000..01b641b20 --- /dev/null +++ b/frontend/src/components/main-layout/ConnectWallet.tsx @@ -0,0 +1,138 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + establishMetamaskConnection, + establishWalletConnection, + setConnectWalletOpen, + setIsLoading, +} from '@/store/features/wallet/walletSlice'; +import { networks } from '@/utils/chainsInfo'; +import { NotSupportedMetamaskChainIds, SUPPORTED_WALLETS } from '@/utils/constants'; +import { getLocalNetworks } from '@/utils/localStorage'; +import { + connectSnap, + experimentalSuggestChain, + getSnap, +} from '@leapwallet/cosmos-snap-provider'; +import { Dialog, DialogContent, Slide, SlideProps } from '@mui/material'; +import Image from 'next/image'; +import React, { forwardRef } from 'react'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const Transition = forwardRef(function Transition( + props: SlideProps & { children: React.ReactElement }, + ref: React.Ref +) { + return ; +}); + +const ConnectWallet = () => { + const dispatch = useAppDispatch(); + const open = useAppSelector((state) => state.wallet.connectWalletOpen); + const connectWalletClose = () => { + dispatch(setConnectWalletOpen(false)); + }; + + const selectWallet = (walletName: string) => { + tryConnectWallet(walletName.toLowerCase()); + connectWalletClose(); + }; + + const tryConnectWallet = async (walletName: string) => { + if (walletName === 'metamask') { + dispatch(setIsLoading()); + try { + const snapInstalled = await getSnap(); + if (!snapInstalled) { + await connectSnap(); // Initiates installation if not already present + } + + for (let i = 0; i < networks.length; i++) { + const chainId: string = networks[i].config.chainId; + if (NotSupportedMetamaskChainIds.indexOf(chainId) <= -1) { + try { + await experimentalSuggestChain(networks[i].config, { + force: false, + }); + dispatch( + establishMetamaskConnection({ + walletName, + network: networks[i], + }) + ); + } catch (error) { + console.log('Error while connecting ', chainId); + } + } + } + } catch (error) { + console.log('trying to connect wallet ', error); + } + } else { + dispatch( + establishWalletConnection({ + walletName, + networks: [...networks, ...getLocalNetworks()], + }) + ); + } + }; + + return ( + + +
    +
    + +
    +
    +
    Connect Wallet
    +
    + Connect Your Wallet to interact with resolute +
    +
    +
    + {SUPPORTED_WALLETS.map((wallet, index) => ( +
    { + selectWallet(wallet.name); + }} + key={index} + > + {wallet.name} +

    + {wallet.name === "Metamask" ? "Metamask Snap":wallet.name}

    +
    + ))} +
    +
    +
    +
    +
    + ); +}; + +export default ConnectWallet; diff --git a/frontend/src/components/main-layout/DialogConfirmExitSession.tsx b/frontend/src/components/main-layout/DialogConfirmExitSession.tsx new file mode 100644 index 000000000..137c0a1fd --- /dev/null +++ b/frontend/src/components/main-layout/DialogConfirmExitSession.tsx @@ -0,0 +1,68 @@ +import CustomButton from '@/components/common/CustomButton'; +import { LOGOUT_ICON } from '@/constants/image-names'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const DialogConfirmExitSession = ({ + open, + onClose, + onConfirm, +}: { + open: boolean; + onClose: () => void; + onConfirm: () => void; +}) => { + return ( + + +
    + +
    +
    + Verify Ownership +
    +
    Logout
    +
    + Are you sure you want to logout? +
    +
    +
    + +
    +
    +
    +
    + ); +}; + +export default DialogConfirmExitSession; diff --git a/frontend/src/components/main-layout/DynamicSection.tsx b/frontend/src/components/main-layout/DynamicSection.tsx new file mode 100644 index 000000000..70c71a3cb --- /dev/null +++ b/frontend/src/components/main-layout/DynamicSection.tsx @@ -0,0 +1,60 @@ +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { usePathname } from 'next/navigation'; +import React from 'react'; + +const DynamicSection = ({ children }: { children: React.ReactNode }) => { + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork.chainName + ); + + return selectedNetwork ? ( + {children} + ) : ( + <>{children} + ); +}; + +export default DynamicSection; + +const NetworkSupport = ({ children }: { children: React.ReactNode }) => { + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork.chainName + ); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const chainID = nameToChainIDs?.[selectedNetwork.toLowerCase()]; + + return chainID ? ( + {children} + ) : ( +
    Network not supported
    + ); +}; + + +// TODO: Implement module not supported screen +const Module = ({ + children, + chainID, +}: { + children: React.ReactNode; + chainID: string; +}) => { + const pathName = usePathname().toLowerCase(); + const { getChainInfo } = useGetChainInfo(); + const { enableModules } = getChainInfo(chainID); + + const renderModuleContent = () => { + if (pathName.includes('feegrant') && !enableModules.feegrant) { + return
    Feegrant is not supported
    ; + } + + if (pathName.includes('authz') && !enableModules.authz) { + return
    Authz is not supported
    ; + } + + return <>{children}; + }; + + return renderModuleContent(); +}; diff --git a/frontend/src/components/main-layout/ExitSession.tsx b/frontend/src/components/main-layout/ExitSession.tsx new file mode 100644 index 000000000..07a23eb9d --- /dev/null +++ b/frontend/src/components/main-layout/ExitSession.tsx @@ -0,0 +1,78 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + resetError, + resetTxAndHash, +} from '@/store/features/common/commonSlice'; +import { resetWallet } from '@/store/features/wallet/walletSlice'; +import { resetState as bankReset } from '@/store/features/bank/bankSlice'; +import { resetState as rewardsReset } from '@/store/features/distribution/distributionSlice'; +import { resetCompleteState as stakingReset } from '@/store/features/staking/stakeSlice'; +import { resetState as authzReset } from '@/store/features/authz/authzSlice'; +import { resetState as feegrantReset } from '@/store/features/feegrant/feegrantSlice'; +import Image from 'next/image'; +import React, { useState } from 'react'; +import useAuthzGrants from '@/custom-hooks/useAuthzGrants'; +import useFeeGrants from '@/custom-hooks/useFeeGrants'; +import { logout } from '@/utils/localStorage'; +import DialogConfirmExitSession from './DialogConfirmExitSession'; + +const ExitSession = () => { + const dispatch = useAppDispatch(); + const { disableAuthzMode } = useAuthzGrants(); + const { disableFeegrantMode } = useFeeGrants(); + + const [confirmExitOpen, setConfirmExitOpen] = useState(false); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const onExitSession = () => { + setConfirmExitOpen(true); + }; + + const exitSession = () => { + dispatch(resetWallet()); + dispatch(resetError()); + dispatch(resetTxAndHash()); + dispatch(bankReset()); + dispatch(rewardsReset()); + dispatch(stakingReset()); + dispatch(authzReset()); + dispatch(feegrantReset()); + disableAuthzMode(); + disableFeegrantMode(); + logout(); + }; + + const onConfirmExitSession = () => { + exitSession(); + setConfirmExitOpen(false); + }; + + return ( + <> + {isWalletConnected ? ( +
    + +
    + ) : null} + setConfirmExitOpen(false)} + onConfirm={onConfirmExitSession} + /> + + ); +}; + +export default ExitSession; diff --git a/frontend/src/components/main-layout/FixedLayout.tsx b/frontend/src/components/main-layout/FixedLayout.tsx new file mode 100644 index 000000000..5a03450c6 --- /dev/null +++ b/frontend/src/components/main-layout/FixedLayout.tsx @@ -0,0 +1,163 @@ +'use client'; + +import React, { useEffect } from 'react'; +import TopBar from './TopBar'; +import SideBar from './SideBar'; +import '@/app/fixed-layout.css'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { networks } from '@/utils/chainsInfo'; +import { + connectSnap, + experimentalSuggestChain, + getSnap, +} from '@leapwallet/cosmos-snap-provider'; +import { + establishMetamaskConnection, + establishWalletConnection, + setIsLoading, + unsetIsLoading, +} from '@/store/features/wallet/walletSlice'; +import { + getLocalNetworks, + getWalletName, + isConnected, + removeAllAuthTokens, +} from '@/utils/localStorage'; +import { setAllNetworksInfo } from '@/store/features/common/commonSlice'; +import useShortCuts from '@/custom-hooks/useShortCuts'; +import TransactionStatusPopup from '../txn-status-popups/TransactionStatusPopup'; +import IBCSwapTxStatus from '../IBCSwapTxStatus'; +import Footer from '../common/Footer'; +import DynamicSection from './DynamicSection'; +import useInitApp from '@/custom-hooks/common/useInitApp'; +import CustomLoader from '../common/CustomLoader'; +import { TxStatus } from '@/types/enums'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; +import { initializeGA } from '@/utils/util'; +import { NotSupportedMetamaskChainIds } from '@/utils/constants'; + +declare let window: WalletWindow; + +const FixedLayout = ({ children }: { children: React.ReactNode }) => { + const dispatch = useAppDispatch(); + useShortCuts(); + const showAuthzAlert = useGetShowAuthzAlert(); + const isLoading = useAppSelector((state) => state.wallet.isLoading); + + const walletState = useAppSelector((state) => state.wallet.status); + + const tryConnectWallet = async (walletName: string) => { + if (walletName === 'metamask') { + dispatch(setIsLoading()); + try { + const snapInstalled = await getSnap(); + if (!snapInstalled) { + await connectSnap(); // Initiates installation if not already present + } + + for (let i = 0; i < networks.length; i++) { + const chainId: string = networks[i].config.chainId; + if (NotSupportedMetamaskChainIds.indexOf(chainId) <= -1) { + try { + await experimentalSuggestChain(networks[i].config, { + force: false, + }); + dispatch( + establishMetamaskConnection({ + walletName, + network: networks[i], + }) + ); + } catch (error) { + console.log('Error while connecting ', chainId); + } + } + } + } catch (error) { + console.log('trying to connect wallet ', error); + } + } else { + dispatch( + establishWalletConnection({ + walletName, + networks: [...networks, ...getLocalNetworks()], + }) + ); + } + }; + + useEffect(() => { + if (!window.GA_INITIALIZED) { + initializeGA(); + window.GA_INITIALIZED = true; + } + }, []); + + useEffect(() => { + dispatch(setAllNetworksInfo()); + + const walletName = getWalletName(); + if (isConnected()) { + tryConnectWallet(walletName); + } else { + dispatch(unsetIsLoading()); + } + + const accountChangeListener = () => { + setTimeout(() => tryConnectWallet(walletName), 1000); + removeAllAuthTokens(); + window.location.reload(); + }; + + window.addEventListener( + `${walletName}_keystorechange`, + accountChangeListener + ); + + return () => { + window.removeEventListener( + `${walletName}_keystorechange`, + accountChangeListener + ); + }; + }, []); + + // Initialize the application state + useInitApp(); + + return ( +
    + +
    +
    + +
    +
    + {walletState === TxStatus.PENDING || isLoading ? ( +
    + +
    + ) : ( + {children} + )} +
    +
    +
    +
    +
    +
    +
    + + +
    + ); +}; + +export default FixedLayout; diff --git a/frontend/src/components/main-layout/Loading.tsx b/frontend/src/components/main-layout/Loading.tsx new file mode 100644 index 000000000..963286be3 --- /dev/null +++ b/frontend/src/components/main-layout/Loading.tsx @@ -0,0 +1,90 @@ +import { RESOLUTE_LOGO } from '@/constants/image-names'; +import Image from 'next/image'; +import Link from 'next/link'; +import React from 'react'; + +const Loading = () => { + return ( +
    + +
    +
    + +
    +
    +
    + ); +}; + +export default Loading; + +const TopBarLoading = () => { + return ( +
    + +
    + ); +}; + +const SideBarLoading = () => { + return ( +
    + + + +
    + ); +}; + +const SelectNetworkLoading = () => { + return ( +
    +
    +
    +
    +
    +
    +
    +
    + ); +}; + +const SideMenuLoading = () => { + const menuOptions = Array.from(Array(10).keys()); + return ( +
    +
    + {menuOptions.map((i) => ( + + ))} +
    +
    + ); +}; + +const MenuItemLoading = () => { + return ( +
    +
    +
    +
    + ); +}; diff --git a/frontend/src/components/main-layout/MenuItem.tsx b/frontend/src/components/main-layout/MenuItem.tsx new file mode 100644 index 000000000..0bd72404c --- /dev/null +++ b/frontend/src/components/main-layout/MenuItem.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { Tooltip } from '@mui/material'; +import { MenuItemI } from '@/constants/sidebar-options'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { tabLink } from '@/utils/util'; + +interface MenuItemProps { + itemData: MenuItemI; + pathName: string; + isExpanded?: boolean; +} + +const MenuItem: React.FC = ({ + itemData, + pathName, + isExpanded, +}) => { + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork.chainName + ); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + + const routePath = pathName === 'overview' ? '/' : `/${pathName}`; + const { icon, name, path, isMetaMaskSupported, authzSupported } = itemData; + + const pageLink = tabLink(path, selectedNetwork); + const walletName = localStorage.getItem('WALLET_NAME'); + const isMetamaskSupported = isMetaMaskSupported || walletName !== 'metamask'; + const isEnableModule = !isAuthzMode || authzSupported; + + const tooltipTitle = !isMetamaskSupported + ? "MetaMask doesn't support" + : !isEnableModule + ? `Authz is not supporting ${name}` + : null; + + const isSelected = routePath === path; + const isDisabled = !(isEnableModule && isMetamaskSupported); + + return ( + + +
    +
    + {name} +
    {name}
    +
    + + {itemData.multipleOptions && ( +
    + {isExpanded +
    + )} +
    +
    + + ); +}; + +export default MenuItem; diff --git a/frontend/src/components/main-layout/ProfileDialog.tsx b/frontend/src/components/main-layout/ProfileDialog.tsx new file mode 100644 index 000000000..307823c1a --- /dev/null +++ b/frontend/src/components/main-layout/ProfileDialog.tsx @@ -0,0 +1,188 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + getConnectWalletLogo, + shortenAddress, + shortenName, +} from '@/utils/util'; +import { Dialog, DialogContent, Slide, SlideProps } from '@mui/material'; +import Image from 'next/image'; +import React, { forwardRef, useEffect, useState } from 'react'; + +import { resetWallet } from '@/store/features/wallet/walletSlice'; +import { logout } from '@/utils/localStorage'; +import { + resetError, + resetTxAndHash, +} from '@/store/features/common/commonSlice'; +import { resetState as bankReset } from '@/store/features/bank/bankSlice'; +import { resetState as rewardsReset } from '@/store/features/distribution/distributionSlice'; +import { resetCompleteState as stakingReset } from '@/store/features/staking/stakeSlice'; +import { resetState as authzReset } from '@/store/features/authz/authzSlice'; +import { resetState as feegrantReset } from '@/store/features/feegrant/feegrantSlice'; +import DialogConfirmExitSession from './DialogConfirmExitSession'; +import useGetAccountInfo from '@/custom-hooks/useGetAccountInfo'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import Copy from '../common/Copy'; +import useAuthzGrants from '@/custom-hooks/useAuthzGrants'; +import useFeeGrants from '@/custom-hooks/useFeeGrants'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const Transition = forwardRef(function Transition( + props: SlideProps & { children: React.ReactElement }, + ref: React.Ref +) { + return ; +}); + +const ProfileDialog = ({ + onClose, + open, +}: { + open: boolean; + onClose: () => void; +}) => { + const { disableAuthzMode } = useAuthzGrants(); + const { disableFeegrantMode } = useFeeGrants(); + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork + ); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + + const chainID = nameToChainIDs?.[selectedNetwork?.chainName?.toLowerCase()]; + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const { getChainInfo } = useGetChainInfo(); + const networkInfo = chainID?.length ? getChainInfo(chainID) : null; + const [chainInfo] = useGetAccountInfo(chainID); + const [walletLogo, setWalletLogo] = useState(''); + const [confirmExitOpen, setConfirmExitOpen] = useState(false); + + const walletUserName = useAppSelector((state) => state.wallet.name); + + useEffect(() => { + if (isWalletConnected) { + setWalletLogo(getConnectWalletLogo()); + } + }, [isWalletConnected]); + + const dispatch = useAppDispatch(); + + const handleLogout = () => { + setConfirmExitOpen(true); // Open confirmation dialog instead of logging out directly + }; + + const handleConfirmExitSession = () => { + dispatch(resetWallet()); + dispatch(resetError()); + dispatch(resetTxAndHash()); + dispatch(bankReset()); + dispatch(rewardsReset()); + dispatch(stakingReset()); + dispatch(authzReset()); + dispatch(feegrantReset()); + disableAuthzMode(); + disableFeegrantMode(); + logout(); + setConfirmExitOpen(false); + onClose(); + }; + + const handleCancelExitSession = () => { + setConfirmExitOpen(false); + }; + + return ( + <> + + +
    + +
    +
    +
    Profile
    +
    + View your account information here +
    +
    +
    +
    + +
    {walletUserName}
    +
    + <> + {chainID?.length && networkInfo && chainInfo ? ( + <> +
    +
    +

    Address

    +
    +

    + {shortenAddress(networkInfo.address, 15)} +

    + +
    +
    +
    +

    Sequence

    + +

    {chainInfo?.sequence}

    +
    +
    +
    +

    Pubkey

    +
    +

    + {shortenName(chainInfo?.pubkey, 25)} +

    + +
    +
    + + ) : null} + +
    + +
    +
    +
    +
    +
    + + + + ); +}; + +export default ProfileDialog; diff --git a/frontend/src/components/main-layout/SelectNetwork.tsx b/frontend/src/components/main-layout/SelectNetwork.tsx new file mode 100644 index 000000000..9d9e0089d --- /dev/null +++ b/frontend/src/components/main-layout/SelectNetwork.tsx @@ -0,0 +1,140 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setChangeNetworkDialogOpen } from '@/store/features/common/commonSlice'; +import { + ALL_NETWORKS_GRADIENT, + ALL_NETWORKS_ICON, + COSMOS_CHAIN_ID, +} from '@/utils/constants'; +import { copyToClipboard } from '@/utils/copyToClipboard'; +import { shortenMsg, shortenName } from '@/utils/util'; +import { Box, Tooltip } from '@mui/material'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; + +const SelectNetwork = () => { + const dispatch = useAppDispatch(); + const [walletAddress, setWalletAddress] = useState(''); + const [chainLogo, setChainLogo] = useState(ALL_NETWORKS_ICON); + const [chainGradient, setChainGradient] = useState(''); + + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork + ); + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + const networks = useAppSelector((state) => state.wallet.networks); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const openChangeNetwork = () => { + dispatch(setChangeNetworkDialogOpen({ open: true, showSearch: true })); + }; + + useEffect(() => { + if (selectedNetwork.chainName && allNetworks) { + const chainID = nameToChainIDs[selectedNetwork.chainName]; + setChainLogo(allNetworks?.[chainID]?.logos?.menu || ALL_NETWORKS_ICON); + setChainGradient(allNetworks?.[chainID]?.config?.theme?.gradient); + } + if (selectedNetwork.chainName && isWalletConnected) { + const chainID = nameToChainIDs[selectedNetwork.chainName]; + setWalletAddress(networks[chainID]?.walletInfo.bech32Address); + setChainLogo(allNetworks?.[chainID]?.logos?.menu || ALL_NETWORKS_ICON); + } else if (!selectedNetwork.chainName) { + setWalletAddress(''); + setChainLogo(ALL_NETWORKS_ICON); + setChainGradient(''); + } + if (!selectedNetwork.chainName && isWalletConnected) { + setWalletAddress( + networks?.[COSMOS_CHAIN_ID]?.walletInfo?.bech32Address || '' + ); + } + }, [selectedNetwork, isWalletConnected, allNetworks]); + + return ( +
    +
    + + + +
    +
    +
    + {shortenName(selectedNetwork.chainName, 15) || 'All Networks'} +
    + dropdown-icon +
    + {walletAddress?.length ? ( + + ) : null} +
    +
    +
    + ); +}; + +export default SelectNetwork; + +export const WalletAddress = ({ + address, + displayAddress = true, +}: { + address: string; + displayAddress?: boolean; +}) => { + const [copied, setCopied] = useState(false); + + const handleCopy = (e: React.MouseEvent) => { + copyToClipboard(address); + setCopied(true); + e.stopPropagation(); + }; + + useEffect(() => { + let timer: NodeJS.Timeout; + if (copied) { + timer = setTimeout(() => { + setCopied(false); + }, 2000); + } + return () => clearTimeout(timer); + }, [copied]); + + return ( +
    + {(displayAddress && ( +
    + {shortenMsg(address, 15)} +
    + )) || + null} + + + copy + +
    + ); +}; diff --git a/frontend/src/components/main-layout/SideBar.tsx b/frontend/src/components/main-layout/SideBar.tsx new file mode 100644 index 000000000..11585519a --- /dev/null +++ b/frontend/src/components/main-layout/SideBar.tsx @@ -0,0 +1,28 @@ +'use client'; + +import React from 'react'; +import ExitSession from './ExitSession'; +import SideMenu from './SideMenu'; +import SelectNetwork from './SelectNetwork'; +import ConnectWallet from './ConnectWallet'; +import DialogSelectNetwork from '../select-network/DialogSelectNetwork'; +import DialogAddNetwork from '../select-network/DialogAddNetwork'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetShowAuthzAlert from '@/custom-hooks/useGetShowAuthzAlert'; + +const SideBar = () => { + const addNetworkOpen = useAppSelector((state) => state.common.addNetworkOpen); + const showAuthzAlert = useGetShowAuthzAlert(); + return ( +
    + + + + + + {addNetworkOpen ? : null} +
    + ); +}; + +export default SideBar; diff --git a/frontend/src/components/main-layout/SideMenu.tsx b/frontend/src/components/main-layout/SideMenu.tsx new file mode 100644 index 000000000..3ed83cb1f --- /dev/null +++ b/frontend/src/components/main-layout/SideMenu.tsx @@ -0,0 +1,365 @@ +import React, { useState } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import MenuItem from './MenuItem'; +import { getSelectedPartFromURL } from '@/utils/util'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import FeegrantButton from '../common/FeegrantButton'; +import AuthzButton from '../common/AuthzButton'; +import Link from 'next/link'; +import { Tooltip } from '@mui/material'; +import { isMetaMaskWallet } from '@/utils/localStorage'; +import { MenuItemI, SIDEBAR_MENU_OPTIONS } from '@/constants/sidebar-options'; + +const DISABLED_FOR_METAMASK = [ + 'ibc-swap', + 'authz', + 'feegrant', + 'cosmwasm', + 'multi-send', + 'txn-builder', +]; +const DISABLED_FOR_AUTHZ_MODE = [ + 'ibc-swap', + 'feegrant', + 'cosmwasm', + 'history', + 'multi-send', + 'txn-builder', +]; +const DISABLED_FOR_FEEGRANT_MODE = [ + 'ibc-swap', + 'authz', + 'cosmwasm', + 'history', + 'txn-builder', +]; + +const SideMenu = () => { + const pathName = usePathname(); + const pathParts = pathName.split('/'); + const selectedPart = getSelectedPartFromURL(pathParts).toLowerCase(); + + return ( +
    +
    + {SIDEBAR_MENU_OPTIONS.map((item) => + item.multipleOptions ? ( + + ) : ( + + ) + )} +
    +
    + ); +}; + +export default SideMenu; + +const MoreOptions = ({ + item, + selectedPart, +}: { + item: MenuItemI; + selectedPart: string; +}) => { + const router = useRouter(); + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork.chainName + ); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const isFeegrantMode = useAppSelector( + (state) => state.feegrant.feegrantModeEnabled + ); + + const [isExpanded, setIsExpanded] = useState(false); + const [isTransfersSettingsExpanded, setIsTransfersSettingsExpanded] = + useState(true); + + const toggleExpand = () => setIsExpanded(!isExpanded); + const toggleTransfersSettingsExpand = () => + setIsTransfersSettingsExpanded(!isTransfersSettingsExpanded); + + const changeTransfersPath = (type: string) => { + const path = selectedNetwork + ? `/transfers/${selectedNetwork.toLowerCase()}?type=${type}` + : `/transfers?type=${type}`; + router.push(path); + }; + + const changeContractsPath = (tab: string) => { + const queryParams = tab ? `?tab=${tab}` : ''; + const path = selectedNetwork + ? `/cosmwasm/${selectedNetwork.toLowerCase()}${queryParams}` + : `/cosmwasm${queryParams}`; + router.push(path); + }; + + const isDisabled = (module: string) => { + if (isMetaMaskWallet() && DISABLED_FOR_METAMASK.includes(module)) { + return { disabled: true, tooltip: "MetaMask doesn't support" }; + } + if (isAuthzMode && DISABLED_FOR_AUTHZ_MODE.includes(module)) { + return { disabled: true, tooltip: `Authz is not supporting ${module}` }; + } + if (isFeegrantMode && DISABLED_FOR_FEEGRANT_MODE.includes(module)) { + return { + disabled: true, + tooltip: `Feegrant is not supporting ${module}`, + }; + } + return { disabled: false, tooltip: null }; + }; + + return ( + <> + {item.name.toLowerCase() === 'transfers' && ( +
    +
    + +
    + + {isTransfersSettingsExpanded && ( +
    +
    +
    +
    changeTransfersPath('single')} + className="cursor-pointer hover:font-semibold" + > + Single +
    +
    +
    +
    + +
    { + if (!isDisabled('multi-send').disabled) { + changeTransfersPath('multi-send'); + } + }} + className={`hover:font-semibold ${isDisabled('multi-send').disabled ? '!cursor-not-allowed' : 'cursor-pointer'}`} + > + Multiple +
    +
    +
    +
    +
    + +
    { + if (!isDisabled('ibc-swap').disabled) + changeTransfersPath('ibc-swap'); + }} + className={`hover:font-semibold ${isDisabled('ibc-swap').disabled ? '!cursor-not-allowed' : 'cursor-pointer'}`} + > + IBC Swap +
    +
    +
    +
    + )} +
    + )} + {item.name.toLowerCase() === 'transactions' && ( +
    +
    + +
    + {isExpanded && ( +
    +
    +
    +
    + + History + +
    +
    +
    +
    + +
    + + Builder + +
    +
    +
    +
    + )} +
    + )} + {item.name.toLowerCase() === 'settings' && ( +
    +
    + +
    + + {isExpanded && ( +
    +
    +
    + +
    + + Authz Mode + + +
    +
    +
    +
    +
    + +
    + + Feegrant Mode + + +
    +
    +
    +
    + )} +
    + )} + {item.name.toLowerCase() === 'cosmwasm' && ( +
    +
    + +
    + {isExpanded && ( +
    +
    +
    + +
    changeContractsPath('codes')} + className={`hover:font-semibold ${isDisabled('cosmwasm').disabled ? '!cursor-not-allowed' : 'cursor-pointer'}`} + > + Codes +
    +
    +
    +
    +
    + +
    changeContractsPath('contracts')} + className={`hover:font-semibold ${isDisabled('cosmwasm').disabled ? '!cursor-not-allowed' : 'cursor-pointer'}`} + > + Contracts +
    +
    +
    +
    + )} +
    + )} + + ); +}; diff --git a/frontend/src/components/main-layout/TopBar.tsx b/frontend/src/components/main-layout/TopBar.tsx new file mode 100644 index 000000000..6df9fafd7 --- /dev/null +++ b/frontend/src/components/main-layout/TopBar.tsx @@ -0,0 +1,123 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import React, { useEffect, useState } from 'react'; +import ProfileDialog from './ProfileDialog'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { setConnectWalletOpen } from '@/store/features/wallet/walletSlice'; +import { getConnectWalletLogo, shortenString } from '@/utils/util'; +import { RootState } from '@/store/store'; +import AuthzGrantsAlert from './AuthzGrantsAlert'; +import { RESOLUTE_LOGO } from '@/constants/image-names'; + +const TopBar = () => { + const dispatch = useAppDispatch(); + const [profileOpen, setProfileOpen] = useState(false); + const [walletLogo, setWalletLogo] = useState(''); + + const { + name: walletUserName, + connected: walletConnected, + isLoading: isWalletLoading, + } = useAppSelector((state: RootState) => state.wallet); + + const onClose = () => { + setProfileOpen(false); + }; + + const connectWalletOpen = () => { + dispatch(setConnectWalletOpen(true)); + }; + + useEffect(() => { + setWalletLogo(getConnectWalletLogo()); + }, [walletConnected]); + + return ( +
    +
    + + +
    + +
    + ); +}; + +export default TopBar; diff --git a/frontend/src/components/popups/WalletPage.tsx b/frontend/src/components/popups/WalletPage.tsx index 5100a45bf..f319bb8e3 100644 --- a/frontend/src/components/popups/WalletPage.tsx +++ b/frontend/src/components/popups/WalletPage.tsx @@ -1,8 +1,8 @@ import React from 'react'; import Image from 'next/image'; import { Dialog, DialogContent } from '@mui/material'; -import { supportedWallets } from '@/utils/contants'; import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import { SUPPORTED_WALLETS } from '@/utils/constants'; const Walletpage = ({ open, @@ -32,7 +32,7 @@ const Walletpage = ({
    - {supportedWallets.map((wallet, index) => ( + {SUPPORTED_WALLETS.map((wallet, index) => (
    { diff --git a/frontend/src/components/select-network/DialogAddNetwork.tsx b/frontend/src/components/select-network/DialogAddNetwork.tsx new file mode 100644 index 000000000..67c22f121 --- /dev/null +++ b/frontend/src/components/select-network/DialogAddNetwork.tsx @@ -0,0 +1,242 @@ +import React, { ChangeEvent, useEffect, useState } from 'react'; +import CustomDialog from '../common/CustomDialog'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { + setAddNetworkDialogOpen, + setError, +} from '@/store/features/common/commonSlice'; +import CustomButton from '../common/CustomButton'; +import { validate, ValidationError } from 'jsonschema'; +import { get } from 'lodash'; +import { convertKeysToCamelCase } from '@/utils/util'; +import { getLocalNetworks, setLocalNetwork } from '@/utils/localStorage'; +import { + establishWalletConnection, + resetConnectWalletStatus, +} from '@/store/features/wallet/walletSlice'; +import { networks } from '@/utils/chainsInfo'; +import { TxStatus } from '@/types/enums'; +import networkConfigFormat from '@/utils/networkConfigSchema.json'; +import Image from 'next/image'; +import { UPLOAD_ICON } from '@/constants/image-names'; +import { ADD_NETWORK_TEMPLATE_URL } from '@/utils/constants'; +import Link from 'next/link'; +import { IconButton, Tooltip } from '@mui/material'; +import ClearIcon from '@mui/icons-material/Clear'; +import { CHAIN_ID_EXIST_ERROR, CHAIN_NAME_EXIST_ERROR } from '@/utils/errors'; + +const DialogAddNetwork = () => { + const dispatch = useAppDispatch(); + const open = useAppSelector((state) => state.common.addNetworkOpen); + const handleClose = () => { + dispatch(setAddNetworkDialogOpen(false)); + }; + + const [uploadedFileName, setUploadedFileName] = useState(''); + const [chainIDExist, setChainIDExist] = useState(false); + const [chainNameExist, setChainNameExist] = useState(false); + const [showErrors, setShowErrors] = useState(false); + const [validationErrors, setValidationErrors] = useState( + [] + ); + const [networkConfig, setNetworkConfig] = useState({}); + + const nameToChainIDs: Record = useAppSelector( + (state) => state.wallet.nameToChainIDs + ); + const connectWalletStatus = useAppSelector((state) => state.wallet.status); + + const handleFileChange = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = (e) => { + const contents = e.target?.result as string; + onFileContents(contents); + setUploadedFileName(file.name); + }; + reader.onerror = (e) => { + alert(e); + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const onFileContents = (content: string): void => { + try { + const parsedData = JSON.parse(content); + const res = validate(parsedData, networkConfigFormat); + setValidationErrors(res.errors); + if (!get(res, 'errors.length')) { + setChainNameExist( + chainNameExists(get(parsedData, 'config.chain_name')) + ); + setChainIDExist(chainIDExists(get(parsedData, 'config.chain_id'))); + parsedData.is_custom_network = true; + setNetworkConfig(parsedData); + } else { + setNetworkConfig({}); + } + } catch (e) { + setNetworkConfig({}); + console.log(e); + } + }; + + const chainNameExists = (chainName: string) => { + const chainNamesList = Object.keys(nameToChainIDs); + if (chainNamesList.includes(chainName.toLowerCase())) { + return true; + } + return false; + }; + + const chainIDExists = (chainID: string) => { + const chainNamesList = Object.keys(nameToChainIDs); + for (const chain in chainNamesList) { + if ( + nameToChainIDs[chainNamesList[chain]].toLowerCase() === + chainID.toLowerCase() + ) { + return true; + } + } + return false; + }; + + const addNetwork = () => { + const chainID = get(networkConfig, 'config.chain_id'); + if (!chainIDExist && !chainNameExist && chainID) { + const networkConfigFormatted = convertKeysToCamelCase(networkConfig); + setLocalNetwork(networkConfigFormatted, chainID); + dispatch( + establishWalletConnection({ + walletName: 'keplr', + networks: [...networks, ...getLocalNetworks()], + }) + ); + } else { + setNetworkConfig({}); + setError({ + type: 'error', + message: 'Invalid JSON file', + }); + } + }; + + useEffect(() => { + if (uploadedFileName && !validationErrors?.length) { + if (connectWalletStatus === TxStatus.IDLE) { + handleClose(); + dispatch(setError({ type: 'success', message: 'Network Added' })); + } else if (connectWalletStatus === TxStatus.REJECTED) { + dispatch(setError({ type: 'error', message: 'Failed to add network' })); + } + } + }, [connectWalletStatus]); + + useEffect(() => { + dispatch(resetConnectWalletStatus()); + }, []); + + return ( + +
    +
    { + document.getElementById('network_config_file')!.click(); + }} + > + {uploadedFileName ? ( +
    + {uploadedFileName}{' '} + + { + setUploadedFileName(''); + setChainIDExist(false); + setChainNameExist(false); + setShowErrors(false); + e.stopPropagation(); + }} + > + + + +
    + ) : ( +
    + +
    + Upload JSON here +
    +
    +
    Download Sample
    + e.stopPropagation()} + className="text-[14px] underline underline-offset-[3px] font-bold text-[#ffffffad]" + > + here + +
    +
    + )} +
    +
    + +
    + {uploadedFileName && validationErrors?.length ? ( +
    +
    +
    Invalid json file
    +
    setShowErrors((showErrors) => !showErrors)} + > + view errors +
    +
    + {showErrors && + validationErrors?.map((item, index) => ( +
  • {item.stack}
  • + ))} +
    + ) : ( +
    + {chainNameExist ?
  • {CHAIN_NAME_EXIST_ERROR}
  • : <>} + {chainIDExist ?
  • {CHAIN_ID_EXIST_ERROR}
  • : <>} +
    + )} +
    + +
    +
    + ); +}; + +export default DialogAddNetwork; diff --git a/frontend/src/components/select-network/DialogConfirmDeleteNetwork.tsx b/frontend/src/components/select-network/DialogConfirmDeleteNetwork.tsx new file mode 100644 index 000000000..a5d182f05 --- /dev/null +++ b/frontend/src/components/select-network/DialogConfirmDeleteNetwork.tsx @@ -0,0 +1,66 @@ +import { DELETE_ILLUSTRATION } from '@/constants/image-names'; +import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; +import CustomButton from '../common/CustomButton'; + +const DialogConfirmDeleteNetwork = ({ + onClose, + open, + onConfirm, +}: { + open: boolean; + onClose: () => void; + onConfirm: () => void; +}) => { + return ( + + +
    + +
    +
    + Delete +
    +
    Remove Network
    +
    + Are you sure you want to remove network? +
    +
    +
    + +
    +
    +
    +
    + ); +}; + +export default DialogConfirmDeleteNetwork; diff --git a/frontend/src/components/select-network/DialogSelectNetwork.tsx b/frontend/src/components/select-network/DialogSelectNetwork.tsx new file mode 100644 index 000000000..640d2dedf --- /dev/null +++ b/frontend/src/components/select-network/DialogSelectNetwork.tsx @@ -0,0 +1,175 @@ +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { + setAddNetworkDialogOpen, + setChangeNetworkDialogOpen, + setSelectedNetwork, +} from '@/store/features/common/commonSlice'; +import { allNetworksLink, changeNetworkRoute } from '@/utils/util'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import Link from 'next/link'; +import { usePathname, useSearchParams } from 'next/navigation'; +import React, { useState } from 'react'; +import NetworkItem from '../select-network/NetworkItem'; +import SearchNetworkInput from './SearchNetworkInput'; +import useHandleRouteChange from '@/custom-hooks/routing/useHandleRouteChange'; + +const DialogSelectNetwork = () => { + useHandleRouteChange(); + const dispatch = useAppDispatch(); + const pathName = usePathname(); + const searchParams = useSearchParams(); + const { getChainNamesAndLogos } = useGetChainInfo(); + const chains = getChainNamesAndLogos(); + + const [searchQuery, setSearchQuery] = useState(''); + + const pathParts = pathName.split('/'); + + const dialogOpen = useAppSelector( + (state) => state.common.changeNetworkDialog.open + ); + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork + ); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); + + const filteredChains = chains.filter((chain) => + chain.chainName.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const onClose = () => { + dispatch(setChangeNetworkDialogOpen({ open: false, showSearch: false })); + setSearchQuery(''); + }; + + const isSelected = (chainName: string): boolean => { + return ( + selectedNetwork?.chainName?.toLowerCase() === chainName.toLowerCase() + ); + }; + + const constructUrlWithQueryParams = (newChain: string) => { + const queryParams = new URLSearchParams(Array.from(searchParams.entries())); + const baseUrl = changeNetworkRoute(pathName, newChain); + return `${baseUrl}?${queryParams.toString()}`; + }; + + const constructAllNetworksUrl = (pathParts: string[]) => { + const queryParams = new URLSearchParams(Array.from(searchParams.entries())); + return `${allNetworksLink(pathParts)}?${queryParams.toString()}`; + }; + + return ( + + +
    +
    + +
    +
    +
    +
    Select Network
    +
    + Select a network from the list of supported networks on Resolute +
    +
    +
    +
    + + setSearchQuery(e.target.value) + } + searchQuery={searchQuery} + /> +
    + {isWalletConnected ? ( + + ) : null} +
    +
    + { + dispatch(setSelectedNetwork({ chainName: '' })); + onClose(); + }} + className={`network-item justify-center ${selectedNetwork.chainName?.length ? '' : 'bg-[#FFFFFF14] !border-transparent'}`} + prefetch={false} + > +
    + +
    +

    + All Networks +

    + +
    +
    +
    All Networks
    +
    +
    +
    + {filteredChains.map((chain) => ( + + ))} +
    + {filteredChains?.length === 0 && ( +
    +
    - No Networks Found -
    +
    + )} +
    +
    +
    +
    +
    + ); +}; + +export default DialogSelectNetwork; diff --git a/frontend/src/components/select-network/NetworkItem.tsx b/frontend/src/components/select-network/NetworkItem.tsx new file mode 100644 index 000000000..afac55296 --- /dev/null +++ b/frontend/src/components/select-network/NetworkItem.tsx @@ -0,0 +1,96 @@ +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { + setChangeNetworkDialogOpen, + setError, + setSelectedNetwork, +} from '@/store/features/common/commonSlice'; +import { Avatar, Badge, IconButton, Tooltip } from '@mui/material'; +import Link from 'next/link'; +import React, { useState } from 'react'; +import CancelIcon from '@mui/icons-material/Cancel'; +import { establishWalletConnection } from '@/store/features/wallet/walletSlice'; +import { networks } from '@/utils/chainsInfo'; +import { getLocalNetworks, removeLocalNetwork } from '@/utils/localStorage'; +import { useRouter } from 'next/navigation'; +import DialogConfirmDeleteNetwork from './DialogConfirmDeleteNetwork'; + +const NetworkItem = ({ + chainName, + chainLogo, + pathName, + handleClose, + selected, + isCustomNetwork, + chainID, +}: { + chainName: string; + chainLogo: string; + pathName: string; + handleClose: () => void; + selected: boolean; + isCustomNetwork?: boolean; + chainID: string; +}) => { + const dispatch = useAppDispatch(); + const router = useRouter(); + + const [removeNetworkDialogOpen, setRemoveNetworkDialogOpen] = useState(false); + + const handleRemoveNetwork = async () => { + await removeLocalNetwork(chainID); + dispatch( + establishWalletConnection({ + walletName: 'keplr', + networks: [...networks, ...getLocalNetworks()], + }) + ); + setRemoveNetworkDialogOpen(false); + dispatch(setChangeNetworkDialogOpen({ open: false, showSearch: true })); + dispatch(setError({ type: 'success', message: 'Network Removed' })); + setTimeout(() => router.push('/'), 2000); + }; + + return ( + + setRemoveNetworkDialogOpen(true)} + color="primary" + size="small" + > + + + + ) : null + } + overlap="rectangular" + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + > + { + dispatch(setSelectedNetwork({ chainName: chainName.toLowerCase() })); + handleClose(); + }} + > +
    + +
    +

    {chainName}

    + + setRemoveNetworkDialogOpen(false)} + onConfirm={handleRemoveNetwork} + /> +
    + ); +}; + +export default NetworkItem; diff --git a/frontend/src/components/select-network/SearchNetworkInput.tsx b/frontend/src/components/select-network/SearchNetworkInput.tsx new file mode 100644 index 000000000..a93156fdc --- /dev/null +++ b/frontend/src/components/select-network/SearchNetworkInput.tsx @@ -0,0 +1,26 @@ +import Image from 'next/image'; +import React from 'react'; + +const SearchNetworkInput = ({ + searchQuery, + handleSearchQueryChange, +}: { + searchQuery: string; + handleSearchQueryChange: (e: React.ChangeEvent) => void; +}) => { + return ( +
    + + +
    + ); +}; + +export default SearchNetworkInput; diff --git a/frontend/src/components/txn-builder/TxnBuilder.tsx b/frontend/src/components/txn-builder/TxnBuilder.tsx new file mode 100644 index 000000000..125f6a005 --- /dev/null +++ b/frontend/src/components/txn-builder/TxnBuilder.tsx @@ -0,0 +1,197 @@ +import { NO_MESSAGES_ILLUSTRATION } from '@/constants/image-names'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import Image from 'next/image'; +import React, { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import '@/app/(routes)/multiops/multiops.css'; +import { TextField } from '@mui/material'; +import { customMUITextFieldStyles } from '@/app/(routes)/multiops/styles'; +import CustomButton from '../common/CustomButton'; +import MessagesList from './components/MessagesList'; +import SelectMessage from './components/SelectMessage'; + +const TxnBuilder = ({ + chainID, + onSubmit, + loading, + availableBalance, + fromAddress, + isMultisig, +}: { + chainID: string; + onSubmit: (data: TxnBuilderForm) => void; + loading: boolean; + availableBalance: number; + fromAddress: string; + isMultisig: boolean; +}) => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const basicChainInfo = getChainInfo(chainID); + const { decimals, displayDenom, minimalDenom } = getDenomInfo(chainID); + const currency = { + coinDenom: displayDenom, + coinDecimals: decimals, + coinMinimalDenom: minimalDenom, + }; + const { feeAmount } = basicChainInfo; + + const [txType, setTxType] = useState(''); + const [messages, setMessages] = useState([]); + + const { handleSubmit, control } = useForm({ + defaultValues: { + gas: 900000, + memo: '', + fees: feeAmount * 10 ** currency.coinDecimals, + }, + }); + + const handleSelectMessage = (type: TxnMsgType) => { + setTxType(type); + }; + + const handleAddMessage = (msg: Msg) => { + setMessages((prev) => [...prev, msg]); + }; + + const onDeleteMsg = (index: number) => { + const arr = messages.filter((_, i) => i !== index); + setMessages(arr); + }; + + const clearAllMessages = () => { + setTxType(''); + setMessages([]); + }; + + const onFormSubmit = (data: { gas: number; memo: string; fees: number }) => { + onSubmit({ + fees: data.fees, + gas: data.gas, + memo: data.memo, + msgs: messages, + }); + }; + + return ( +
    + { + setTxType(''); + }} + isMultisig={isMultisig} + /> +
    +
    +
    Transaction Summary
    + {messages?.length ? ( +
    + Clear All +
    + ) : null} +
    + {messages?.length ? ( +
    + +
    +
    +
    +
    +
    Enter Gas
    + ( + + )} + /> +
    +
    +
    Enter Memo (optional)
    + ( + + )} + /> +
    +
    + + +
    +
    + ) : ( +
    + No Messages +
    + Select a message from the left side to add here +
    +
    + )} +
    +
    + ); +}; + +export default TxnBuilder; diff --git a/frontend/src/components/txn-builder/components/AddMsgButton.tsx b/frontend/src/components/txn-builder/components/AddMsgButton.tsx new file mode 100644 index 000000000..2ad381e0f --- /dev/null +++ b/frontend/src/components/txn-builder/components/AddMsgButton.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +interface AddMsgButtonProps { + fileUploadTxns: Msg[]; + handleAddMsgs: (msg: Msg[]) => void; + onRemoveFileUploadTxns: () => void; +} + +const AddMsgButton = (props: AddMsgButtonProps) => { + const { fileUploadTxns, handleAddMsgs, onRemoveFileUploadTxns } = props; + + return ( + + ); +}; + +export default AddMsgButton; diff --git a/frontend/src/components/txn-builder/components/CustomAutoComplete.tsx b/frontend/src/components/txn-builder/components/CustomAutoComplete.tsx new file mode 100644 index 000000000..3f9f72b26 --- /dev/null +++ b/frontend/src/components/txn-builder/components/CustomAutoComplete.tsx @@ -0,0 +1,118 @@ +import ValidatorLogo from '@/app/(routes)/staking/components/ValidatorLogo'; +import { + customAutoCompleteStyles, + customTextFieldStyles, +} from '@/app/(routes)/transfers/styles'; +import NoOptions from '@/components/common/NoOptions'; +import { shortenName } from '@/utils/util'; +import { + Autocomplete, + CircularProgress, + Paper, + TextField, +} from '@mui/material'; +import React from 'react'; + +const CustomAutoComplete = ({ + options, + selectedOption, + handleChange, + dataLoading, + name, + emptyText, +}: { + options: ValidatorOption[]; + selectedOption: ValidatorOption | null; + handleChange: (option: ValidatorOption | null) => void; + dataLoading: boolean; + name: string; + emptyText: string; +}) => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderOption = (props: any, option: ValidatorOption) => ( +
  • +
    + +
    + + {shortenName(option.label, 15)} + +
    +
    +
  • + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderInput = (params: any) => ( + + {selectedOption && ( + + )} + {params.InputProps.startAdornment} + + ), + }} + sx={{ + '& .MuiInputBase-input': { + color: 'white', + fontSize: '14px', + fontWeight: 300, + fontFamily: 'Libre Franklin', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: 'white', + }, + }} + /> + ); + + return ( + option.label} + renderOption={renderOption} + renderInput={renderInput} + noOptionsText={} + onChange={(_, newValue) => handleChange(newValue)} + value={selectedOption} + PaperComponent={({ children }) => ( + + {dataLoading ? ( +
    + +
    + ) : ( + children + )} +
    + )} + sx={{ ...customTextFieldStyles, ...customAutoCompleteStyles }} + /> + ); +}; + +export default CustomAutoComplete; diff --git a/frontend/src/components/txn-builder/components/FileUpload.tsx b/frontend/src/components/txn-builder/components/FileUpload.tsx new file mode 100644 index 000000000..91c376cfc --- /dev/null +++ b/frontend/src/components/txn-builder/components/FileUpload.tsx @@ -0,0 +1,267 @@ +import { UPLOAD_ICON } from '@/constants/image-names'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import { MULTIOPS_MSG_TYPES, MULTIOPS_SAMPLE_FILES } from '@/utils/constants'; +import { + parseDelegateMsgsFromContent, + parseReDelegateMsgsFromContent, + parseSendMsgsFromContent, + parseUnDelegateMsgsFromContent, + parseVoteMsgsFromContent, +} from '@/utils/parseMsgs'; +import Image from 'next/image'; +import React from 'react'; +import * as XLSX from 'xlsx'; + +interface FileUploadProps { + fromAddress: string; + msgType: string; + onUpload: (msgs: Msg[]) => void; + onCancel: () => void; + msgsCount: number; +} + +const FileUpload = (props: FileUploadProps) => { + const { fromAddress, msgType, onUpload, onCancel, msgsCount } = props; + + const dispatch = useAppDispatch(); + + const parseExcel = (content: ArrayBuffer) => { + const workbook = XLSX.read(content, { type: 'array' }); + const sheetName = workbook.SheetNames[0]; + const sheet = workbook.Sheets[sheetName]; + return XLSX.utils.sheet_to_csv(sheet); + }; + + const onFileContents = (content: string, type: string) => { + switch (type) { + case MULTIOPS_MSG_TYPES.send: { + const [parsedTxns, error] = parseSendMsgsFromContent( + fromAddress, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + onUpload(parsedTxns); + dispatch(setError({ type: 'success', message: 'File uploaded' })); + } + break; + } + case MULTIOPS_MSG_TYPES.delegate: { + const [parsedTxns, error] = parseDelegateMsgsFromContent( + fromAddress, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + onUpload(parsedTxns); + dispatch(setError({ type: 'success', message: 'File uploaded' })); + } + break; + } + case MULTIOPS_MSG_TYPES.redelegate: { + const [parsedTxns, error] = parseReDelegateMsgsFromContent( + fromAddress, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + onUpload(parsedTxns); + dispatch(setError({ type: 'success', message: 'File uploaded' })); + } + break; + } + case MULTIOPS_MSG_TYPES.undelegate: { + const [parsedTxns, error] = parseUnDelegateMsgsFromContent( + fromAddress, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + onUpload(parsedTxns); + dispatch(setError({ type: 'success', message: 'File uploaded' })); + } + break; + } + case MULTIOPS_MSG_TYPES.vote: { + const [parsedTxns, error] = parseVoteMsgsFromContent( + fromAddress, + content + ); + if (error) { + dispatch( + setError({ + type: 'error', + message: error, + }) + ); + } else { + onUpload(parsedTxns); + dispatch(setError({ type: 'success', message: 'File uploaded' })); + } + break; + } + default: + onUpload([]); + } + }; + + return ( + <> +
    +
    +
    and/or
    +
    +
    +
    +
    { + document.getElementById('multiops_file')!.click(); + }} + > + {msgsCount > 0 ? ( +
    +
    + You are adding{' '} + {msgsCount} messages to this + transaction +
    + +
    + ) : ( +
    +
    + +
    Upload CSV or Excel here
    +
    +
    +
    + Download Sample +
    + +
    +
    + )} +
    + { + const files = e.target.files; + if (!files) { + return; + } + const file = files[0]; + if (!file) { + return; + } + + try { + const reader = new FileReader(); + reader.onload = (e) => { + try { + const contents = e?.target?.result; + if ( + file.type === + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || + file.type === 'application/vnd.ms-excel' + ) { + const parsedContent = parseExcel(contents as ArrayBuffer); + onFileContents(parsedContent, msgType); + } else { + const decoder = new TextDecoder('utf-8'); + const decodedContent = decoder.decode( + contents as ArrayBuffer + ); + onFileContents(decodedContent, msgType); + } + } catch (_) { + dispatch( + setError({ + type: 'error', + message: 'Error while parsing file contents', + }) + ); + } + }; + reader.onerror = (e) => { + console.log('Error reading the file', e); + dispatch( + setError({ + type: 'error', + message: 'Error reading the file.', + }) + ); + }; + reader.readAsArrayBuffer(file); + } catch (error) { + dispatch( + setError({ + type: 'error', + message: 'Error while uploading file', + }) + ); + } + e.target.value = ''; + }} + /> +
    + + ); +}; + +export default FileUpload; diff --git a/frontend/src/components/txn-builder/components/MessagesList.tsx b/frontend/src/components/txn-builder/components/MessagesList.tsx new file mode 100644 index 000000000..e4b1cdd73 --- /dev/null +++ b/frontend/src/components/txn-builder/components/MessagesList.tsx @@ -0,0 +1,166 @@ +import DelegateMessage from '@/app/(routes)/multiops/components/Messages/DelegateMessage'; +import DepositMessage from '@/app/(routes)/multiops/components/Messages/DepositMessage'; +import RedelegateMessage from '@/app/(routes)/multiops/components/Messages/RedelegateMessage'; +import SendMessage from '@/app/(routes)/multiops/components/Messages/SendMessage'; +import UndelegateMessage from '@/app/(routes)/multiops/components/Messages/UndelegateMessage'; +import VoteMessage from '@/app/(routes)/multiops/components/Messages/VoteMessage'; +import { paginationComponentStyles } from '@/app/(routes)/staking/styles'; +import { + DELEGATE_TYPE_URL, + DEPOSIT_TYPE_URL, + REDELEGATE_TYPE_URL, + SEND_TYPE_URL, + UNDELEGATE_TYPE_URL, + VOTE_TYPE_URL, +} from '@/utils/constants'; +import { Pagination } from '@mui/material'; +import React, { useEffect, useState } from 'react'; + +const PER_PAGE = 7; + +const renderMessage = ( + msg: Msg, + index: number, + currency: Currency, + onDelete: (index: number) => void, + chainID: string +) => { + switch (msg.typeUrl) { + case SEND_TYPE_URL: + return ( + + ); + case DELEGATE_TYPE_URL: + return ( + + ); + case UNDELEGATE_TYPE_URL: + return ( + + ); + case REDELEGATE_TYPE_URL: + return ( + + ); + case VOTE_TYPE_URL: + return ( + + ); + case DEPOSIT_TYPE_URL: + return ( + + ); + default: + return msg?.typeUrl ? ( +
    + {msg?.typeUrl} +
    + ) : null; + } +}; + +const MessagesList = ({ + messages, + currency, + onDeleteMsg, + chainID, +}: { + messages: Msg[]; + currency: Currency; + onDeleteMsg: (index: number) => void; + chainID: string; +}) => { + const [slicedMsgs, setSlicedMsgs] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + + useEffect(() => { + if (messages.length < PER_PAGE) { + setSlicedMsgs(messages); + } else { + const page = Math.ceil(messages.length / PER_PAGE); + setCurrentPage(page); + setSlicedMsgs( + messages?.slice( + (page - 1) * PER_PAGE, + (page - 1) * PER_PAGE + PER_PAGE + ) + ); + } + }, [messages]); + + return ( +
    +
    PER_PAGE ? 'border-b-[0.5px] border-[#ffffff2e]' : ''}`} + > + {slicedMsgs.map((msg, index) => ( +
    + {renderMessage( + msg, + index + PER_PAGE * (currentPage - 1), + currency, + onDeleteMsg, + chainID + )} +
    + ))} +
    +
    PER_PAGE + ? 'mt-2 flex justify-end opacity-100' + : 'mt-2 flex justify-end opacity-0' + } + > + { + setCurrentPage(v); + setSlicedMsgs(messages?.slice((v - 1) * PER_PAGE, v * PER_PAGE)); + }} + /> +
    +
    + ); +}; + +export default MessagesList; diff --git a/frontend/src/components/txn-builder/components/ProposalsList.tsx b/frontend/src/components/txn-builder/components/ProposalsList.tsx new file mode 100644 index 000000000..f86f9c4f5 --- /dev/null +++ b/frontend/src/components/txn-builder/components/ProposalsList.tsx @@ -0,0 +1,95 @@ +import { + customAutoCompleteStyles, + customTextFieldStyles, +} from '@/app/(routes)/transfers/styles'; +import NoOptions from '@/components/common/NoOptions'; +import { shortenName } from '@/utils/util'; +import { + Autocomplete, + CircularProgress, + Paper, + TextField, +} from '@mui/material'; +import React from 'react'; + +const ProposalsList = ({ + options, + selectedOption, + handleChange, + dataLoading, +}: { + options: ProposalOption[]; + selectedOption: ProposalOption | null; + handleChange: (option: ProposalOption | null) => void; + dataLoading: boolean; +}) => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderOption = (props: any, option: ProposalOption) => ( +
  • +
    + #{option.value} + {shortenName(option.label, 36)} +
    +
  • + ); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderInput = (params: any) => ( + + ); + + return ( + option.label} + renderOption={renderOption} + renderInput={renderInput} + noOptionsText={} + onChange={(_, newValue) => handleChange(newValue)} + value={selectedOption} + PaperComponent={({ children }) => ( + + {dataLoading ? ( +
    + +
    + ) : ( + children + )} +
    + )} + sx={{ ...customTextFieldStyles, ...customAutoCompleteStyles }} + /> + ); +}; + +export default ProposalsList; diff --git a/frontend/src/components/txn-builder/components/SelectMessage.tsx b/frontend/src/components/txn-builder/components/SelectMessage.tsx new file mode 100644 index 000000000..d8b779e5e --- /dev/null +++ b/frontend/src/components/txn-builder/components/SelectMessage.tsx @@ -0,0 +1,111 @@ +import SectionHeader from '@/components/common/SectionHeader'; +import { TXN_BUILDER_MSGS } from '@/constants/txn-builder'; +import React from 'react'; +import SendForm from '../messages/SendForm'; +import DelegateForm from '../messages/DelegateForm'; +import UndelegateForm from '../messages/UndelegateForm'; +import RedelegateForm from '../messages/RedelegateForm'; +import VoteForm from '../messages/VoteForm'; +import CustomMessageForm from '../messages/CustomMessageForm'; + +const SelectMessage = ({ + handleSelectMessage, + txType, + handleAddMessage, + currency, + chainID, + fromAddress, + availableBalance, + cancelAddMsg, + isMultisig, +}: { + handleSelectMessage: (type: TxnMsgType) => void; + txType: string; + handleAddMessage: (msg: Msg) => void; + currency: Currency; + chainID: string; + fromAddress: string; + availableBalance: number; + cancelAddMsg: () => void; + isMultisig: boolean; +}) => { + return ( +
    +
    + +
    + {TXN_BUILDER_MSGS.map((msg: TxnMsgType) => ( + + ))} +
    +
    +
    + {txType === 'Send' && ( + + )} + {txType === 'Delegate' && ( + + )} + {txType === 'Undelegate' && ( + + )} + {txType === 'Redelegate' && ( + + )} + {txType === 'Vote' && ( + + )} + {txType === 'Custom' && ( + + )} +
    +
    + ); +}; + +export default SelectMessage; diff --git a/frontend/src/components/txn-builder/components/VoteOptionsList.tsx b/frontend/src/components/txn-builder/components/VoteOptionsList.tsx new file mode 100644 index 000000000..6f1cef70b --- /dev/null +++ b/frontend/src/components/txn-builder/components/VoteOptionsList.tsx @@ -0,0 +1,84 @@ +import { Autocomplete, Paper, TextField } from '@mui/material'; +import React from 'react'; +import { + customAutoCompleteStyles, + customTextFieldStyles, +} from '@/app/(routes)/transfers/styles'; + +const voteOptions: VoteOption[] = [ + { + label: 'Yes', + value: 1, + }, + { + label: 'No', + value: 3, + }, + { + label: 'Abstain', + value: 2, + }, + { + label: 'No With Veto', + value: 4, + }, +]; + +const VoteOptionsList = ({ + selectedOption, + handleChange, +}: { + selectedOption: VoteOption | null; + handleChange: (option: VoteOption | null) => void; +}) => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const renderInput = (params: any) => ( + + ); + + return ( + option.label} + renderInput={renderInput} + onChange={(_, newValue) => handleChange(newValue)} + value={selectedOption} + PaperComponent={({ children }) => ( + + {children} + + )} + sx={{ ...customTextFieldStyles, ...customAutoCompleteStyles }} + /> + ); +}; +export default VoteOptionsList; diff --git a/frontend/src/components/txn-builder/messages/CustomMessageForm.tsx b/frontend/src/components/txn-builder/messages/CustomMessageForm.tsx new file mode 100644 index 000000000..10bd8ecad --- /dev/null +++ b/frontend/src/components/txn-builder/messages/CustomMessageForm.tsx @@ -0,0 +1,128 @@ +import { + customMessageValueFieldStyles, + customMUITextFieldStyles, +} from '@/app/(routes)/multiops/styles'; +import { CUSTOM_MSG_VALUE_PLACEHOLDER } from '@/constants/txn-builder'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import { TextField } from '@mui/material'; +import React from 'react'; +import { Controller, useForm } from 'react-hook-form'; + +interface CustomMessageProps { + onAddMsg: (payload: Msg) => void; + cancelAddMsg: () => void; +} + +const CustomMessageForm = (props: CustomMessageProps) => { + const { onAddMsg, cancelAddMsg } = props; + const dispatch = useAppDispatch(); + const { + handleSubmit, + control, + reset + } = useForm({ + defaultValues: { + typeUrl: '', + value: '', + }, + }); + + const onSubmit = (data: { typeUrl: string; value: string }) => { + try { + const msg: Msg = { + typeUrl: data.typeUrl, + value: JSON.parse(data.value), + }; + + onAddMsg(msg); + reset(); + } catch (_) { + dispatch(setError({ type: 'error', message: 'Invalid input for value' })); + } + }; + + return ( +
    +
    +
    +
    Custom Message
    + +
    +
    +
    +
    Enter Type URL
    + ( + + )} + /> +
    +
    +
    Enter Value
    + ( + + )} + /> +
    +
    +
    +
    + +
    +
    + ); +}; + +export default CustomMessageForm; diff --git a/frontend/src/components/txn-builder/messages/DelegateForm.tsx b/frontend/src/components/txn-builder/messages/DelegateForm.tsx new file mode 100644 index 000000000..c97086089 --- /dev/null +++ b/frontend/src/components/txn-builder/messages/DelegateForm.tsx @@ -0,0 +1,184 @@ +import { customMUITextFieldStyles } from '@/app/(routes)/multiops/styles'; +import { InputAdornment, TextField } from '@mui/material'; +import React, { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import CustomAutoComplete from '../components/CustomAutoComplete'; +import useStaking from '@/custom-hooks/txn-builder/useStaking'; +import { useAppSelector } from '@/custom-hooks/StateHooks'; +import { TxStatus } from '@/types/enums'; +import { Decimal } from '@cosmjs/math'; +import { formatCoin } from '@/utils/util'; +import FileUpload from '../components/FileUpload'; +import AddMsgButton from '../components/AddMsgButton'; + +interface DelegateFormProps { + chainID: string; + fromAddress: string; + onDelegate: (payload: Msg) => void; + currency: Currency; + availableBalance: number; + cancelAddMsg: () => void; +} + +const DelegateForm = (props: DelegateFormProps) => { + const { + fromAddress, + chainID, + currency, + onDelegate, + availableBalance, + cancelAddMsg, + } = props; + const { getValidators } = useStaking(); + const { validatorsList } = getValidators({ chainID }); + const [selectedOption, setSelectedOption] = useState( + null + ); + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + const { handleSubmit, control, setValue, reset } = useForm({ + defaultValues: { + amount: '', + validator: '', + delegator: fromAddress, + }, + }); + + const [fileUploadTxns, setFileUploadTxns] = useState([]); + + const handleValidatorChange = (option: ValidatorOption | null) => { + setValue('validator', option?.address || ''); + setSelectedOption(option); + }; + + const onSubmit = (data: { + amount: string; + validator: string; + delegator: string; + }) => { + if (data.validator) { + const baseAmount = Decimal.fromUserInput( + data.amount, + Number(currency?.coinDecimals) + ).atomics; + const msgDelegate = { + delegatorAddress: data.delegator, + validatorAddress: data.validator, + amount: { + amount: baseAmount, + denom: currency?.coinMinimalDenom, + }, + }; + + onDelegate({ + typeUrl: '/cosmos.staking.v1beta1.MsgDelegate', + value: msgDelegate, + }); + reset(); + handleValidatorChange(null); + } + }; + + const handleAddMsgs = (msgs: Msg[]) => { + for (const msg of msgs) { + onDelegate(msg); + } + }; + + const onAddFileUploadTxns = (msgs: Msg[]) => { + setFileUploadTxns(msgs); + }; + + const onRemoveFileUploadTxns = () => { + setFileUploadTxns([]); + }; + + return ( +
    +
    +
    +
    +
    Delegate
    + +
    +
    +
    +
    Select Validator
    + +
    +
    +
    Enter Amount
    + ( + + + {'Available:'}{' '} + {formatCoin(availableBalance, currency.coinDenom)}{' '} + +
    + ), + }} + /> + )} + /> +
    +
    +
    + +
    + + + ); +}; + +export default DelegateForm; diff --git a/frontend/src/components/txn-builder/messages/RedelegateForm.tsx b/frontend/src/components/txn-builder/messages/RedelegateForm.tsx new file mode 100644 index 000000000..aec7f1b55 --- /dev/null +++ b/frontend/src/components/txn-builder/messages/RedelegateForm.tsx @@ -0,0 +1,240 @@ +import { customMUITextFieldStyles } from '@/app/(routes)/multiops/styles'; +import { TextField, InputAdornment } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import CustomAutoComplete from '../components/CustomAutoComplete'; +import useStaking from '@/custom-hooks/txn-builder/useStaking'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getDelegations } from '@/store/features/staking/stakeSlice'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { TxStatus } from '@/types/enums'; +import { formatCoin } from '@/utils/util'; +import { Decimal } from '@cosmjs/math'; +import FileUpload from '../components/FileUpload'; +import AddMsgButton from '../components/AddMsgButton'; + +interface ReDelegateProps { + chainID: string; + fromAddress: string; + onReDelegate: (payload: Msg) => void; + currency: Currency; + cancelAddMsg: () => void; +} + +const RedelegateForm = (props: ReDelegateProps) => { + const { fromAddress, chainID, currency, onReDelegate, cancelAddMsg } = props; + const dispatch = useAppDispatch(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { restURLs: baseURLs } = getChainInfo(chainID); + const { decimals: coinDecimals, displayDenom } = getDenomInfo(chainID); + const { getValidatorsForUndelegation, getValidators } = useStaking(); + const { delegatedValidators, delegationsData } = getValidatorsForUndelegation( + { chainID } + ); + const { validatorsList } = getValidators({ chainID }); + const [selectedOption, setSelectedOption] = useState( + null + ); + const [selectedDestValidator, setSelectedDestValidator] = + useState(null); + const [amountForUndelegation, setAmountForUndelegation] = useState< + { amount: string; denom: string } | undefined + >(); + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + + const { handleSubmit, control, setValue, reset } = useForm({ + defaultValues: { + amount: '', + validatorSrcAddress: '', + validatorDstAddress: '', + delegator: fromAddress, + }, + }); + + const [fileUploadTxns, setFileUploadTxns] = useState([]); + + const handleSrcValidatorChange = (option: ValidatorOption | null) => { + setValue('validatorSrcAddress', option?.address || ''); + setSelectedOption(option); + updateAmount(option?.address); + }; + + const handleDestValidatorChange = (option: ValidatorOption | null) => { + setValue('validatorDstAddress', option?.address || ''); + setSelectedDestValidator(option); + }; + + const updateAmount = (address: string | undefined) => { + if (address) { + const item = delegationsData.find( + (item) => item.validatorAddress === address + ); + setAmountForUndelegation({ + amount: (Number(item?.amount) / 10 ** coinDecimals).toFixed(6) || '', + denom: item?.denom || '', + }); + } else { + setAmountForUndelegation(undefined); + } + }; + + const onSubmit = (data: { + amount: string; + validatorSrcAddress: string; + validatorDstAddress: string; + delegator: string; + }) => { + if (data?.validatorSrcAddress && data?.validatorDstAddress) { + const baseAmount = Decimal.fromUserInput( + data.amount, + Number(currency?.coinDecimals) + ).atomics; + const msgReDelegate = { + delegatorAddress: data.delegator, + validatorSrcAddress: data.validatorSrcAddress, + validatorDstAddress: data.validatorDstAddress, + amount: { + amount: baseAmount, + denom: currency?.coinMinimalDenom, + }, + }; + + onReDelegate({ + typeUrl: '/cosmos.staking.v1beta1.MsgBeginRedelegate', + value: msgReDelegate, + }); + reset(); + handleSrcValidatorChange(null); + handleDestValidatorChange(null); + } + }; + + const handleAddMsgs = (msgs: Msg[]) => { + for (const msg of msgs) { + onReDelegate(msg); + } + }; + + const onAddFileUploadTxns = (msgs: Msg[]) => { + setFileUploadTxns(msgs); + }; + + const onRemoveFileUploadTxns = () => { + setFileUploadTxns([]); + }; + + useEffect(() => { + if (chainID) { + dispatch(getDelegations({ chainID, baseURLs, address: fromAddress })); + } + }, []); + + return ( +
    +
    +
    +
    +
    Redelegate
    + +
    +
    +
    +
    +
    Source Validator
    + +
    +
    +
    Destination Validator
    + +
    +
    +
    +
    +
    Enter Amount
    + ( + + {amountForUndelegation ? ( + + {'Available for Redelegation :'}{' '} + {formatCoin( + Number(amountForUndelegation?.amount), + displayDenom + )}{' '} + + ) : null} +
    + ), + }} + /> + )} + /> +
    +
    +
    +
    + +
    + + + ); +}; + +export default RedelegateForm; diff --git a/frontend/src/components/txn-builder/messages/SendForm.tsx b/frontend/src/components/txn-builder/messages/SendForm.tsx new file mode 100644 index 000000000..fe2513a05 --- /dev/null +++ b/frontend/src/components/txn-builder/messages/SendForm.tsx @@ -0,0 +1,269 @@ +import { customMUITextFieldStyles } from '@/app/(routes)/multiops/styles'; +import { InputAdornment, TextField, Select, MenuItem } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { Decimal } from '@cosmjs/math'; +import FileUpload from '../components/FileUpload'; +import AddMsgButton from '../components/AddMsgButton'; +import useGetAllAssets from '@/custom-hooks/multisig/useGetAllAssets'; +import { customSelectStyles } from '../styles'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; + +interface SendFormProps { + fromAddress: string; + onSend: (payload: Msg) => void; + currency: Currency; + availableBalance: number; + cancelAddMsg: () => void; + chainID: string; + isMultisig: boolean; +} + +const SendForm = (props: SendFormProps) => { + const { fromAddress, onSend, cancelAddMsg, chainID, isMultisig } = props; + const dispatch = useAppDispatch(); + const { getDenomInfo } = useGetChainInfo(); + const { displayDenom: nativeDisplayDenom } = getDenomInfo(chainID); + const { getAllAssets, getParsedAsset } = useGetAllAssets(); + const { allAssets } = getAllAssets(chainID, true, isMultisig); + const { + handleSubmit, + control, + reset, + formState: { errors }, + setValue, + getValues, + } = useForm({ + defaultValues: { + amount: '', + recipient: '', + from: fromAddress, + selectedAsset: '', + }, + }); + + const [fileUploadTxns, setFileUploadTxns] = useState([]); + + const onSubmit = (data: { + amount: string; + recipient: string; + from: string; + selectedAsset: string; + }) => { + const selectedAsset = allAssets.find( + (asset) => asset.displayDenom === data.selectedAsset + ); + if (!selectedAsset) return; + + const amountInAtomics = Decimal.fromUserInput( + data.amount, + selectedAsset.decimals + ).atomics; + + const msgSend = { + fromAddress: data.from, + toAddress: data.recipient, + amount: [ + { + amount: amountInAtomics, + denom: selectedAsset.ibcDenom, + }, + ], + }; + + const msg = { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: msgSend, + }; + + onSend(msg); + reset(); + }; + + const handleAddMsgs = (msgs: Msg[]) => { + for (const msg of msgs) { + const { assetInfo } = getParsedAsset({ + amount: msg.value?.amount?.[0]?.amount, + chainID, + denom: msg.value?.amount?.[0]?.denom, + }); + if (assetInfo) { + onSend(msg); + } else { + dispatch( + setError({ type: 'error', message: 'Encountered an invalid denom' }) + ); + } + } + }; + + const onAddFileUploadTxns = (msgs: Msg[]) => { + setFileUploadTxns(msgs); + }; + + const onRemoveFileUploadTxns = () => { + setFileUploadTxns([]); + }; + + useEffect(() => { + if (allAssets.length && !getValues('selectedAsset').length) { + const nativeAsset = allAssets.find( + (asset) => asset.displayDenom === nativeDisplayDenom + ); + if (nativeAsset) { + setValue('selectedAsset', nativeAsset?.displayDenom || ''); + } else { + setValue('selectedAsset', allAssets?.[0]?.displayDenom || ''); + } + } + }, [allAssets]); + + return ( +
    +
    +
    +
    +
    Send
    + +
    +
    +
    +
    Enter Address
    + ( + + )} + /> +
    +
    +
    Enter Amount
    + ( + + ( + + )} + /> + + ), + }} + /> + )} + /> +
    + {errors?.selectedAsset?.message} +
    +
    +
    +
    + +
    + + + ); +}; + +export default SendForm; diff --git a/frontend/src/components/txn-builder/messages/UndelegateForm.tsx b/frontend/src/components/txn-builder/messages/UndelegateForm.tsx new file mode 100644 index 000000000..ff748b979 --- /dev/null +++ b/frontend/src/components/txn-builder/messages/UndelegateForm.tsx @@ -0,0 +1,212 @@ +import { customMUITextFieldStyles } from '@/app/(routes)/multiops/styles'; +import { TextField, InputAdornment } from '@mui/material'; +import React, { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import CustomAutoComplete from '../components/CustomAutoComplete'; +import useStaking from '@/custom-hooks/txn-builder/useStaking'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import { getDelegations } from '@/store/features/staking/stakeSlice'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { TxStatus } from '@/types/enums'; +import { formatCoin } from '@/utils/util'; +import { Decimal } from '@cosmjs/math'; +import FileUpload from '../components/FileUpload'; +import AddMsgButton from '../components/AddMsgButton'; + +interface UnDelegateProps { + chainID: string; + fromAddress: string; + onUndelegate: (payload: Msg) => void; + currency: Currency; + cancelAddMsg: () => void; +} + +const UndelegateForm = (props: UnDelegateProps) => { + const { fromAddress, chainID, currency, onUndelegate, cancelAddMsg } = props; + const dispatch = useAppDispatch(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { restURLs: baseURLs } = getChainInfo(chainID); + const { decimals: coinDecimals, displayDenom } = getDenomInfo(chainID); + const { getValidatorsForUndelegation } = useStaking(); + const { delegatedValidators, delegationsData } = getValidatorsForUndelegation( + { chainID } + ); + const [selectedOption, setSelectedOption] = useState( + null + ); + const [amountForUndelegation, setAmountForUndelegation] = useState< + { amount: string; denom: string } | undefined + >(); + const validatorsLoading = useAppSelector( + (state) => state.staking.chains?.[chainID]?.validators.status + ); + const { handleSubmit, control, setValue, reset } = useForm({ + defaultValues: { + amount: '', + validator: '', + delegator: fromAddress, + }, + }); + + const [fileUploadTxns, setFileUploadTxns] = useState([]); + + const handleValidatorChange = (option: ValidatorOption | null) => { + setValue('validator', option?.address || ''); + setSelectedOption(option); + updateAmount(option?.address); + }; + + const updateAmount = (address: string | undefined) => { + if (address) { + const item = delegationsData.find( + (item) => item.validatorAddress === address + ); + setAmountForUndelegation({ + amount: (Number(item?.amount) / 10 ** coinDecimals).toFixed(6) || '', + denom: item?.denom || '', + }); + } else { + setAmountForUndelegation(undefined); + } + }; + + const onSubmit = (data: { + amount: string; + validator: string; + delegator: string; + }) => { + if (data.validator) { + const baseAmount = Decimal.fromUserInput( + data.amount, + Number(currency?.coinDecimals) + ).atomics; + const msgUnDelegate = { + delegatorAddress: data.delegator, + validatorAddress: data.validator, + amount: { + amount: baseAmount, + denom: currency?.coinMinimalDenom, + }, + }; + + onUndelegate({ + typeUrl: '/cosmos.staking.v1beta1.MsgUndelegate', + value: msgUnDelegate, + }); + reset(); + handleValidatorChange(null); + } + }; + + const handleAddMsgs = (msgs: Msg[]) => { + for (const msg of msgs) { + onUndelegate(msg); + } + }; + + const onAddFileUploadTxns = (msgs: Msg[]) => { + setFileUploadTxns(msgs); + }; + + const onRemoveFileUploadTxns = () => { + setFileUploadTxns([]); + }; + + useEffect(() => { + if (chainID) { + dispatch(getDelegations({ chainID, baseURLs, address: fromAddress })); + } + }, []); + + return ( +
    +
    +
    +
    +
    Undelegate
    + +
    +
    +
    +
    Select Validator
    + +
    +
    +
    Amount
    + ( + + {amountForUndelegation ? ( + + {'Staked :'}{' '} + {formatCoin( + Number(amountForUndelegation?.amount), + displayDenom + )}{' '} + + ) : null} +
    + ), + }} + /> + )} + /> +
    +
    +
    + +
    + + + ); +}; + +export default UndelegateForm; diff --git a/frontend/src/components/txn-builder/messages/VoteForm.tsx b/frontend/src/components/txn-builder/messages/VoteForm.tsx new file mode 100644 index 000000000..f49ed967b --- /dev/null +++ b/frontend/src/components/txn-builder/messages/VoteForm.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useAppDispatch } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getProposalsInVoting } from '@/store/features/gov/govSlice'; +import ProposalsList from '../components/ProposalsList'; +import useGov from '@/custom-hooks/txn-builder/useGov'; +import VoteOptionsList from '../components/VoteOptionsList'; +import { msgVoteTypeUrl } from '@/txns/gov/vote'; +import FileUpload from '../components/FileUpload'; +import AddMsgButton from '../components/AddMsgButton'; + +interface VoteProps { + fromAddress: string; + onVote: (payload: Msg) => void; + chainID: string; + cancelAddMsg: () => void; +} + +const VoteForm = (props: VoteProps) => { + const { fromAddress, onVote, chainID, cancelAddMsg } = props; + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { baseURL, govV1, restURLs: baseURLs } = getChainInfo(chainID); + const { getActiveProposals } = useGov(); + const { activeProposalsList, proposalsLoading } = getActiveProposals({ + chainID, + }); + const [selectedOption, setSelectedOption] = useState( + null + ); + const [selectedVoteOption, setSelectedVoteOption] = + useState(null); + + const { handleSubmit, setValue, reset } = useForm({ + defaultValues: { + proposalID: '', + voteOption: '', + from: fromAddress, + }, + }); + + const [fileUploadTxns, setFileUploadTxns] = useState([]); + + const handleProposalChange = (option: ProposalOption | null) => { + setValue('proposalID', option?.value || ''); + setSelectedOption(option); + }; + const handleVoteChange = (option: VoteOption | null) => { + setValue('voteOption', option?.value?.toString() || ''); + setSelectedVoteOption(option); + }; + + const onSubmit = (data: { + proposalID: string; + voteOption: string; + from: string; + }) => { + const msgVote = { + voter: data.from, + option: Number(data.voteOption), + proposalId: Number(data.proposalID), + }; + + const msg = { + typeUrl: msgVoteTypeUrl, + value: msgVote, + }; + + onVote(msg); + reset(); + handleProposalChange(null); + handleVoteChange(null); + }; + + const handleAddMsgs = (msgs: Msg[]) => { + for (const msg of msgs) { + onVote(msg); + } + }; + + const onAddFileUploadTxns = (msgs: Msg[]) => { + setFileUploadTxns(msgs); + }; + + const onRemoveFileUploadTxns = () => { + setFileUploadTxns([]); + }; + + useEffect(() => { + if (chainID) { + dispatch( + getProposalsInVoting({ chainID, baseURL, govV1, baseURLs, voter: '' }) + ); + } + }, []); + + return ( +
    +
    +
    +
    +
    Vote
    + +
    +
    +
    +
    Select Proposal
    + +
    +
    +
    Select Vote
    + +
    +
    +
    + +
    + + + ); +}; + +export default VoteForm; diff --git a/frontend/src/components/txn-builder/styles.ts b/frontend/src/components/txn-builder/styles.ts new file mode 100644 index 000000000..7e455bf36 --- /dev/null +++ b/frontend/src/components/txn-builder/styles.ts @@ -0,0 +1,22 @@ +export const customSelectStyles = { + '& .MuiOutlinedInput-input': { + color: '#ffffff80', + }, + '& .MuiOutlinedInput-root': { + padding: '0px !important', + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: '#ffffff80', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none !important', + }, + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + marginRight: '-14px', + width: '170px', +}; diff --git a/frontend/src/components/txn-status-popups/DialogTxnStatus.tsx b/frontend/src/components/txn-status-popups/DialogTxnStatus.tsx new file mode 100644 index 000000000..4efbfd40f --- /dev/null +++ b/frontend/src/components/txn-status-popups/DialogTxnStatus.tsx @@ -0,0 +1,46 @@ +import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const DialogTxnStatus = ({ + handleClose, + open, + children, +}: { + open: boolean; + handleClose: () => void; + children: React.ReactNode; +}) => { + return open ? ( + + +
    + +
    + {children} +
    +
    +
    +
    + ) : null; +}; + +export default DialogTxnStatus; diff --git a/frontend/src/components/txn-status-popups/ShareTxn.tsx b/frontend/src/components/txn-status-popups/ShareTxn.tsx new file mode 100644 index 000000000..080de61f8 --- /dev/null +++ b/frontend/src/components/txn-status-popups/ShareTxn.tsx @@ -0,0 +1,41 @@ +import { copyToClipboard } from '@/utils/copyToClipboard'; +import { Tooltip } from '@mui/material'; +import Image from 'next/image'; +import React, { useEffect, useState } from 'react'; + +const ShareTxn = ({ content }: { content: string }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = (e: React.MouseEvent) => { + copyToClipboard(content); + setCopied(true); + e.stopPropagation(); + }; + + useEffect(() => { + let timer: NodeJS.Timeout; + if (copied) { + timer = setTimeout(() => { + setCopied(false); + }, 2000); + } + return () => clearTimeout(timer); + }, [copied]); + + return ( +
    + + copy + +
    + ); +}; + +export default ShareTxn; diff --git a/frontend/src/components/txn-status-popups/TransactionStatusPopup.tsx b/frontend/src/components/txn-status-popups/TransactionStatusPopup.tsx new file mode 100644 index 000000000..da8ef5067 --- /dev/null +++ b/frontend/src/components/txn-status-popups/TransactionStatusPopup.tsx @@ -0,0 +1,189 @@ +import { + REDIRECT_ICON, + TXN_FAILED_ICON, + TXN_SUCCESS_ICON, +} from '@/constants/image-names'; +import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; +import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; +import { getRecentTransactions } from '@/store/features/recent-transactions/recentTransactionsSlice'; +import { dialogBoxPaperPropStyles } from '@/utils/commonStyles'; +import { IBC_SEND_TYPE_URL, SEND_TYPE_URL } from '@/utils/constants'; +import { getTxnURL, getTxnURLOnResolute, shortenMsg } from '@/utils/util'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import Link from 'next/link'; +import React, { useEffect, useMemo, useState } from 'react'; +import TxnMessage from '../TxnMessage'; +import { parseBalance } from '@/utils/denom'; +import Copy from '../common/Copy'; +import ShareTxn from './ShareTxn'; + +const TransactionStatusPopup = () => { + const tx = useAppSelector((state) => state.common.txSuccess.tx); + const selectedNetwork = useAppSelector( + (state) => state.common.selectedNetwork.chainName + ); + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const { getAllChainAddresses } = useGetChainInfo(); + + const [isOpen, setIsOpen] = useState(false); + const dispatch = useAppDispatch(); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { explorerTxHashEndpoint = '', chainName = '' } = tx?.chainID + ? getChainInfo(tx.chainID) + : {}; + const { + decimals = 0, + displayDenom = '', + minimalDenom = '', + } = tx?.chainID ? getDenomInfo(tx?.chainID) : {}; + const currency = useMemo( + () => ({ + coinMinimalDenom: minimalDenom, + coinDecimals: decimals, + coinDenom: displayDenom, + }), + [minimalDenom, decimals, displayDenom] + ); + + const handleClose = () => { + setIsOpen(false); + }; + + useEffect(() => { + if (tx) { + setIsOpen(true); + } else { + setIsOpen(false); + } + }, [tx]); + + useEffect(() => { + if (tx?.msgs) { + const chainIDs = selectedNetwork?.length + ? [nameToChainIDs[selectedNetwork]] + : Object.values(nameToChainIDs); + if (tx?.msgs[0]?.typeUrl === SEND_TYPE_URL) { + dispatch( + getRecentTransactions({ + addresses: getAllChainAddresses(chainIDs), + module: 'bank', + }) + ); + } else if (!(tx?.msgs[0]?.typeUrl === IBC_SEND_TYPE_URL)) { + dispatch( + getRecentTransactions({ + addresses: getAllChainAddresses(chainIDs), + module: 'all', + }) + ); + } + } + }, [tx]); + + return isOpen ? ( + + +
    + +
    +
    + Transaction Successful +
    +
    +
    +
    + {tx?.code === 0 ? ( + Transaction Successful + ) : ( + Transaction Failed + )} +
    + + + + +
    +
    +
    +
    + +
    +
    +
    Txn Hash
    +
    +
    + {shortenMsg(tx?.transactionHash || '', 20) || '-'} +
    + {tx?.transactionHash ? ( + + ) : null} +
    +
    +
    +
    +
    Fees
    +
    + {tx?.fee?.[0] + ? parseBalance( + tx?.fee, + currency.coinDecimals, + currency.coinMinimalDenom + ) + : '-'}{' '} + {currency.coinDenom} +
    +
    +
    +
    Txn Messages
    +
    #{tx?.msgs?.length}
    +
    +
    +
    +
    +
    +
    +
    +
    + ) : null; +}; + +export default TransactionStatusPopup; diff --git a/frontend/src/components/txn-status-popups/TxnInfoCard.tsx b/frontend/src/components/txn-status-popups/TxnInfoCard.tsx new file mode 100644 index 000000000..ed9b55f07 --- /dev/null +++ b/frontend/src/components/txn-status-popups/TxnInfoCard.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const TxnInfoCard = ({ + children, + name, +}: { + name: string; + children: React.ReactNode; +}) => { + return ( +
    +
    {name}
    +
    {children}
    +
    + ); +}; + +export default TxnInfoCard; diff --git a/frontend/src/components/txn-status-popups/TxnStatus.tsx b/frontend/src/components/txn-status-popups/TxnStatus.tsx new file mode 100644 index 000000000..18e18d411 --- /dev/null +++ b/frontend/src/components/txn-status-popups/TxnStatus.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import ShareTxn from './ShareTxn'; +import { getTxnURL, getTxnURLOnResolute } from '@/utils/util'; +import Link from 'next/link'; +import Image from 'next/image'; +import { REDIRECT_ICON } from '@/constants/image-names'; + +const TxnStatus = ({ + explorer, + txHash, + txSuccess, + chainName, +}: { + txSuccess: boolean; + explorer: string; + txHash: string; + chainName: string; +}) => { + return ( +
    +
    + {txSuccess ? ( + Transaction Successful + ) : ( + Transaction Failed + )} +
    + + + + +
    + ); +}; + +export default TxnStatus; diff --git a/frontend/src/constants/gov-constants.ts b/frontend/src/constants/gov-constants.ts new file mode 100644 index 000000000..02ba8719e --- /dev/null +++ b/frontend/src/constants/gov-constants.ts @@ -0,0 +1,28 @@ +interface VoteOption { + label: string; + value: number; + selectedColor: string; +} + +export const GOV_VOTE_OPTIONS: VoteOption[] = [ + { + label: 'Yes', + value: 1, + selectedColor: '#2BA472', + }, + { + label: 'No', + value: 3, + selectedColor: '#D92101', + }, + { + label: 'Abstain', + value: 2, + selectedColor: '#FFC13C', + }, + { + label: 'Veto', + value: 4, + selectedColor: '#DA561E', + }, +]; diff --git a/frontend/src/constants/image-names.ts b/frontend/src/constants/image-names.ts new file mode 100644 index 000000000..4605ef170 --- /dev/null +++ b/frontend/src/constants/image-names.ts @@ -0,0 +1,46 @@ +export const EMPTY_ILLUSTRATION = '/illustrations/empty-illustration.png'; +export const DROP_DOWN_OPEN = '/icons/styled-drop-down-icon-open.svg'; +export const DROP_DOWN_CLOSE = '/icons/styled-drop-down-icon-close.svg'; +export const MENU_ICON = '/icons/menu-icon.svg'; +export const VERIFY_ILLUSTRATION = '/illustrations/verify-illustration.png'; +export const TXN_SUCCESS_ICON = '/icons/success-icon.png'; +export const TXN_FAILED_ICON = '/icons/failed-icon.png'; +export const SHARE_ICON = '/icons/share-icon.svg'; +export const REDIRECT_ICON = '/icons/redirect-icon.svg'; +export const REDIRECT_ICON_GREEN = '/icons/redirect-icon-green.svg'; +export const REDIRECT_ICON_RED = '/icons/redirect-icon-red.svg'; +export const DELETE_ILLUSTRATION = '/illustrations/delete.png'; +export const ADD_ICON = '/icons/add-icon.svg'; +export const REMOVE_ICON = '/icons/remove-icon.svg'; +export const MINUS_ICON = '/icons/minus-icon.svg'; +export const PLUS_ICON = '/icons/plus-icon.svg'; +export const MINUS_ICON_DISABLED = '/icons/minus-icon-disabled.svg'; +export const PLUS_ICON_DISABLED = '/icons/plus-icon-disabled.svg'; +export const TOGGLE_OFF = '/icons/toggle-off.svg'; +export const TOGGLE_ON = '/icons/toggle-on.svg'; +export const I_ICON = '/icons/i-icon.svg'; +export const TIMER_ICON = '/icons/timer-icon.svg'; +export const SEARCH_ICON = '/icons/search-icon.svg'; +export const FLIP_ICON = '/icons/flip-icon.svg'; +export const ROUTE_ICON = '/icons/route-icon.svg'; +export const SETTINGS_ICON = '/icons/settings-icon.svg'; +export const NO_DATA_ILLUSTRATION = '/illustrations/no-data-illustration.png'; +export const NO_MESSAGES_ILLUSTRATION = + '/illustrations/no-messages-illustration.png'; +export const LOGOUT_ICON = '/icons/logout.png'; +export const ALERT_ICON = '/icons/alert-icon.svg'; +export const TIMER_ICON_YELLOW = 'timer-icon.svg'; +export const UPLOAD_ICON = '/icons/upload-icon.svg'; +export const REPEAT_ICON = '/icons/repeat-icon.png'; +export const ROCKET_LAUNCH_GIF = '/rocket-launch.gif'; +export const SWAP_ROUTE_ICON = '/icons/swap-route.svg'; +export const SWAP_ICON_FILLED = '/icons/swap-icon-filled.svg'; +export const GLOBE_ICON = '/icons/globe-icon.svg'; +export const CHECK_ICON_FILLED = '/icons/check-filled.svg'; +export const CROSS_ICON = '/icons/cross-icon.svg'; +export const RESOLUTE_LOGO = '/vitwit-logo.svg'; +export const TICK_ICON = '/icons/tick-icon.svg'; +export const DROP_DOWN_ICON_FILLED = '/icons/drop-down-arrow-filled.svg'; +export const REMOVE_ICON_OUTLINED = '/icons/remove-icon-outlined.svg'; +export const ADD_ICON_ROUNDED = '/icons/add-icon-rounded.svg'; +export const CANCEL_ICON_SOLID = '/icons/cancel-icon-solid.svg'; diff --git a/frontend/src/constants/sidebar-options.ts b/frontend/src/constants/sidebar-options.ts new file mode 100644 index 000000000..28088d12d --- /dev/null +++ b/frontend/src/constants/sidebar-options.ts @@ -0,0 +1,75 @@ +export interface MenuItemI { + name: string; + icon: string; + path: string; + authzSupported: boolean; + isMetaMaskSupported: boolean; + multipleOptions: boolean; +} + +export const SIDEBAR_MENU_OPTIONS: MenuItemI[] = [ + { + name: 'Dashboard', + icon: '/sidebar-menu-icons/dashboard-icon.svg', + path: '/', + authzSupported: true, + isMetaMaskSupported: true, + multipleOptions: false, + }, + { + name: 'Staking', + icon: '/sidebar-menu-icons/staking-icon.svg', + path: '/staking', + authzSupported: true, + isMetaMaskSupported: true, + multipleOptions: false, + }, + { + name: 'Governance', + icon: '/sidebar-menu-icons/gov-icon.svg', + path: '/governance', + authzSupported: true, + isMetaMaskSupported: true, + multipleOptions: false, + }, + { + name: 'Transfers', + icon: '/sidebar-menu-icons/transfers-icon.svg', + path: '/transfers', + authzSupported: true, + isMetaMaskSupported: true, + multipleOptions: true, + }, + { + name: 'MultiSig', + icon: '/sidebar-menu-icons/multisig-icon.svg', + path: '/multisig', + authzSupported: false, + isMetaMaskSupported: false, + multipleOptions: false, + }, + { + name: 'Transactions', + icon: '/sidebar-menu-icons/txn-history-icon.svg', + path: '/transactions/history', + authzSupported: false, + isMetaMaskSupported: true, + multipleOptions: true, + }, + { + name: 'Cosmwasm', + icon: '/sidebar-menu-icons/smart-contracts-icon.svg', + path: '/cosmwasm', + authzSupported: false, + isMetaMaskSupported: false, + multipleOptions: true, + }, + { + name: 'Settings', + icon: '/sidebar-menu-icons/settings-icon.svg', + path: '/settings', + authzSupported: true, + isMetaMaskSupported: false, + multipleOptions: true, + }, +]; diff --git a/frontend/src/constants/txn-builder.ts b/frontend/src/constants/txn-builder.ts new file mode 100644 index 000000000..bb05486f9 --- /dev/null +++ b/frontend/src/constants/txn-builder.ts @@ -0,0 +1,21 @@ +export const TXN_BUILDER_MSGS: TxnMsgType[] = [ + 'Send', + 'Delegate', + 'Undelegate', + 'Redelegate', + 'Vote', + 'Custom', +]; + +export const CUSTOM_MSG_VALUE_PLACEHOLDER = `Eg: +{\n "fromAddress": "cosmos1e9yazjmsmjsqftsvkqv3hhfkqd45sk53uy7c3c",\n "toAddress": "cosmos1480rvurtf360fugxt76ny8hrydzrlxm9gcvtgl",\n "amount": [\n {\n "amount": "1",\n "denom": "uatom"\n }\n ]\n} +`; + +export const DEFAULT_MESSAGES_COUNT = { + Send: 0, + Delegate: 0, + Redelegate: 0, + Undelegate: 0, + Vote: 0, + Custom: 0, +}; diff --git a/frontend/src/constants/wallet.ts b/frontend/src/constants/wallet.ts new file mode 100644 index 000000000..87307e8c1 --- /dev/null +++ b/frontend/src/constants/wallet.ts @@ -0,0 +1,3 @@ +export const KEPLR = 'keplr'; +export const LEAP = 'leap'; +export const COSMOSTATION = 'cosmostation'; \ No newline at end of file diff --git a/frontend/src/custom-hooks/common/useInitApp.ts b/frontend/src/custom-hooks/common/useInitApp.ts new file mode 100644 index 000000000..15195fdcd --- /dev/null +++ b/frontend/src/custom-hooks/common/useInitApp.ts @@ -0,0 +1,149 @@ +import { useEffect, useRef } from 'react'; +import { RootState } from '@/store/store'; +import { + getAllValidators, + getAuthzDelegations, + getAuthzUnbonding, + getDelegations, + getUnbonding, +} from '@/store/features/staking/stakeSlice'; +import { + getAuthzDelegatorTotalRewards, + getDelegatorTotalRewards, +} from '@/store/features/distribution/distributionSlice'; +import { getAuthzBalances, getBalances } from '@/store/features/bank/bankSlice'; +import { useAppDispatch, useAppSelector } from '../StateHooks'; +import useAddressConverter from '../useAddressConverter'; +import useGetChainInfo from '../useGetChainInfo'; +import useFetchPriceInfo from '../useFetchPriceInfo'; +import useInitFeegrant from '../useInitFeegrant'; +import useInitAuthz from '../useInitAuthz'; +import { getAuthzAlertData, isAuthzAlertDataSet } from '@/utils/localStorage'; + +const fetchAuthz = (isAuthzMode: boolean): boolean => { + if (isAuthzMode) { + return true; + } + if ( + !isAuthzMode && + ((isAuthzAlertDataSet() && getAuthzAlertData()) || !isAuthzAlertDataSet()) + ) { + return true; + } + return false; +}; + +/* eslint-disable react-hooks/rules-of-hooks */ +const useInitApp = () => { + const dispatch = useAppDispatch(); + const { convertAddress } = useAddressConverter(); + + const isFeegrantModeEnabled = useAppSelector( + (state) => state.feegrant.feegrantModeEnabled + ); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); + const nameToChainIDs = useAppSelector( + (state: RootState) => state.wallet.nameToChainIDs + ); + const chainIDs = Object.values(nameToChainIDs); + + const walletState = useAppSelector((state) => state.wallet); + const isWalletConnected = useAppSelector( + (state: RootState) => state.wallet.connected + ); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + + const fetchedChains = useRef<{ [key: string]: boolean }>({}); + const validatorsFetchedChains = useRef<{ [key: string]: boolean }>({}); + + useEffect(() => { + if (chainIDs.length > 0 && isWalletConnected) { + chainIDs.forEach((chainID) => { + if (!fetchedChains.current[chainID]) { + const { address, baseURL, restURLs } = getChainInfo(chainID); + + if (isWalletConnected && address.length) { + const authzGranterAddress = convertAddress(chainID, authzAddress); + const { minimalDenom } = getDenomInfo(chainID); + const chainRequestData = { + baseURLs: restURLs, + address: isAuthzMode ? authzGranterAddress : address, + chainID, + }; + + // Fetch delegations + dispatch( + isAuthzMode + ? getAuthzDelegations(chainRequestData) + : getDelegations(chainRequestData) + ); + + // Fetch available balances + dispatch( + isAuthzMode + ? getAuthzBalances({ ...chainRequestData, baseURL }) + : getBalances({ ...chainRequestData, baseURL }) + ); + + // Fetch rewards + dispatch( + isAuthzMode + ? getAuthzDelegatorTotalRewards({ + ...chainRequestData, + baseURL, + denom: minimalDenom, + }) + : getDelegatorTotalRewards({ + ...chainRequestData, + baseURL, + denom: minimalDenom, + }) + ); + + // Fetch unbonding delegations + dispatch( + isAuthzMode + ? getAuthzUnbonding(chainRequestData) + : getUnbonding(chainRequestData) + ); + + // Mark chain as fetched + fetchedChains.current[chainID] = true; + } + } + }); + } + }, [ + isWalletConnected, + isAuthzMode, + chainIDs, + getChainInfo, + convertAddress, + getDenomInfo, + authzAddress, + dispatch, + walletState, + ]); + + useEffect(() => { + if (chainIDs.length > 0) { + chainIDs.forEach((chainID) => { + const { restURLs } = getChainInfo(chainID); + if (restURLs?.length && !validatorsFetchedChains.current[chainID]) { + // Fetch validators + dispatch(getAllValidators({ baseURLs: restURLs, chainID })); + + // Mark chain as fetched + validatorsFetchedChains.current[chainID] = true; + } + }); + } + }, [chainIDs, walletState]); + + useFetchPriceInfo(); + useInitFeegrant({ chainIDs, shouldFetch: isFeegrantModeEnabled }); + useInitAuthz({ chainIDs, shouldFetch: fetchAuthz(isAuthzMode) }); +}; + +export default useInitApp; diff --git a/frontend/src/custom-hooks/governance/useGetProposals.tsx b/frontend/src/custom-hooks/governance/useGetProposals.tsx new file mode 100644 index 000000000..1660d6c3b --- /dev/null +++ b/frontend/src/custom-hooks/governance/useGetProposals.tsx @@ -0,0 +1,206 @@ +import { useAppSelector } from '../StateHooks'; +import useGetChainInfo from '../useGetChainInfo'; +import { get } from 'lodash'; +import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; +import { ProposalsData } from '@/types/gov'; +import { voteOptions } from '@/utils/constants'; + +interface ProposalOverview extends ProposalsData { + proposalInfo: ProposalsData['proposalInfo'] & { + proposalDescription: string; + }; +} + +const useGetProposals = () => { + const { getChainInfo } = useGetChainInfo(); + const govState = useAppSelector((state) => state.gov.chains); + + const getProposals = ({ + chainIDs, + showAll = false, + deposits = false, + }: { + chainIDs: string[]; + showAll?: boolean; + deposits?: boolean; + }) => { + const proposalsData: ProposalsData[] = []; + chainIDs.forEach((chainID) => { + const { chainLogo, chainName } = getChainInfo(chainID); + const activeProposals = govState?.[chainID]?.active?.proposals || []; + const depositProposals = govState?.[chainID]?.deposit?.proposals || []; + + if (!deposits && Array.isArray(activeProposals)) { + activeProposals?.forEach((proposal) => { + const proposalTitle = get( + proposal, + 'content.title', + get(proposal, 'title', get(proposal, 'content.@type', '')) + ); + + const endTime = getTimeDifferenceToFutureDate( + get(proposal, 'voting_end_time') + ); + const proposalId = get( + proposal, + 'proposal_id', + get(proposal, 'id', '') + ); + proposalsData.push({ + chainID, + chainName, + chainLogo, + isActive: true, + proposalInfo: { + proposalTitle, + proposalId, + endTime, + }, + }); + }); + } + if (showAll) { + if (Array.isArray(depositProposals)) { + depositProposals?.forEach((proposal) => { + const proposalTitle = get( + proposal, + 'content.title', + get(proposal, 'title', get(proposal, 'content.@type', '')) + ); + const endTime = getTimeDifferenceToFutureDate( + get(proposal, 'deposit_end_time') + ); + const proposalId = get( + proposal, + 'proposal_id', + get(proposal, 'id', '') + ); + proposalsData.push({ + chainID, + chainName, + chainLogo, + isActive: false, + proposalInfo: { + endTime, + proposalId, + proposalTitle, + }, + }); + }); + } + } + + if (deposits) { + if (Array.isArray(depositProposals)) { + depositProposals?.forEach((proposal) => { + const proposalTitle = get( + proposal, + 'content.title', + get(proposal, 'title', get(proposal, 'content.@type', '')) + ); + const endTime = getTimeDifferenceToFutureDate( + get(proposal, 'deposit_end_time') + ); + const proposalId = get( + proposal, + 'proposal_id', + get(proposal, 'id', '') + ); + proposalsData.push({ + chainID, + chainName, + chainLogo, + isActive: false, + proposalInfo: { + endTime, + proposalId, + proposalTitle, + }, + }); + }); + } + } + }); + return proposalsData; + }; + + const getProposalOverview = ({ + chainID, + proposalId, + isActive, + }: { + chainID: string; + proposalId: string; + isActive: boolean; + }): ProposalOverview => { + const { chainLogo, chainName } = getChainInfo(chainID); + const activeProposals = govState?.[chainID]?.active?.proposals; + const depositProposals = govState?.[chainID]?.deposit?.proposals; + const proposal = isActive + ? activeProposals?.find( + (proposal) => + get(proposal, 'proposal_id', get(proposal, 'id', '')) === proposalId + ) + : depositProposals?.find( + (proposal) => + get(proposal, 'proposal_id', get(proposal, 'id', '')) === proposalId + ); + const proposalTitle = get( + proposal, + 'content.title', + get(proposal, 'title', get(proposal, 'content.@type', '-')) + ); + const endTime = getTimeDifferenceToFutureDate( + get(proposal, isActive ? 'voting_end_time' : 'deposit_end_time', '-') + ); + const proposalDescription = get( + proposal, + 'content.description', + get(proposal, 'summary', '') + ); + + return { + chainID, + chainLogo, + chainName, + isActive, + proposalInfo: { + endTime, + proposalId, + proposalTitle, + proposalDescription, + }, + }; + }; + + const getVote = ({ + proposalId, + address, + chainID, + }: { + proposalId: string; + address: string; + chainID: string; + }) => { + const voteData = govState?.[chainID]?.votes?.proposals?.[proposalId]?.vote; + if (voteData) { + const voter = voteData?.voter || ''; + const option = voteData?.option || ''; + if (address?.length && voter?.length && address === voter) { + if (option?.length && option !== 'VOTE_OPTION_UNSPECIFIED') { + const votedOption = voteOptions?.[option] || ''; + return votedOption; + } else { + const votedOption = voteOptions?.[voteData?.options?.[0]?.option] || ''; + return votedOption; + } + } + } + + return ''; + }; + + return { getProposals, getProposalOverview, getVote }; +}; + +export default useGetProposals; diff --git a/frontend/src/custom-hooks/governance/useInitGovernance.ts b/frontend/src/custom-hooks/governance/useInitGovernance.ts new file mode 100644 index 000000000..846293167 --- /dev/null +++ b/frontend/src/custom-hooks/governance/useInitGovernance.ts @@ -0,0 +1,47 @@ +import { useEffect, useRef } from 'react'; +import { useAppDispatch, useAppSelector } from '../StateHooks'; +import useGetChainInfo from '../useGetChainInfo'; +import { + getProposalsInDeposit, + getProposalsInVoting, +} from '@/store/features/gov/govSlice'; + +const useInitGovernance = ({ chainIDs }: { chainIDs: string[] }) => { + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const walletState = useAppSelector((state) => state.wallet.status); + const fetchedChains = useRef<{ [key: string]: boolean }>({}); + + useEffect(() => { + chainIDs.forEach((chainID) => { + if (!fetchedChains.current[chainID]) { + const { + address, + baseURL, + restURLs: baseURLs, + govV1, + } = getChainInfo(chainID); + dispatch( + getProposalsInVoting({ + baseURL, + baseURLs, + chainID, + govV1, + voter: address, + }) + ); + dispatch( + getProposalsInDeposit({ + baseURL, + baseURLs, + chainID, + govV1, + }) + ); + fetchedChains.current[chainID] = true; + } + }); + }, [chainIDs, walletState]); +}; + +export default useInitGovernance; diff --git a/frontend/src/custom-hooks/multisig/useFetchTxns.ts b/frontend/src/custom-hooks/multisig/useFetchTxns.ts new file mode 100644 index 000000000..8889f3bc5 --- /dev/null +++ b/frontend/src/custom-hooks/multisig/useFetchTxns.ts @@ -0,0 +1,65 @@ +import { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../StateHooks'; +import { TxStatus } from '@/types/enums'; +import { setError } from '@/store/features/common/commonSlice'; +import { + resetBroadcastTxnRes, + resetsignTransactionRes, + resetSignTxnState, + resetUpdateTxnState, +} from '@/store/features/multisig/multisigSlice'; + +// To refetch txns after singing or broadcasting txn +const useFetchTxns = () => { + const dispatch = useAppDispatch(); + const signTxStatus = useAppSelector( + (state) => state.multisig.signTransactionRes + ); + const broadcastTxnStatus = useAppSelector( + (state) => state.multisig.broadcastTxnRes + ); + + const resetSignTxn = () => { + dispatch(resetSignTxnState()); + dispatch(resetsignTransactionRes()); + }; + + const resetBroadcastTxn = () => { + dispatch(resetUpdateTxnState()); + dispatch(resetBroadcastTxnRes()); + }; + + useEffect(() => { + if (signTxStatus.status === TxStatus.IDLE) { + dispatch(setError({ type: 'success', message: 'Successfully signed' })); + resetSignTxn(); + } else if (signTxStatus.status === TxStatus.REJECTED) { + dispatch( + setError({ + type: 'error', + message: signTxStatus.error || 'Error while signing the transaction', + }) + ); + resetSignTxn(); + } + }, [signTxStatus]); + + useEffect(() => { + if (broadcastTxnStatus.status === TxStatus.IDLE) { + dispatch( + setError({ type: 'success', message: 'Broadcasted successfully' }) + ); + resetBroadcastTxn(); + } else if (broadcastTxnStatus.status === TxStatus.REJECTED) { + dispatch( + setError({ + type: 'error', + message: broadcastTxnStatus.error || 'Failed to broadcasted', + }) + ); + resetBroadcastTxn(); + } + }, [broadcastTxnStatus]); +}; + +export default useFetchTxns; diff --git a/frontend/src/custom-hooks/multisig/useGetAllAssets.ts b/frontend/src/custom-hooks/multisig/useGetAllAssets.ts new file mode 100644 index 000000000..16d124b92 --- /dev/null +++ b/frontend/src/custom-hooks/multisig/useGetAllAssets.ts @@ -0,0 +1,128 @@ +import { useAppSelector } from '../StateHooks'; +import useGetChainInfo from '../useGetChainInfo'; +import chainDenoms from '@/utils/chainDenoms.json'; +import { parseBalance } from '@/utils/denom'; + +interface MultisigAsset { + amount: number; + displayDenom: string; + minimalDenom: string; + decimals: number; + amountInDenom: number; + ibcDenom: string; +} + +const chainDenomsData = chainDenoms as AssetData; + +const useGetAllAssets = () => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + + const multisigBalances = useAppSelector( + (state) => state.multisig.balance.balance + ); + const accountBalances = useAppSelector((state) => state.bank.balances); + + const getAllAssets = ( + chainID: string, + includeNative: boolean, + isMultisig: boolean + ) => { + const { chainName } = getChainInfo(chainID); + const { + minimalDenom: nativeMinimalDenom, + displayDenom, + decimals: nativeDecimals, + } = getDenomInfo(chainID); + const allAssets: MultisigAsset[] = []; + const balances = isMultisig + ? multisigBalances + : accountBalances?.[chainID].list; + balances.forEach((balance) => { + const denomInfo = chainDenomsData[chainName.toLowerCase()]?.filter( + (denomInfo) => { + return denomInfo.denom === balance.denom; + } + ); + const isNativeDenom = balance.denom === nativeMinimalDenom; + if (!isNativeDenom && !denomInfo?.length) { + return; + } + const assetData = { + symbol: isNativeDenom ? displayDenom : denomInfo?.[0].symbol, + decimals: isNativeDenom ? nativeDecimals : denomInfo?.[0].decimals, + originDenom: isNativeDenom ? nativeMinimalDenom : denomInfo?.[0].denom, + ibcDenom: isNativeDenom ? nativeMinimalDenom : denomInfo?.[0].denom, + }; + if ( + assetData.symbol && + assetData.decimals && + assetData.originDenom && + assetData.ibcDenom + ) { + const assetInfo = { + amount: Number(balance.amount), + amountInDenom: parseBalance( + [{ amount: balance.amount, denom: assetData.originDenom }], + assetData.decimals, + assetData.originDenom + ), + decimals: assetData.decimals, + displayDenom: assetData.symbol, + minimalDenom: assetData.originDenom, + ibcDenom: assetData.ibcDenom, + }; + if (includeNative || nativeMinimalDenom !== assetData.originDenom) { + allAssets.push(assetInfo); + } + } + }); + return { allAssets }; + }; + + const getParsedAsset = ({ + amount, + chainID, + denom, + }: { + chainID: string; + amount: string; + denom: string; + }) => { + const { chainName } = getChainInfo(chainID); + const { + minimalDenom: nativeMinimalDenom, + displayDenom, + decimals: nativeDecimals, + } = getDenomInfo(chainID); + const denomInfo = chainDenomsData[chainName.toLowerCase()]?.filter( + (denomInfo) => { + return denomInfo.denom === denom; + } + ); + const isNativeDenom = denom === nativeMinimalDenom; + if (!denomInfo?.length && !isNativeDenom) { + return { assetInfo: null }; + } + const assetData = { + symbol: isNativeDenom ? displayDenom : denomInfo?.[0].symbol, + decimals: isNativeDenom ? nativeDecimals : denomInfo?.[0].decimals, + originDenom: isNativeDenom ? nativeMinimalDenom : denomInfo?.[0].denom, + }; + if (assetData.decimals && assetData.symbol && assetData.originDenom) { + const assetInfo = { + amountInDenom: parseBalance( + [{ amount, denom: assetData.originDenom }], + assetData.decimals, + assetData.originDenom + ), + displayDenom: assetData.symbol, + }; + return { assetInfo }; + } + return { assetInfo: null }; + }; + + return { getAllAssets, getParsedAsset }; +}; + +export default useGetAllAssets; diff --git a/frontend/src/custom-hooks/routing/useHandleRouteChange.ts b/frontend/src/custom-hooks/routing/useHandleRouteChange.ts new file mode 100644 index 000000000..f3db83277 --- /dev/null +++ b/frontend/src/custom-hooks/routing/useHandleRouteChange.ts @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from '../StateHooks'; +import { setSelectedNetwork } from '@/store/features/common/commonSlice'; +import { usePathname } from 'next/navigation'; + +const useHandleRouteChange = () => { + const pathName = usePathname(); + const dispatch = useAppDispatch(); + + useEffect(() => { + const pathParts = pathName.split('/') || []; + + const getChainName = (index: number) => pathParts?.[index]?.toLowerCase() || ''; + + if (pathParts.includes('validator')) { + dispatch(setSelectedNetwork({ chainName: '' })); + } else if (pathParts.includes('feegrant') || pathParts.includes('authz')) { + if (pathParts.length >= 4) { + const isNewFeegrantOrAuthz = pathParts.includes('new-feegrant') || pathParts.includes('new-authz'); + dispatch(setSelectedNetwork({ chainName: isNewFeegrantOrAuthz ? '' : getChainName(3) })); + } else { + dispatch(setSelectedNetwork({ chainName: '' })); + } + } else if (pathParts.includes('builder') || pathParts.includes('history')) { + dispatch(setSelectedNetwork({ chainName: pathParts.length >= 4 ? getChainName(3) : '' })); + } else if (pathParts.length >= 3) { + dispatch(setSelectedNetwork({ chainName: getChainName(2) })); + } else { + dispatch(setSelectedNetwork({ chainName: '' })); + } + }, [pathName]); +}; + +export default useHandleRouteChange; diff --git a/frontend/src/custom-hooks/staking/useValidators.ts b/frontend/src/custom-hooks/staking/useValidators.ts new file mode 100644 index 000000000..9fd107157 --- /dev/null +++ b/frontend/src/custom-hooks/staking/useValidators.ts @@ -0,0 +1,58 @@ +import { get } from 'lodash'; +import { useAppSelector } from '../StateHooks'; + +const useValidators = () => { + const stakingData = useAppSelector((state) => state.staking.chains); + + const getValidators = ({ chainID }: { chainID: string }) => { + const validators = stakingData?.[chainID]?.validators; + const validatorsList = []; + for (let i = 0; i < validators?.activeSorted.length; i++) { + const validator = validators?.active[validators?.activeSorted[i]]; + const temp = { + label: validator?.description.moniker, + address: validators?.activeSorted[i], + identity: validator?.description.identity, + description: validator?.description?.details || '', + commission: + Number(get(validator, 'commission.commission_rates.rate', 0)) * 100, + }; + validatorsList.push(temp); + } + + for (let i = 0; i < validators?.inactiveSorted.length; i++) { + const validator = validators?.inactive[validators?.inactiveSorted[i]]; + if (!validator?.jailed) { + const temp = { + label: validator?.description.moniker, + address: validators?.inactiveSorted[i], + identity: validator?.description.identity, + description: validator?.description?.details || '', + commission: + Number(get(validator, 'commission.commission_rates.rate', 0)) * 100, + }; + validatorsList.push(temp); + } + } + + return { validatorsList }; + }; + + const getValidatorInfoByAddress = ({ + address, + chainID, + }: { + address: string; + chainID: string; + }) => { + const { validatorsList } = getValidators({ chainID }); + const validator = validatorsList.find( + (validator) => validator.address === address + ); + return validator; + }; + + return { getValidators, getValidatorInfoByAddress }; +}; + +export default useValidators; diff --git a/frontend/src/custom-hooks/txn-builder/useGov.ts b/frontend/src/custom-hooks/txn-builder/useGov.ts new file mode 100644 index 000000000..a4f6bf2bc --- /dev/null +++ b/frontend/src/custom-hooks/txn-builder/useGov.ts @@ -0,0 +1,32 @@ +import { useAppSelector } from '../StateHooks'; +import { TxStatus } from '@/types/enums'; +import { get } from 'lodash'; + +const useGov = () => { + const govData = useAppSelector((state) => state.gov.chains); + const getActiveProposals = ({ chainID }: { chainID: string }) => { + const activeProposals = govData?.[chainID]?.active?.proposals; + const proposalsLoading = + govData?.[chainID]?.active?.status === TxStatus.PENDING; + const activeProposalsList = []; + for (let i = 0; i < activeProposals?.length; i++) { + const proposal = activeProposals[i]; + const proposalTitle = get( + proposal, + 'content.title', + get(proposal, 'title', get(proposal, 'content.@type', '')) + ); + const proposalId = get(proposal, 'proposal_id', get(proposal, 'id', '')); + const temp = { + label: proposalTitle, + value: proposalId, + }; + activeProposalsList.push(temp); + } + + return { activeProposalsList, proposalsLoading }; + }; + return { getActiveProposals }; +}; + +export default useGov; diff --git a/frontend/src/custom-hooks/txn-builder/useStaking.ts b/frontend/src/custom-hooks/txn-builder/useStaking.ts new file mode 100644 index 000000000..a9f73305f --- /dev/null +++ b/frontend/src/custom-hooks/txn-builder/useStaking.ts @@ -0,0 +1,63 @@ +import { useAppSelector } from '../StateHooks'; + +const useStaking = () => { + const stakingData = useAppSelector((state) => state.staking.chains); + + const getValidators = ({ chainID }: { chainID: string }) => { + const validators = stakingData?.[chainID]?.validators; + const validatorsList = []; + for (let i = 0; i < validators?.activeSorted.length; i++) { + const validator = validators?.active[validators?.activeSorted[i]]; + const temp = { + label: validator?.description.moniker, + address: validators?.activeSorted[i], + identity: validator?.description.identity, + }; + validatorsList.push(temp); + } + + for (let i = 0; i < validators?.inactiveSorted.length; i++) { + const validator = validators?.inactive[validators?.inactiveSorted[i]]; + if (!validator?.jailed) { + const temp = { + label: validator?.description.moniker, + address: validators?.inactiveSorted[i], + identity: validator?.description.identity, + }; + validatorsList.push(temp); + } + } + + return { validatorsList }; + }; + + const getValidatorsForUndelegation = ({ chainID }: { chainID: string }) => { + const { validatorsList } = getValidators({ chainID }); + const totalDelegations = + stakingData?.[chainID]?.delegations?.delegations?.delegation_responses || + []; + const delegationsData = []; + const delegatedValidators = []; + + for (const validator of validatorsList) { + const delegation = totalDelegations.find( + (item) => item.delegation.validator_address === validator.address + ); + + if (delegation) { + delegationsData.push({ + validatorAddress: validator.address, + amount: delegation.balance.amount, + denom: delegation.balance.denom + }); + delegatedValidators.push(validator); + } + } + + return { delegationsData, delegatedValidators }; + }; + + return { getValidators, getValidatorsForUndelegation }; +}; + +export default useStaking; diff --git a/frontend/src/custom-hooks/useAccount.ts b/frontend/src/custom-hooks/useAccount.ts new file mode 100644 index 000000000..0be46a987 --- /dev/null +++ b/frontend/src/custom-hooks/useAccount.ts @@ -0,0 +1,93 @@ +import { useAppSelector } from './StateHooks'; +import useGetAssets from './useGetAssets'; +import useGetChains from './useGetChains'; + +declare let window: WalletWindow; + +interface CustomChainData extends ChainData { + chainName: string; +} + +const useAccount = () => { + const balances = useAppSelector((state) => state.bank.balances); + const { getTokensByChainID } = useGetAssets(); + const { getChainConfig } = useGetChains(); + const getAccountAddress = async ( + chainID: string + ): Promise<{ address: string }> => { + try { + const account = await window.wallet.getKey(chainID); + return { address: account.bech32Address }; + } catch (error) { + const chainConfig = getChainConfig(chainID); + const chainData: CustomChainData = { + ...chainConfig, + chainName: chainConfig.networkName, + }; + try { + const account = await window.wallet.experimentalSuggestChain(chainData); + return { address: account.bech32Address }; + } catch (error) { + console.log(error); + return { address: '' }; + } + } + }; + + const getAvailableBalance = async ({ + chainID, + denom, + }: { + chainID: string; + denom: string; + }) => { + const chainBalances = balances?.[chainID]?.list || []; + + const balanceInfo = { + amount: 0, + minimalDenom: '', + displayDenom: '', + decimals: 0, + parsedAmount: 0, + }; + + const chainAssets = await getTokensByChainID(chainID, true); + + chainBalances.forEach((balance) => { + const filteredDenomInfo = chainAssets?.filter((denomInfo) => { + return denomInfo.denom === balance.denom; + }); + const denomInfo = filteredDenomInfo[0]; + if (denomInfo && denomInfo.denom === denom) { + balanceInfo.amount = parseFloat(balance.amount); + const precision = denomInfo.decimals || 0 > 6 ? 6 : denomInfo.decimals; + balanceInfo.decimals = denomInfo.decimals || 0; + balanceInfo.displayDenom = denomInfo.symbol || ''; + balanceInfo.minimalDenom = denomInfo.denom; + + balanceInfo.parsedAmount = parseFloat( + (Number(balance.amount) / 10.0 ** (denomInfo.decimals || 0)).toFixed( + precision + ) + ); + } + }); + + if (!balanceInfo.minimalDenom || !balanceInfo.displayDenom) { + const filteredDenomInfo = chainAssets?.filter((denomInfo) => { + return denomInfo.denom === denom; + }); + balanceInfo.minimalDenom = filteredDenomInfo[0].denom || ''; + balanceInfo.displayDenom = filteredDenomInfo[0].symbol || ''; + balanceInfo.decimals = filteredDenomInfo[0].decimals || 0; + } + + return { + balanceInfo, + }; + }; + + return { getAccountAddress, getAvailableBalance }; +}; + +export default useAccount; diff --git a/frontend/src/custom-hooks/useAddressConverter.ts b/frontend/src/custom-hooks/useAddressConverter.ts new file mode 100644 index 000000000..97a9acc65 --- /dev/null +++ b/frontend/src/custom-hooks/useAddressConverter.ts @@ -0,0 +1,13 @@ +import useGetChainInfo from './useGetChainInfo'; +import { getAddressByPrefix } from '@/utils/address'; + +const useAddressConverter = () => { + const { getChainInfo } = useGetChainInfo(); + const convertAddress = (chainID: string, address: string) => { + const { prefix } = getChainInfo(chainID); + return getAddressByPrefix(address, prefix); + }; + return { convertAddress }; +}; + +export default useAddressConverter; diff --git a/frontend/src/custom-hooks/useAuthzExecHelper.ts b/frontend/src/custom-hooks/useAuthzExecHelper.ts new file mode 100644 index 000000000..15b2b1c36 --- /dev/null +++ b/frontend/src/custom-hooks/useAuthzExecHelper.ts @@ -0,0 +1,254 @@ +import { VoteOption } from 'cosmjs-types/cosmos/gov/v1beta1/gov'; +import useAddressConverter from './useAddressConverter'; +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import useGetChainInfo from './useGetChainInfo'; +import { AuthzExecVoteMsg } from '@/txns/authz'; +import { capitalizeFirstLetter } from '@/utils/util'; +import { AuthzExecDepositMsg, AuthzExecSendMsg } from '@/txns/authz/exec'; +import { msgSendTypeUrl } from '@/txns/bank/send'; +import { txBankSend } from '@/store/features/bank/bankSlice'; +import { txDeposit, txVote } from '@/store/features/gov/govSlice'; +import { isTimeExpired } from '@/utils/datetime'; + +export interface AuthzExecHelpVote { + grantee: string; + proposalId: number; + option: VoteOption; + granter: string; + chainID: string; + memo: string; +} + +export interface AuthzExecHelpDeposit { + grantee: string; + proposalId: number; + amount: number; + granter: string; + chainID: string; + memo: string; +} + +export interface AuthzExecHelpSend { + grantee: string; + recipient: string; + amount: number; + denom: string; + granter: string; + chainID: string; + memo: string; +} + +export const AUTHZ_VOTE_MSG = '/cosmos.gov.v1beta1.MsgVote'; +export const AUTHZ_DEPOSIT_MSG = '/cosmos.gov.v1beta1.MsgDeposit'; + +const useAuthzExecHelper = () => { + const { convertAddress } = useAddressConverter(); + const dispatch = useAppDispatch(); + const authzChains = useAppSelector((state) => state.authz.chains); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + + const txAuthzVote = (data: AuthzExecHelpVote) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + let isExpired = false; + const haveGrant = grants.some((grant) => { + if ( + grant.authorization['@type'] === + '/cosmos.authz.v1beta1.GenericAuthorization' && + grant.authorization.msg === AUTHZ_VOTE_MSG + ) { + isExpired = isTimeExpired(grant.expiration); + return true; + } else return false; + }); + if (isExpired) { + dispatch( + setError({ + type: 'error', + message: `Your Send permission on ${capitalizeFirstLetter( + basicChainInfo.chainName + )} from this account is expired`, + }) + ); + return; + } + if (!haveGrant) { + dispatch( + setError({ + type: 'error', + message: `You don't have permission to Vote on ${capitalizeFirstLetter( + basicChainInfo.chainName + )} from this account`, + }) + ); + } else { + const { minimalDenom } = getDenomInfo(data.chainID); + const msg = AuthzExecVoteMsg( + data.grantee, + data.proposalId, + data.option, + address + ); + dispatch( + txVote({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: data.memo, + denom: minimalDenom, + authzChainGranter: address, + }) + ); + } + }; + + const txAuthzDeposit = (data: AuthzExecHelpDeposit) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + let isExpired = false; + + const haveGrant = grants.some((grant) => { + if ( + grant.authorization['@type'] === + '/cosmos.authz.v1beta1.GenericAuthorization' && + grant.authorization.msg === AUTHZ_DEPOSIT_MSG + ) { + isExpired = isTimeExpired(grant.expiration); + return !isExpired; + } else return false; + }); + if (isExpired) { + dispatch( + setError({ + type: 'error', + message: `Your Deposit permission on ${capitalizeFirstLetter( + basicChainInfo.chainName + )} from this account is expired`, + }) + ); + return; + } + if (!haveGrant) { + dispatch( + setError({ + type: 'error', + message: `You don't have permission to Deposit on ${capitalizeFirstLetter( + basicChainInfo.chainName + )} from this account`, + }) + ); + } else { + const { minimalDenom } = getDenomInfo(data.chainID); + const msg = AuthzExecDepositMsg( + data.grantee, + data.proposalId, + address, + data.amount, + minimalDenom + ); + dispatch( + txDeposit({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: data.memo, + denom: minimalDenom, + authzChainGranter: address, + }) + ); + } + }; + + const txAuthzSend = (data: AuthzExecHelpSend) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + let errorMsg = `You don't have permission to Send on ${capitalizeFirstLetter( + basicChainInfo.chainName + )} from this account`; + let isExpired = false; + const haveGrant = grants.some((grant) => { + if ( + grant.authorization['@type'] === + '/cosmos.authz.v1beta1.GenericAuthorization' && + grant.authorization.msg === msgSendTypeUrl + ) { + isExpired = isTimeExpired(grant.expiration); + return true; + } + let validSend = false; + if ( + grant.authorization['@type'] === + '/cosmos.bank.v1beta1.SendAuthorization' + ) { + isExpired = isTimeExpired(grant.expiration); + if (grant.authorization?.allow_list?.length) { + const allowed = grant.authorization.allow_list.some( + (allowedAddress) => allowedAddress === data.recipient + ); + if (!allowed) { + errorMsg = 'You are not allowed send tokens to this address'; + return false; + } + } + grant.authorization.spend_limit.forEach((coin) => { + if (coin.denom === data.denom) { + if (+coin.amount < data.amount) { + errorMsg = 'Spend Limit Exceeded'; + } else { + validSend = true; + } + } + }); + return validSend; + } + }); + if (isExpired) { + dispatch( + setError({ + type: 'error', + message: `Your Send permission on ${capitalizeFirstLetter( + basicChainInfo.chainName + )} from this account is expired`, + }) + ); + return; + } + if (!haveGrant) { + dispatch( + setError({ + type: 'error', + message: errorMsg, + }) + ); + } else { + const { minimalDenom } = getDenomInfo(data.chainID); + const msg = AuthzExecSendMsg( + data.grantee, + address, + data.recipient, + data.amount, + data.denom + ); + dispatch( + txBankSend({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: data.memo, + denom: minimalDenom, + authzChainGranter: address, + }) + ); + } + }; + return { txAuthzVote, txAuthzDeposit, txAuthzSend }; +}; + +export default useAuthzExecHelper; diff --git a/frontend/src/custom-hooks/useAuthzGrants.ts b/frontend/src/custom-hooks/useAuthzGrants.ts new file mode 100644 index 000000000..6a14cac22 --- /dev/null +++ b/frontend/src/custom-hooks/useAuthzGrants.ts @@ -0,0 +1,162 @@ +import { exitAuthzMode } from '@/store/features/authz/authzSlice'; +import { resetAuthz as resetBankAuthz } from '@/store/features/bank/bankSlice'; +import { resetAuthz as resetRewardsAuthz } from '@/store/features/distribution/distributionSlice'; +import { resetAuthz as resetStakingAuthz } from '@/store/features/staking/stakeSlice'; +import { + COSMOS_CHAIN_ID, + GENERIC_AUTHORIZATION_TYPE, + IBC_SEND_TYPE_URL, + SEND_AUTHORIZATION_TYPE, + SEND_TYPE_URL, +} from '@/utils/constants'; +import { logoutAuthzMode } from '@/utils/localStorage'; +import { useAppDispatch, useAppSelector } from './StateHooks'; + +export interface ChainAuthz { + chainID: string; + grant: Authorization; +} +export interface InterChainAuthzGrants { + cosmosAddress: string; + address: string; + grants: ChainAuthz[]; +} + +const SEND = 'send'; +const IBC_TRANSFER = 'ibcTransfer'; + +const getAuthzType = (grant: Authorization) => { + if (grant.authorization['@type'] === SEND_AUTHORIZATION_TYPE) { + return SEND; + } + if (grant.authorization['@type'] === GENERIC_AUTHORIZATION_TYPE) { + if (grant.authorization.msg === SEND_TYPE_URL) { + return SEND; + } + if (grant.authorization.msg === IBC_SEND_TYPE_URL) { + return IBC_TRANSFER; + } + } + return null; +}; + +const useAuthzGrants = () => { + const authzChains = useAppSelector((state) => state.authz.chains); + const addressToChainAuthz = useAppSelector( + (state) => state.authz.AddressToChainAuthz + ); + const dispatch = useAppDispatch(); + + const getInterChainGrants = () => { + const interChainGrants: InterChainAuthzGrants[] = []; + + for (const address of Object.keys(addressToChainAuthz)) { + const cosmosAddress = address; + let keyAddress = address; + const chainAuthzs = []; + + for (const chainID of Object.keys(addressToChainAuthz[address])) { + chainAuthzs.push( + ...addressToChainAuthz[address][chainID].map((grant) => ({ + chainID, + grant, + })) + ); + } + + if (!addressToChainAuthz[address][COSMOS_CHAIN_ID]?.length) { + for (const chainID of Object.keys(addressToChainAuthz[address])) { + if (addressToChainAuthz[address][chainID].length) { + keyAddress = addressToChainAuthz[address][chainID][0].granter; + } + } + } + + if (chainAuthzs.length) + interChainGrants.push({ + cosmosAddress, + address: keyAddress, + grants: chainAuthzs, + }); + } + + return interChainGrants; + }; + + const getGrantsToMe = (chainIDs: string[]) => { + let grants: AddressGrants[] = []; + chainIDs && chainIDs.forEach((chainID) => { + + Object.keys(authzChains[chainID]?.GrantsToMeAddressMapping || {}).forEach( + (address) => { + grants = [ + ...grants, + { + address, + chainID, + grants: authzChains[chainID].GrantsToMeAddressMapping[address], + }, + ]; + } + ); + }); + return grants; + }; + + const getGrantsByMe = (chainIDs: string[]) => { + let grants: AddressGrants[] = []; + chainIDs && chainIDs.forEach((chainID) => { + Object.keys(authzChains[chainID]?.GrantsByMeAddressMapping || {}).forEach( + (address) => { + grants = [ + ...grants, + { + address, + chainID, + grants: authzChains[chainID].GrantsByMeAddressMapping[address], + }, + ]; + } + ); + }); + return grants; + }; + + const getSendAuthzGrants = (chainIDs: string[]) => { + const sendGrantsData = { send: 0, ibcTransfer: 0 }; + chainIDs && + chainIDs.forEach((chainID) => { + Object.keys( + authzChains[chainID]?.GrantsByMeAddressMapping || {} + ).forEach((address) => { + authzChains[chainID].GrantsByMeAddressMapping[address].forEach((grant) => { + const authType = getAuthzType(grant); + if (authType === SEND) { + sendGrantsData.send = sendGrantsData.send + 1; + } else if (authType === IBC_TRANSFER) { + sendGrantsData.ibcTransfer = sendGrantsData.ibcTransfer + 1; + } + }) + }); + }); + return sendGrantsData; + }; + + const disableAuthzMode = () => { + dispatch(resetBankAuthz()); + dispatch(resetRewardsAuthz()); + dispatch(resetStakingAuthz()); + dispatch(exitAuthzMode()); + logoutAuthzMode(); + }; + + return { + getGrantsByMe, + getGrantsToMe, + getInterChainGrants, + disableAuthzMode, + getSendAuthzGrants, + }; +}; + +export default useAuthzGrants; diff --git a/frontend/src/custom-hooks/useAuthzStakingExecHelper.tsx b/frontend/src/custom-hooks/useAuthzStakingExecHelper.tsx new file mode 100644 index 000000000..980768393 --- /dev/null +++ b/frontend/src/custom-hooks/useAuthzStakingExecHelper.tsx @@ -0,0 +1,577 @@ +import useAddressConverter from './useAddressConverter'; +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { setError } from '@/store/features/common/commonSlice'; +import useGetChainInfo from './useGetChainInfo'; +import { capitalizeFirstLetter } from '@/utils/util'; +import { + AuthzExecDelegateMsg, + AuthzExecMsgCancelUnbond, + AuthzExecMsgRestake, + AuthzExecReDelegateMsg, + AuthzExecSetWithdrawAddressMsg, + AuthzExecUnDelegateMsg, + AuthzExecWithdrawRewardsAndCommissionMsg, + AuthzExecWithdrawRewardsMsg, +} from '@/txns/authz/exec'; +import { msgDelegate } from '@/txns/staking/delegate'; +import { msgReDelegate } from '@/txns/staking/redelegate'; +import { DelegationsPairs } from '@/types/distribution'; +import { msgUnbonding } from '@/txns/staking/unbonding'; +import { msgUnDelegate } from '@/txns/staking/undelegate'; +import { + txSetWithdrawAddress, + txWithdrawAllRewards, + txWithdrawValidatorCommission, + txWithdrawValidatorCommissionAndRewards, +} from '@/store/features/distribution/distributionSlice'; +import { + txCancelUnbonding, + txDelegate, + txReDelegate, + txRestake, + txUnDelegate, +} from '@/store/features/staking/stakeSlice'; +import { isTimeExpired } from '@/utils/datetime'; +import { + GENERIC_AUTHORIZATION_TYPE, + STAKE_AUTHORIZATION_TYPE, +} from '@/utils/constants'; +import useGetDistributionMsgs from './useGetDistributionMsgs'; +import { msgSetWithdrawAddress } from '@/txns/distribution/setWithdrawAddress'; + +export interface AuthzExecHelpDelegate { + grantee: string; + granter: string; + validator: string; + amount: number; + denom: string; + chainID: string; +} + +export interface AuthzExecHelpReDelegate { + grantee: string; + granter: string; + srcValidator: string; + validator: string; + amount: number; + denom: string; + chainID: string; +} + +export interface AuthzExecHelpWithdrawRewards { + grantee: string; + granter: string; + pairs: DelegationsPairs[]; + chainID: string; + isTxAll?: boolean; +} + +export interface AuthzExecHelpWithdrawRewardsAndCommission { + grantee: string; + granter: string; + chainID: string; +} + +export interface AuthzExecHelpSetWithdrawAddress { + grantee: string; + granter: string; + chainID: string; + withdrawAddress: string; +} + +export interface AuthzExecHelpCancelUnbond { + grantee: string; + granter: string; + msg: Msg; + chainID: string; +} + +export interface AuthzExecHelpRestake { + grantee: string; + granter: string; + msgs: Msg[]; + chainID: string; + isTxAll?: boolean; +} + +export interface authzFilterOptions { + generic: { + msg: string; + }; + stake?: { + type: string; + }; +} + +export const haveGenericGrant = (grant: Authorization, msg: string) => { + return ( + grant.authorization['@type'] === GENERIC_AUTHORIZATION_TYPE && + grant.authorization.msg === msg + ); +}; + +export const haveStakeGrant = (grant: Authorization, stakeType: string) => { + return ( + grant.authorization['@type'] === STAKE_AUTHORIZATION_TYPE && + grant.authorization.authorization_type === stakeType + ); +}; + +export const haveAuthorization = ( + grants: Authorization[], + options: authzFilterOptions +) => { + let isExpired = false; + + const haveGrant = grants.some((grant) => { + if ( + haveGenericGrant(grant, options.generic.msg) || + (options.stake && haveStakeGrant(grant, options.stake.type)) + ) { + isExpired = isTimeExpired(grant.expiration); + return true; + } else return false; + }); + + return { isExpired: isExpired, haveGrant: haveGrant }; +}; + +export const AUTHZ_VOTE_MSG = '/cosmos.gov.v1beta1.MsgVote'; +export const AUTHZ_DEPOSIT_MSG = '/cosmos.gov.v1beta1.MsgDeposit'; +const AUTHZ_WITHDRAW_MSG = + '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward'; + +const useAuthzStakingExecHelper = () => { + const { convertAddress } = useAddressConverter(); + const dispatch = useAppDispatch(); + const authzChains = useAppSelector((state) => state.authz.chains); + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const { + getWithdrawCommissionAndRewardsMsgs, + getSetWithdrawAddressMsg, + getWithdrawCommissionMsgs, + } = useGetDistributionMsgs(); + + const isInvalidAction = ( + isExpired: boolean, + haveGrant: boolean, + chainName: string, + action: string + ) => { + if (isExpired) { + throwGrantExpiredError(chainName, action); + return true; + } + + if (!haveGrant) { + throwGrantNotFoundError(chainName, action); + return true; + } + + return false; + }; + + const throwGrantExpiredError = (chainName: string, action: string) => { + dispatch( + setError({ + type: 'error', + message: `Your ${action} permission on ${capitalizeFirstLetter( + chainName + )} from this account is expired`, + }) + ); + }; + + const throwGrantNotFoundError = (chainName: string, action: string) => { + dispatch( + setError({ + type: 'error', + message: `You don't have permission to ${action} on ${capitalizeFirstLetter( + chainName + )} from this account`, + }) + ); + }; + + const txAuthzDelegate = (data: AuthzExecHelpDelegate) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + + const authzFilters: authzFilterOptions = { + generic: { + msg: msgDelegate, + }, + stake: { + type: 'AUTHORIZATION_TYPE_DELEGATE', + }, + }; + + const { haveGrant, isExpired } = haveAuthorization(grants, authzFilters); + + if ( + isInvalidAction( + isExpired, + haveGrant, + basicChainInfo.chainName, + 'Delegate' + ) + ) + return; + else { + const { minimalDenom } = getDenomInfo(data.chainID); + const msg = AuthzExecDelegateMsg( + data.grantee, + address, + data.validator, + data.amount, + data.denom + ); + + dispatch( + txDelegate({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + }) + ); + } + }; + + const txAuthzUnDelegate = (data: AuthzExecHelpDelegate) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + + const authzFilters: authzFilterOptions = { + generic: { + msg: msgUnDelegate, + }, + stake: { + type: 'AUTHORIZATION_TYPE_UNDELEGATE', + }, + }; + + const { haveGrant, isExpired } = haveAuthorization(grants, authzFilters); + + if ( + isInvalidAction( + isExpired, + haveGrant, + basicChainInfo.chainName, + 'Un-Delegate' + ) + ) + return; + else { + const { minimalDenom } = getDenomInfo(data.chainID); + const msg = AuthzExecUnDelegateMsg( + data.grantee, + address, + data.validator, + data.amount, + data.denom + ); + + dispatch( + txUnDelegate({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + }) + ); + } + }; + + const txAuthzReDelegate = (data: AuthzExecHelpReDelegate) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + + const authzFilters: authzFilterOptions = { + generic: { + msg: msgReDelegate, + }, + stake: { + type: 'AUTHORIZATION_TYPE_REDELEGATE', + }, + }; + + const { haveGrant, isExpired } = haveAuthorization(grants, authzFilters); + + if ( + isInvalidAction( + isExpired, + haveGrant, + basicChainInfo.chainName, + 'Change Delegation' + ) + ) + return; + else { + const { minimalDenom } = getDenomInfo(data.chainID); + const msg = AuthzExecReDelegateMsg( + data.grantee, + address, + data.srcValidator, + data.validator, + data.amount, + data.denom + ); + + dispatch( + txReDelegate({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + }) + ); + } + }; + + const txAuthzClaim = (data: AuthzExecHelpWithdrawRewards) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + + const authzFilters: authzFilterOptions = { + generic: { + msg: AUTHZ_WITHDRAW_MSG, + }, + }; + + const { haveGrant, isExpired } = haveAuthorization(grants, authzFilters); + + if ( + isInvalidAction( + isExpired, + haveGrant, + basicChainInfo.chainName, + 'Claim Rewards' + ) + ) + return; + else { + const { minimalDenom } = getDenomInfo(data.chainID); + const pairs = data.pairs.map((pair) => { + pair.delegator = address; + return pair; + }); + const msg = AuthzExecWithdrawRewardsMsg(data.grantee, pairs); + + dispatch( + txWithdrawAllRewards({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + isTxAll: data.isTxAll, + }) + ); + } + }; + + const txAuthzCancelUnbond = (data: AuthzExecHelpCancelUnbond) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + + const authzFilters: authzFilterOptions = { + generic: { + msg: msgUnbonding, + }, + }; + + const { haveGrant, isExpired } = haveAuthorization(grants, authzFilters); + + if ( + isInvalidAction( + isExpired, + haveGrant, + basicChainInfo.chainName, + 'Cancel Un-bonding' + ) + ) + return; + else { + const { minimalDenom } = getDenomInfo(data.chainID); + const msg = AuthzExecMsgCancelUnbond(data.msg, data.grantee); + + dispatch( + txCancelUnbonding({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + }) + ); + } + }; + + const txAuthzRestake = (data: AuthzExecHelpRestake) => { + const basicChainInfo = getChainInfo(data.chainID); + const address = convertAddress(data.chainID, data.granter); + const grants: Authorization[] = + authzChains?.[data.chainID]?.GrantsToMeAddressMapping?.[address] || []; + + const authzFilters: authzFilterOptions = { + generic: { + msg: msgDelegate, + }, + stake: { + type: 'AUTHORIZATION_TYPE_DELEGATE', + }, + }; + + const { haveGrant, isExpired } = haveAuthorization(grants, authzFilters); + + if ( + isInvalidAction( + isExpired, + haveGrant, + basicChainInfo.chainName, + 'Re-stake' + ) + ) + return; + else { + const { minimalDenom } = getDenomInfo(data.chainID); + const msg = AuthzExecMsgRestake(data.msgs, data.grantee); + + dispatch( + txRestake({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + isTxAll: data.isTxAll, + }) + ); + } + }; + + const txAuthzWithdrawRewardsAndCommission = ( + data: AuthzExecHelpWithdrawRewardsAndCommission + ) => { + const { chainID } = data; + const basicChainInfo = getChainInfo(chainID); + const address = convertAddress(chainID, data.granter); + + const { minimalDenom } = getDenomInfo(chainID); + const msgs = getWithdrawCommissionAndRewardsMsgs({ chainID }); + const msg = AuthzExecWithdrawRewardsAndCommissionMsg(data.grantee, msgs); + + dispatch( + txWithdrawValidatorCommissionAndRewards({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + }) + ); + }; + + const txAuthzWithdrawCommission = ( + data: AuthzExecHelpWithdrawRewardsAndCommission + ) => { + const { chainID } = data; + const basicChainInfo = getChainInfo(chainID); + const address = convertAddress(chainID, data.granter); + + const { minimalDenom } = getDenomInfo(chainID); + const msgs = getWithdrawCommissionMsgs({ chainID }); + const msg = AuthzExecWithdrawRewardsAndCommissionMsg(data.grantee, msgs); + + dispatch( + txWithdrawValidatorCommission({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + }) + ); + }; + + const txAuthzSetWithdrawAddress = (data: AuthzExecHelpSetWithdrawAddress) => { + const { chainID } = data; + const basicChainInfo = getChainInfo(chainID); + const address = convertAddress(chainID, data.granter); + const grants: Authorization[] = + authzChains?.[chainID]?.GrantsToMeAddressMapping?.[address] || []; + + const authzFilters: authzFilterOptions = { + generic: { + msg: msgSetWithdrawAddress, + }, + }; + + const { haveGrant, isExpired } = haveAuthorization(grants, authzFilters); + + if ( + isInvalidAction( + isExpired, + haveGrant, + basicChainInfo.chainName, + 'Set Withdraw Address' + ) + ) + return; + else { + const { minimalDenom } = getDenomInfo(chainID); + const msgs = getSetWithdrawAddressMsg({ + chainID: chainID, + withdrawAddress: data.withdrawAddress, + }); + const msg = AuthzExecSetWithdrawAddressMsg(data.grantee, [msgs]); + + dispatch( + txSetWithdrawAddress({ + isAuthzMode: true, + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + authzChainGranter: address, + }) + ); + } + }; + + return { + txAuthzDelegate, + txAuthzUnDelegate, + txAuthzReDelegate, + txAuthzClaim, + txAuthzCancelUnbond, + txAuthzRestake, + txAuthzWithdrawRewardsAndCommission, + txAuthzSetWithdrawAddress, + txAuthzWithdrawCommission, + }; +}; + +export default useAuthzStakingExecHelper; diff --git a/frontend/src/custom-hooks/useChain.ts b/frontend/src/custom-hooks/useChain.ts new file mode 100644 index 000000000..7a3d6dd5f --- /dev/null +++ b/frontend/src/custom-hooks/useChain.ts @@ -0,0 +1,80 @@ +import { capitalizeFirstLetter } from '@/utils/util'; +import { chains } from 'chain-registry'; + +const useChain = () => { + const getChainEndpoints = (chainID: string) => { + const filteredChain = chains.filter((chain) => chain.chain_id === chainID); + const chainData = filteredChain[0]; + const apis: string[] = []; + const rpcs: string[] = []; + chainData?.apis?.rest?.slice(0, 3).forEach((api) => { + apis.push(api.address); + }); + chainData?.apis?.rpc?.slice(0, 3).forEach((rpc) => { + rpcs.push(rpc.address); + }); + return { + apis, + rpcs, + }; + }; + + const getExplorerEndpoints = (chainID: string) => { + if (!chainID.length) { + return { + explorerEndpoint: '', + }; + } + const filteredChain = chains.filter((chain) => chain.chain_id === chainID); + const chainData = filteredChain[0]; + let explorerEndpoint = chainData.explorers?.[0].tx_page || ''; + chainData.explorers?.forEach((explorer) => { + if (explorer.kind?.includes('mintscan')) + explorerEndpoint = explorer.tx_page || ''; + }); + explorerEndpoint = explorerEndpoint ? explorerEndpoint.split('$')[0] : ''; + return { + explorerEndpoint, + }; + }; + + const getChainNameFromID = (chainID: string) => { + if (!chainID.length) { + return { + chainName: '', + }; + } + const filteredChain = chains.filter((chain) => chain.chain_id === chainID); + const chainData = filteredChain?.[0]; + return { + chainName: chainData?.chain_name || capitalizeFirstLetter(chainID), + }; + }; + + const getChainLogoURI = (chainID: string) => { + if (!chainID.length) { + return { + chainLogo: '', + }; + } + const filteredChain = chains.filter((chain) => chain.chain_id === chainID); + const chainData = filteredChain?.[0]; + return { + chainLogo: + chainData?.logo_URIs?.svg || + chainData?.logo_URIs?.jpeg || + chainData?.logo_URIs?.jpeg || + chainData?.logo_URIs?.png || + '', + }; + }; + + return { + getChainEndpoints, + getExplorerEndpoints, + getChainNameFromID, + getChainLogoURI, + }; +}; + +export default useChain; diff --git a/frontend/src/custom-hooks/useContracts.ts b/frontend/src/custom-hooks/useContracts.ts new file mode 100644 index 000000000..6839eb491 --- /dev/null +++ b/frontend/src/custom-hooks/useContracts.ts @@ -0,0 +1,536 @@ +import { + connectWithSigner, + getContract, + queryContract, +} from '@/store/features/cosmwasm/cosmwasmService'; +import { extractContractMessages } from '@/utils/util'; +import { useState } from 'react'; +import { useDummyWallet } from './useDummyWallet'; +import chainDenoms from '@/utils/chainDenoms.json'; +import useGetChainInfo from './useGetChainInfo'; +import { Event } from 'cosmjs-types/tendermint/abci/types'; +import { toUtf8 } from '@cosmjs/encoding'; +import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'; + +declare let window: WalletWindow; + +const dummyQuery = { + '': '', +}; + +const assetsData = chainDenoms as AssetData; + +const GAS = '900000'; + +const getCodeIdFromEvents = (events: Event[]) => { + let codeId = ''; + for (let i = 0; i < events.length; i++) { + const event = events[i]; + if (event.type === 'store_code') { + for (let j = 0; j < event.attributes.length; j++) { + const attribute = event.attributes[j]; + if (attribute.key === 'code_id') { + codeId = attribute.value; + break; + } + } + } + } + return codeId; +}; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const getCodeId = (txData: any) => { + return getCodeIdFromEvents(txData?.events || []); +}; + +const useContracts = () => { + // ------------------------------------------// + // ---------------DEPENDENCIES---------------// + // ------------------------------------------// + const { getDummyWallet } = useDummyWallet(); + const { getChainInfo } = useGetChainInfo(); + + // ------------------------------------------// + // ------------------STATES------------------// + // ------------------------------------------// + const [contractLoading, setContractLoading] = useState(false); + const [contractError, setContractError] = useState(''); + const [messagesLoading, setMessagesLoading] = useState(false); + const [messagesError, setMessagesError] = useState(''); + const [messageInputsLoading, setMessageInputsLoading] = useState(false); + const [messageInputsError, setMessageInputsError] = useState(''); + const [executeMessagesLoading, setExecuteMessagesLoading] = useState(false); + const [executeMessagesError, setExecuteMessagesError] = useState(''); + const [executeInputsLoading, setExecuteInputsLoading] = useState(false); + const [executeInputsError, setExecuteInputsError] = useState(''); + + const getContractInfo = async ({ + address, + baseURLs, + chainID, + }: { + baseURLs: string[]; + address: string; + chainID: string; + }) => { + try { + setContractLoading(true); + setContractError(''); + const res = await getContract(baseURLs, address, chainID); + setContractError(''); + return { + data: await res.json(), + }; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + setContractError(error.message); + } finally { + setContractLoading(false); + } + return { + data: null, + }; + }; + + const getContractMessages = async ({ + address, + baseURLs, + chainID, + queryMsg = dummyQuery, + }: { + address: string; + baseURLs: string[]; + chainID: string; + queryMsg?: any; + }) => { + let messages: string[] = []; + try { + setMessagesLoading(true); + setMessagesError(''); + const res = await queryContract( + baseURLs, + address, + btoa(JSON.stringify(queryMsg)), + chainID + ); + console.log('res.....', res); + return { + messages: [], + }; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + const errMsg = error.message; + if (errMsg?.includes('expected') || errMsg?.includes('missing field')) { + messages = extractContractMessages(error.message); + } else { + messages = []; + setMessagesError('Failed to fetch messages'); + } + } finally { + setMessagesLoading(false); + } + return { + messages, + }; + }; + + const getContractMessageInputs = async ({ + address, + baseURLs, + queryMsg, + extractedMessages, + msgName, + chainID, + }: { + address: string; + baseURLs: string[]; + queryMsg: any; + msgName: string; + extractedMessages: string[]; + chainID: string; + }) => { + setMessageInputsLoading(true); + setMessageInputsError(''); + + const queryWithRetry = async (msg: { + [key: string]: any; + }): Promise => { + try { + await queryContract( + baseURLs, + address, + btoa(JSON.stringify(queryMsg)), + chainID + ); + return; + } catch (error: any) { + const errMsg = error.message; + if (errMsg?.includes('Failed to query contract')) { + setMessageInputsError('Failed to fetch messages'); + extractedMessages = []; + } else if (errMsg?.includes('expected')) { + setMessageInputsError('Failed to fetch messages'); + extractedMessages = []; + } else { + const newlyExtractedMessages = extractContractMessages(error.message); + if (newlyExtractedMessages.length === 0) { + return; + } else { + extractedMessages.push(...newlyExtractedMessages); + for (const field of extractedMessages) { + msg[msgName][field] = '1'; + } + await queryWithRetry(msg); + } + } + } + }; + + await queryWithRetry(queryMsg); + setMessageInputsLoading(false); + return { + messages: extractedMessages, + }; + }; + + const getQueryContract = async ({ + address, + baseURLs, + queryData, + chainID, + }: GetQueryContractFunctionInputs) => { + try { + const respose = await queryContract( + baseURLs, + address, + btoa(queryData), + chainID + ); + return { + data: respose, + }; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + throw new Error(error.message); + } + }; + + const getExecuteMessages = async ({ + rpcURLs, + chainID, + contractAddress, + }: { + rpcURLs: string[]; + chainID: string; + contractAddress: string; + }) => { + const { dummyAddress, dummyWallet } = await getDummyWallet({ chainID }); + let messages: string[] = []; + setExecuteMessagesLoading(true); + setExecuteMessagesError(''); + let client: SigningCosmWasmClient; + try { + client = await connectWithSigner(rpcURLs, dummyWallet); + } catch (error: any) { + setExecuteMessagesError('Failed to fetch messages'); + setExecuteMessagesLoading(false); + return { messages: [] }; + } + try { + await client.simulate( + dummyAddress, + [ + { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: { + sender: dummyAddress, + contract: contractAddress, + msg: Buffer.from('{"": {}}'), + funds: [], + }, + }, + ], + undefined + ); + return { + messages: [], + }; + } catch (error: any) { + const errMsg = error.message; + if (errMsg?.includes('expected') || errMsg?.includes('missing field')) { + messages = extractContractMessages(error.message); + } else { + messages = []; + setExecuteMessagesError('Failed to fetch messages'); + } + } finally { + setExecuteMessagesLoading(false); + } + return { + messages, + }; + }; + + const getExecuteMessagesInputs = async ({ + rpcURLs, + chainID, + contractAddress, + msg, + msgName, + extractedMessages, + }: { + rpcURLs: string[]; + chainID: string; + contractAddress: string; + msg: { [key: string]: any }; + msgName: string; + extractedMessages: string[]; + }): Promise<{ messages: string[] }> => { + const { dummyAddress, dummyWallet } = await getDummyWallet({ chainID }); + setExecuteInputsLoading(true); + setExecuteInputsError(''); + let client: SigningCosmWasmClient; + try { + client = await connectWithSigner(rpcURLs, dummyWallet); + } catch (error: any) { + setExecuteInputsError('Failed to fetch messages'); + setExecuteInputsLoading(false); + return { messages: [] }; + } + + const executeWithRetry = async (msg: { + [key: string]: any; + }): Promise => { + try { + await client.simulate( + dummyAddress, + [ + { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: { + sender: dummyAddress, + contract: contractAddress, + msg: Buffer.from(JSON.stringify(msg)), + funds: [], + }, + }, + ], + undefined + ); + + return; + } catch (error: any) { + const errMsg = error.message; + if (errMsg?.includes('expected') || errMsg?.includes('429')) { + setExecuteInputsError('Failed to fetch messages'); + extractedMessages = []; + } else if (errMsg?.includes('Insufficient')) { + setExecuteInputsError(''); + return; + } else { + const newlyExtractedMessages = extractContractMessages(error.message); + setExecuteInputsError(''); + if (newlyExtractedMessages.length === 0) { + return; + } else { + extractedMessages.push(...newlyExtractedMessages); + for (const field of extractedMessages) { + msg[msgName][field] = '1'; + } + await executeWithRetry(msg); + } + } + } + }; + + await executeWithRetry(msg); + setExecuteInputsLoading(false); + return { messages: extractedMessages }; + }; + + const getExecutionOutput = async ({ + rpcURLs, + chainID, + contractAddress, + walletAddress, + msgs, + funds, + }: GetExecutionOutputFunctionInputs) => { + const offlineSigner = window.wallet.getOfflineSigner(chainID); + const client = await connectWithSigner(rpcURLs, offlineSigner); + const { feeAmount, feeCurrencies } = getChainInfo(chainID); + const { coinDecimals, coinDenom } = feeCurrencies[0]; + const fee = { + amount: [ + { + amount: (feeAmount * 10 ** coinDecimals).toString(), + denom: coinDenom, + }, + ], + gas: GAS, + }; + try { + const response = await client.signAndBroadcast( + walletAddress, + [ + { + typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract', + value: { + sender: walletAddress, + contract: contractAddress, + msg: toUtf8(msgs), + funds, + }, + }, + ], + fee, + '' + ); + return { txHash: response.transactionHash }; + } catch (error: any) { + throw new Error(error?.message || 'Failed to execute contract'); + } + }; + + const uploadContract = async ({ + chainID, + address, + messages, + }: UploadContractFunctionInputs) => { + const { feeAmount, feeCurrencies, rpcURLs } = getChainInfo(chainID); + const { coinDecimals, coinDenom } = feeCurrencies[0]; + const offlineSigner = window.wallet.getOfflineSigner(chainID); + const client = await connectWithSigner(rpcURLs, offlineSigner); + + const fee = { + amount: [ + { + amount: (feeAmount * 10 ** coinDecimals).toString(), + denom: coinDenom, + }, + ], + gas: '1100000', + }; + try { + const response = await client.signAndBroadcast( + address, + messages, + fee, + undefined, + undefined + ); + const codeId = getCodeId(response); + return { codeId, txHash: response?.transactionHash }; + } catch (error: any) { + throw new Error(error?.message || 'Failed to upload contract'); + } + }; + + const instantiateContract = async ({ + chainID, + codeId, + msg, + label, + admin, + funds, + }: InstantiateContractFunctionInputs) => { + const { + feeAmount, + feeCurrencies, + rpcURLs, + address: senderAddress, + } = getChainInfo(chainID); + const { coinDecimals, coinDenom } = feeCurrencies[0]; + const offlineSigner = window.wallet.getOfflineSigner(chainID); + const client = await connectWithSigner(rpcURLs, offlineSigner); + const fee = { + amount: [ + { + amount: (feeAmount * 10 ** coinDecimals).toString(), + denom: coinDenom, + }, + ], + gas: GAS, + }; + try { + const response = await client.signAndBroadcast( + senderAddress, + [ + { + typeUrl: '/cosmwasm.wasm.v1.MsgInstantiateContract', + value: { + sender: senderAddress, + codeId: codeId, + msg: toUtf8(msg), + label: label, + funds: funds || [], + admin: admin, + }, + }, + ], + fee, + '' + ); + const instantiateEvent = response.events.find( + (event) => event.type === 'instantiate' + ); + const contractAddress = + instantiateEvent?.attributes.find( + (attr) => attr.key === '_contract_address' + )?.value || ''; + const uploadedCodeId = + instantiateEvent?.attributes.find((attr) => attr.key === 'code_id') + ?.value || ''; + return { + codeId: uploadedCodeId, + contractAddress, + txHash: response?.transactionHash, + }; + } catch (error: any) { + throw new Error(error.message || 'Failed to instantiate'); + } + }; + + const getChainAssets = (chainName: string) => { + const chainAssets = assetsData?.[chainName]; + const assetsList: { + coinMinimalDenom: string; + decimals: number; + symbol: string; + }[] = []; + chainAssets?.forEach((asset) => { + assetsList.push({ + symbol: asset.symbol, + decimals: asset.decimals, + coinMinimalDenom: asset.origin_denom, + }); + }); + return { assetsList }; + }; + + return { + contractLoading, + getContractInfo, + contractError, + getContractMessages, + messagesLoading, + getQueryContract, + getExecuteMessages, + getExecutionOutput, + getChainAssets, + uploadContract, + instantiateContract, + getContractMessageInputs, + getExecuteMessagesInputs, + messageInputsLoading, + messageInputsError, + messagesError, + executeMessagesError, + executeMessagesLoading, + executeInputsError, + executeInputsLoading, + }; +}; + +export default useContracts; diff --git a/frontend/src/custom-hooks/useDummyWallet.ts b/frontend/src/custom-hooks/useDummyWallet.ts new file mode 100644 index 000000000..5c96a4ee1 --- /dev/null +++ b/frontend/src/custom-hooks/useDummyWallet.ts @@ -0,0 +1,21 @@ +import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing'; +import useGetChainInfo from './useGetChainInfo'; +import { DUMMY_WALLET_MNEMONIC } from '@/utils/constants'; + +export const useDummyWallet = () => { + const { getChainInfo } = useGetChainInfo(); + const getDummyWallet = async ({ chainID }: { chainID: string }) => { + const { prefix } = getChainInfo(chainID); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic( + DUMMY_WALLET_MNEMONIC, + { + prefix, + } + ); + const allAccounts = await wallet.getAccounts(); + const { address } = allAccounts[0]; + + return { dummyWallet: wallet, dummyAddress: address }; + }; + return { getDummyWallet }; +}; diff --git a/frontend/src/custom-hooks/useFeeGrants.ts b/frontend/src/custom-hooks/useFeeGrants.ts new file mode 100644 index 000000000..bd48ef8be --- /dev/null +++ b/frontend/src/custom-hooks/useFeeGrants.ts @@ -0,0 +1,110 @@ +import { COSMOS_CHAIN_ID } from '@/utils/constants'; +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { exitFeegrantMode } from '@/store/features/feegrant/feegrantSlice'; +import { logoutFeegrantMode } from '@/utils/localStorage'; + +export interface ChainAllowance { + chainID: string; + grant: Allowance; +} +export interface InterChainFeegrants { + cosmosAddress: string; + address: string; + grants: ChainAllowance[]; +} + +const useFeeGrants = () => { + const dispatch = useAppDispatch(); + const feegrantChains = useAppSelector((state) => state.feegrant.chains); + const addressToChainFeegrant = useAppSelector( + (state) => state.feegrant.addressToChainFeegrant + ); + + const getInterChainGrants = () => { + const interChainGrants: InterChainFeegrants[] = []; + + for (const address of Object.keys(addressToChainFeegrant)) { + const cosmosAddress = address; + let keyAddress = address; + const chainFeegrants = []; + + for (const chainID of Object.keys(addressToChainFeegrant[address])) { + chainFeegrants.push( + ...addressToChainFeegrant[address][chainID].map((grant) => ({ + chainID, + grant, + })) + ); + } + + if (!addressToChainFeegrant[address][COSMOS_CHAIN_ID]?.length) { + for (const chainID of Object.keys(addressToChainFeegrant[address])) { + if (addressToChainFeegrant[address][chainID].length) { + keyAddress = addressToChainFeegrant[address][chainID][0].granter; + } + } + } + + if (chainFeegrants.length) + interChainGrants.push({ + cosmosAddress, + address: keyAddress, + grants: chainFeegrants, + }); + } + + return interChainGrants; + }; + + const getGrantsToMe = (chainIDs: string[]) => { + let grants: AddressFeegrants[] = []; + chainIDs.forEach((chainID) => { + Object.keys( + feegrantChains[chainID]?.grantsToMeAddressMapping || {} + ).forEach((address) => { + grants = [ + ...grants, + { + address, + chainID, + grants: feegrantChains[chainID].grantsToMeAddressMapping[address], + }, + ]; + }); + }); + return grants; + }; + + const getGrantsByMe = (chainIDs: string[]) => { + let grants: AddressFeegrants[] = []; + chainIDs.forEach((chainID) => { + Object.keys( + feegrantChains[chainID]?.grantsByMeAddressMapping || {} + ).forEach((address) => { + grants = [ + ...grants, + { + address, + chainID, + grants: feegrantChains[chainID].grantsByMeAddressMapping[address], + }, + ]; + }); + }); + return grants; + }; + + const disableFeegrantMode = () => { + dispatch(exitFeegrantMode()); + logoutFeegrantMode(); + }; + + return { + getGrantsByMe, + getGrantsToMe, + getInterChainGrants, + disableFeegrantMode, + }; +}; + +export default useFeeGrants; diff --git a/frontend/src/custom-hooks/useFetchPriceInfo.ts b/frontend/src/custom-hooks/useFetchPriceInfo.ts new file mode 100644 index 000000000..c701ee6dd --- /dev/null +++ b/frontend/src/custom-hooks/useFetchPriceInfo.ts @@ -0,0 +1,46 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from './StateHooks'; +import { getAllTokensPrice } from '@/store/features/common/commonSlice'; + +const interval = 300000; // 5 minutes + +/** + * Custom hook to fetch token price information at regular intervals. + * + * This hook dispatches the `getAllTokensPrice` action immediately upon component mount + * and then repeatedly every 5 minutes (300,000 milliseconds) to keep the token prices + * updated in the Redux store. + * + * Usage: + * + * ``` + * import useFetchPriceInfo from './path/to/useFetchPriceInfo'; + * + * const MyComponent = () => { + * useFetchPriceInfo(); + * + * return ( + *
    My Component
    + * ); + * }; + * ``` + * + * Dependencies: + * - `useAppDispatch`: A custom hook to access the Redux `dispatch` function. + * - `getAllTokensPrice`: An asynchronous thunk action from the `commonSlice` that fetches all token prices. + * + * @returns {void} + */ + +const useFetchPriceInfo = () => { + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch(getAllTokensPrice()); + const intervalId = setInterval(() => { + dispatch(getAllTokensPrice()); + }, interval); + return () => clearInterval(intervalId); + }, []); +}; + +export default useFetchPriceInfo; diff --git a/frontend/src/custom-hooks/useGetAccountInfo.ts b/frontend/src/custom-hooks/useGetAccountInfo.ts index 6ef177045..39b8efae7 100644 --- a/frontend/src/custom-hooks/useGetAccountInfo.ts +++ b/frontend/src/custom-hooks/useGetAccountInfo.ts @@ -10,7 +10,7 @@ const useGetAccountInfo = (chainID: string) => { const sequence = authInfo?.account.sequence || '-'; const accountNumber = authInfo?.account.account_number || '-'; const pubkey = useAppSelector( - (state) => state.wallet.networks[chainID].walletInfo.pubKey + (state) => state.wallet.networks?.[chainID]?.walletInfo?.pubKey ); const accountInfo: BasicAccountInfo = { pubkey, diff --git a/frontend/src/custom-hooks/useGetAllChainsInfo.ts b/frontend/src/custom-hooks/useGetAllChainsInfo.ts new file mode 100644 index 000000000..8f5006eb4 --- /dev/null +++ b/frontend/src/custom-hooks/useGetAllChainsInfo.ts @@ -0,0 +1,73 @@ +import { useCallback } from 'react'; +import { useAppSelector } from './StateHooks'; +import { RootState } from '@/store/store'; + +export interface DenomInfo { + minimalDenom: string; + decimals: number; + chainName: string; + displayDenom: string; +} + +const useGetAllChainsInfo = () => { + const networks = useAppSelector( + (state: RootState) => state.common.allNetworksInfo + ); + + const getAllDenomInfo = useCallback( + (chainID: string): DenomInfo => { + const config = networks?.[chainID]?.config; + const currency = config?.currencies?.[0]; + const chainName = config?.chainName.toLowerCase(); + + return { + minimalDenom: currency?.coinMinimalDenom, + decimals: currency?.coinDecimals || 0, + chainName, + displayDenom: currency?.coinDenom, + }; + }, + [networks] + ); + + const getAllChainInfo = (chainID: string): AllChainInfo => { + const network = networks[chainID]; + const config = network.config; + const rest = config.rest; + const rpc = config.rpc; + const chainName = config.chainName.toLowerCase(); + + const aminoCfg = network?.aminoConfig; + const prefix = config?.bech32Config.bech32PrefixAccAddr; + const valPrefix = config?.bech32Config.bech32PrefixValAddr; + const feeAmount = config?.feeCurrencies[0].gasPriceStep?.average || 0; + const decimals = config.feeCurrencies[0].coinDecimals || 0; + const feeCurrencies = config?.feeCurrencies; + const explorerTxHashEndpoint = network?.explorerTxHashEndpoint; + const chainLogo = networks[chainID].logos.menu; + + return { + restURLs: config.restURIs, + baseURL: rest, + chainID, + aminoConfig: aminoCfg, + rest, + rpc, + prefix, + feeAmount, + feeCurrencies, + explorerTxHashEndpoint, + chainName, + chainLogo, + decimals, + valPrefix, + }; + }; + + return { + getAllDenomInfo, + getAllChainInfo, + }; +}; + +export default useGetAllChainsInfo; diff --git a/frontend/src/custom-hooks/useGetAssets.ts b/frontend/src/custom-hooks/useGetAssets.ts new file mode 100644 index 000000000..100915802 --- /dev/null +++ b/frontend/src/custom-hooks/useGetAssets.ts @@ -0,0 +1,71 @@ +import { AssetConfig } from '@/types/swaps'; +import { useState } from 'react'; +import { TokenData } from '@0xsquid/sdk/dist/types'; +import axios from 'axios'; +import { SQUID_CLIENT_API, SQUID_ID } from '@/utils/constants'; +import { cleanURL } from '@/utils/util'; + +const useGetAssets = () => { + const [srcAssetsLoading, setSrcAssetsLoading] = useState(false); + const [destAssetLoading, setDestAssetsLoading] = useState(false); + + const fetchAssetsInfo = async (chainID: string, isSource: boolean) => { + try { + if (isSource) { + setSrcAssetsLoading(true); + } else { + setDestAssetsLoading(true); + } + const result = await axios.get( + `${cleanURL(SQUID_CLIENT_API)}/v1/tokens?chainId=${chainID}`, + { + headers: { + 'x-integrator-id': SQUID_ID, + }, + } + ); + const assets: TokenData[] = result.data.tokens; + return assets; + } catch (error) { + console.log('error while fetching data', error); + } finally { + if (isSource) { + setSrcAssetsLoading(false); + } else { + setDestAssetsLoading(false); + } + } + }; + + const getTokensByChainID = async (chainID: string, isSource: boolean) => { + if (!chainID?.length) return []; + const assets = await fetchAssetsInfo(chainID, isSource); + const formattedAssets = assets ? getFormattedAssetsList(assets) : []; + return formattedAssets; + }; + return { + getTokensByChainID, + srcAssetsLoading, + destAssetLoading, + }; +}; + +const getFormattedAssetsList = (data: TokenData[]): AssetConfig[] => { + const assetsList = data + .map((asset): AssetConfig => { + return { + symbol: asset.symbol || '', + label: asset.ibcDenom || '', + logoURI: asset.logoURI || '', + denom: asset.ibcDenom || '', + decimals: asset.decimals || 0, + name: asset.name || '', + }; + }) + .sort((assetA, assetB) => { + return assetA.label.localeCompare(assetB.label); + }); + return assetsList; +}; + +export default useGetAssets; diff --git a/frontend/src/custom-hooks/useGetAssetsAmount.ts b/frontend/src/custom-hooks/useGetAssetsAmount.ts index 76efada90..4edf4559e 100644 --- a/frontend/src/custom-hooks/useGetAssetsAmount.ts +++ b/frontend/src/custom-hooks/useGetAssetsAmount.ts @@ -5,15 +5,19 @@ import { parseBalance } from '@/utils/denom'; import { getIBCBalances } from '@/utils/ibc'; import useGetChainInfo from './useGetChainInfo'; +/* eslint-disable react-hooks/rules-of-hooks */ const useGetAssetsAmount = (chainIDs: string[]) => { + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const stakingChains = useAppSelector( - (state: RootState) => state.staking.chains + (state: RootState) => isAuthzMode? state.staking.authz.chains: state.staking.chains ); + const balanceChains = useAppSelector( - (state: RootState) => state.bank.balances + (state: RootState) => isAuthzMode ? state.bank.authz.balances : state.bank.balances ); const rewardsChains = useAppSelector( - (state: RootState) => state.distribution.chains + (state: RootState) => isAuthzMode ? state.distribution.authzChains : state.distribution.chains ); const tokensPriceInfo = useAppSelector( (state) => state.common.allTokensInfoState.info @@ -26,6 +30,7 @@ const useGetAssetsAmount = (chainIDs: string[]) => { let totalStakedAmount = 0; chainIDs.forEach((chainID) => { const staked = stakingChains?.[chainID]?.delegations?.totalStaked || 0; + if (staked > 0) { const { decimals, minimalDenom } = getDenomInfo(chainID); const usdPriceInfo: TokenInfo | undefined = @@ -34,9 +39,28 @@ const useGetAssetsAmount = (chainIDs: string[]) => { totalStakedAmount += (staked / 10 ** decimals) * usdDenomPrice; } }); + return totalStakedAmount; }, [chainIDs, stakingChains, getDenomInfo, tokensPriceInfo]); + + // calculates un staked amount in usd + const totalUnStakedAmount = useMemo(() => { + let totalUnStakedAmount = 0; + chainIDs.forEach((chainID) => { + const unStaked = stakingChains?.[chainID]?.unbonding?.totalUnbonded || 0; + if (unStaked > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + totalUnStakedAmount += (unStaked / 10 ** decimals) * usdDenomPrice; + } + }); + + return totalUnStakedAmount; + }, [chainIDs, stakingChains, getDenomInfo, tokensPriceInfo]); + // calculates bank balances (native + ibs) in usd const availableAmount: number = useMemo(() => { let totalBalance = 0; @@ -55,12 +79,14 @@ const useGetAssetsAmount = (chainIDs: string[]) => { const usdDenomPrice = usdPriceInfo?.usd || 0; for (let i = 0; i < ibcBalances?.length; i++) { + const ibcUsdDenomPrice = tokensPriceInfo?.[ibcBalances?.[i]?.balance.denom]?.info?.usd || 0 + totalIBCBalance += parseBalance( [ibcBalances[i].balance], ibcBalances?.[i]?.decimals, ibcBalances?.[i]?.balance.denom - ) * usdDenomPrice; + ) * ibcUsdDenomPrice; } const balance = parseBalance( @@ -72,6 +98,7 @@ const useGetAssetsAmount = (chainIDs: string[]) => { if (balanceChains?.[chainID]?.list?.length > 0) { totalBalance += usdDenomPrice * balance; } + }); return totalBalance + totalIBCBalance; @@ -91,10 +118,106 @@ const useGetAssetsAmount = (chainIDs: string[]) => { totalRewardsAmount += (rewards / 10 ** decimals) * usdDenomPrice; } }); + return totalRewardsAmount; }, [chainIDs, rewardsChains, getDenomInfo, tokensPriceInfo]); - return [totalStakedAmount, availableAmount, rewardsAmount]; + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const totalAmountByChain: any = () => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const totalAmountByChainObj: any = {} + + chainIDs.forEach((chainID) => { + let rewardsAmt = 0, availableAmt = 0, stakeAmt = 0, unstakeAmt = 0, ibcAmt = 0; + const rewards = + rewardsChains?.[chainID]?.delegatorRewards?.totalRewards || 0; + if (rewards > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + rewardsAmt = (rewards / 10 ** decimals) * usdDenomPrice || 0; + } + + const { minimalDenom, decimals, chainName } = getDenomInfo(chainID); + const ibcBalances = getIBCBalances( + balanceChains?.[chainID]?.list, + minimalDenom, + chainName + ); + + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + + for (let i = 0; i < ibcBalances?.length; i++) { + const ibcUsdDenomPrice = tokensPriceInfo?.[ibcBalances?.[i]?.balance.denom]?.info?.usd || 0 + + const totalIBCBalance = + parseBalance( + [ibcBalances[i].balance], + ibcBalances?.[i]?.decimals, + ibcBalances?.[i]?.balance.denom + ) * ibcUsdDenomPrice; + + ibcAmt = totalIBCBalance || 0; + } + + const balance = parseBalance( + balanceChains?.[chainID]?.list || [], + decimals, + minimalDenom + ); + + if (balanceChains?.[chainID]?.list?.length > 0) { + const totalBalance = usdDenomPrice * balance; + availableAmt = totalBalance || 0; + } + + + const unStaked = stakingChains?.[chainID]?.unbonding?.totalUnbonded || 0; + if (unStaked > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + const totalUnStakedAmount = (unStaked / 10 ** decimals) * usdDenomPrice; + unstakeAmt = totalUnStakedAmount || 0; + } + + + const staked = stakingChains?.[chainID]?.delegations?.totalStaked || 0; + + if (staked > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + const totalStakedAmount = (staked / 10 ** decimals) * usdDenomPrice; + + stakeAmt = totalStakedAmount || 0; + } + + + + const logoUrl = allNetworks[chainID]?.logos?.menu + const chainConfig = allNetworks[chainID]?.config + totalAmountByChainObj[chainID] = { + total: stakeAmt + rewardsAmt + ibcAmt + unstakeAmt + availableAmt, + logoUrl: logoUrl, + chainName: chainConfig?.chainName, + theme: chainConfig?.theme + } + + }); + + return totalAmountByChainObj + + } + + return [totalStakedAmount, availableAmount, rewardsAmount, totalUnStakedAmount, totalAmountByChain]; }; export default useGetAssetsAmount; diff --git a/frontend/src/custom-hooks/useGetAuthzAssetsAmount.tsx b/frontend/src/custom-hooks/useGetAuthzAssetsAmount.tsx new file mode 100644 index 000000000..8987107c2 --- /dev/null +++ b/frontend/src/custom-hooks/useGetAuthzAssetsAmount.tsx @@ -0,0 +1,220 @@ +import { RootState } from '@/store/store'; +import { useMemo } from 'react'; +import { useAppSelector } from './StateHooks'; +import { parseBalance } from '@/utils/denom'; +import { getIBCBalances } from '@/utils/ibc'; +import useGetChainInfo from './useGetChainInfo'; + +const useGetAuthzAssetsAmount = (chainIDs: string[]) => { + const stakingChains = useAppSelector( + (state: RootState) => state.staking.authz.chains + ); + const balanceChains = useAppSelector( + (state: RootState) => state.bank.authz.balances + ); + const rewardsChains = useAppSelector( + (state: RootState) => state.distribution.authzChains + ); + const tokensPriceInfo = useAppSelector( + (state) => state.common.allTokensInfoState.info + ); + + const { getDenomInfo } = useGetChainInfo(); + + // calculates staked amount in usd + const totalStakedAmount = useMemo(() => { + let totalStakedAmount = 0; + chainIDs.forEach((chainID) => { + const staked = stakingChains?.[chainID]?.delegations?.totalStaked || 0; + if (staked > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + totalStakedAmount += (staked / 10 ** decimals) * usdDenomPrice; + } + }); + return totalStakedAmount; + }, [chainIDs, stakingChains, getDenomInfo, tokensPriceInfo]); + + // calculates bank balances (native + ibs) in usd + const availableAmount: number = useMemo(() => { + let totalBalance = 0; + let totalIBCBalance = 0; + + chainIDs.forEach((chainID) => { + const { minimalDenom, decimals, chainName } = getDenomInfo(chainID); + const ibcBalances = getIBCBalances( + balanceChains?.[chainID]?.list, + minimalDenom, + chainName + ); + + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + + for (let i = 0; i < ibcBalances?.length; i++) { + const ibcUsdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[ibcBalances?.[i]?.balance.denom]?.info; + const ibcUsdDenomPrice = ibcUsdPriceInfo?.usd || 0; + totalIBCBalance += + parseBalance( + [ibcBalances[i].balance], + ibcBalances?.[i]?.decimals, + ibcBalances?.[i]?.balance.denom + ) * ibcUsdDenomPrice; + } + + const balance = parseBalance( + balanceChains?.[chainID]?.list || [], + decimals, + minimalDenom + ); + + if (balanceChains?.[chainID]?.list?.length > 0) { + totalBalance += usdDenomPrice * balance; + } + }); + + return totalBalance + totalIBCBalance; + }, [chainIDs, balanceChains, getDenomInfo, tokensPriceInfo]); + + // calculates rewards amount in usd + const rewardsAmount = useMemo(() => { + let totalRewardsAmount = 0; + chainIDs.forEach((chainID) => { + const rewards = + rewardsChains?.[chainID]?.delegatorRewards?.totalRewards || 0; + if (rewards > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + totalRewardsAmount += (rewards / 10 ** decimals) * usdDenomPrice; + } + }); + return totalRewardsAmount; + }, [chainIDs, rewardsChains, getDenomInfo, tokensPriceInfo]); + + // calculates un staked amount in usd + const totalUnStakedAmount = useMemo(() => { + let totalUnStakedAmount = 0; + chainIDs.forEach((chainID) => { + const unStaked = stakingChains?.[chainID]?.unbonding?.totalUnbonded || 0; + if (unStaked > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + totalUnStakedAmount += (unStaked / 10 ** decimals) * usdDenomPrice; + } + }); + + return totalUnStakedAmount; + }, [chainIDs, stakingChains, getDenomInfo, tokensPriceInfo]); + + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + const totalAmountByChain: any = () => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const totalAmountByChainObj: any = {}; + + chainIDs.forEach((chainID) => { + let rewardsAmt = 0, + availableAmt = 0, + stakeAmt = 0, + unstakeAmt = 0, + ibcAmt = 0; + const rewards = + rewardsChains?.[chainID]?.delegatorRewards?.totalRewards || 0; + if (rewards > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + rewardsAmt = (rewards / 10 ** decimals) * usdDenomPrice || 0; + } + + const { minimalDenom, decimals, chainName } = getDenomInfo(chainID); + const ibcBalances = getIBCBalances( + balanceChains?.[chainID]?.list, + minimalDenom, + chainName + ); + + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + + for (let i = 0; i < ibcBalances?.length; i++) { + const ibcUsdDenomPrice = + tokensPriceInfo?.[ibcBalances?.[i]?.balance.denom]?.info?.usd || 0; + + const totalIBCBalance = + parseBalance( + [ibcBalances[i].balance], + ibcBalances?.[i]?.decimals, + ibcBalances?.[i]?.balance.denom + ) * ibcUsdDenomPrice; + + ibcAmt = totalIBCBalance || 0; + } + + const balance = parseBalance( + balanceChains?.[chainID]?.list || [], + decimals, + minimalDenom + ); + + if (balanceChains?.[chainID]?.list?.length > 0) { + const totalBalance = usdDenomPrice * balance; + availableAmt = totalBalance || 0; + } + + const unStaked = stakingChains?.[chainID]?.unbonding?.totalUnbonded || 0; + if (unStaked > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + const totalUnStakedAmount = (unStaked / 10 ** decimals) * usdDenomPrice; + unstakeAmt = totalUnStakedAmount || 0; + } + + const staked = stakingChains?.[chainID]?.delegations?.totalStaked || 0; + + if (staked > 0) { + const { decimals, minimalDenom } = getDenomInfo(chainID); + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + const totalStakedAmount = (staked / 10 ** decimals) * usdDenomPrice; + + stakeAmt = totalStakedAmount || 0; + } + + const logoUrl = allNetworks[chainID]?.logos?.menu; + const chainConfig = allNetworks[chainID]?.config; + totalAmountByChainObj[chainID] = { + total: stakeAmt + rewardsAmt + ibcAmt + unstakeAmt + availableAmt, + logoUrl: logoUrl, + chainName: chainConfig?.chainName, + theme: chainConfig?.theme, + }; + }); + + return totalAmountByChainObj; + }; + + return [ + totalStakedAmount, + availableAmount, + rewardsAmount, + totalUnStakedAmount, + totalAmountByChain, + ]; +}; + +export default useGetAuthzAssetsAmount; diff --git a/frontend/src/custom-hooks/useGetAuthzRevokeMsgs.ts b/frontend/src/custom-hooks/useGetAuthzRevokeMsgs.ts new file mode 100644 index 000000000..3d2b81325 --- /dev/null +++ b/frontend/src/custom-hooks/useGetAuthzRevokeMsgs.ts @@ -0,0 +1,41 @@ +import useGetChainInfo from './useGetChainInfo'; +import { AuthzRevokeMsg } from '@/txns/authz'; +import useGetFeegranter from './useGetFeegranter'; +import { MAP_TXN_MSG_TYPES } from '@/utils/feegrant'; + +const useGetAuthzRevokeMsgs = ({ + granter, + grantee, + chainID, + typeURLs, +}: { + granter: string; + grantee: string; + chainID: string; + typeURLs: string[]; +}) => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const basicChainInfo = getChainInfo(chainID); + const { decimals, minimalDenom } = getDenomInfo(chainID); + const { getFeegranter } = useGetFeegranter(); + const { feeAmount: avgFeeAmount } = basicChainInfo; + const feeAmount = avgFeeAmount * 10 ** decimals; + + const revokeAuthzMsgs: Msg[] = []; + typeURLs.forEach((typeURL) => { + const msg = AuthzRevokeMsg(granter, grantee, typeURL); + revokeAuthzMsgs.push(msg); + }); + const txRevokeAuthzInputs = { + basicChainInfo: basicChainInfo, + denom: minimalDenom, + feeAmount: feeAmount, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['revoke_authz']), + msgs: revokeAuthzMsgs, + }; + return { + txRevokeAuthzInputs, + }; +}; + +export default useGetAuthzRevokeMsgs; diff --git a/frontend/src/custom-hooks/useGetChainInfo.ts b/frontend/src/custom-hooks/useGetChainInfo.ts index 67957c329..515b2995e 100644 --- a/frontend/src/custom-hooks/useGetChainInfo.ts +++ b/frontend/src/custom-hooks/useGetChainInfo.ts @@ -1,7 +1,8 @@ import { useCallback } from 'react'; import { RootState } from '@/store/store'; import { useAppSelector } from './StateHooks'; -import { COSMOS_CHAIN_ID } from '@/utils/constants'; +import { getAddressByPrefix } from '@/utils/address'; +import { COSMOS_CHAIN_ID, USD_CURRENCY } from '@/utils/constants'; export interface DenomInfo { minimalDenom: string; @@ -12,13 +13,25 @@ export interface DenomInfo { const useGetChainInfo = () => { const networks = useAppSelector((state: RootState) => state.wallet.networks); + const allNetworks = useAppSelector((state) => state.common.allNetworksInfo); + const isWalletConnected = useAppSelector((state) => state.wallet.connected); const balanceChains = useAppSelector( (state: RootState) => state.bank.balances ); + const tokensPriceInfo = useAppSelector( + (state) => state.common.allTokensInfoState.info + ); + + const getCosmosAddress = () => { + const chainID = Object.keys(networks)[0]; + const address = networks?.[chainID]?.walletInfo?.bech32Address; + return getAddressByPrefix(address, 'cosmos'); + }; + const getDenomInfo = useCallback( (chainID: string): DenomInfo => { - const config = networks?.[chainID]?.network?.config; + const config = allNetworks?.[chainID]?.config; const currency = config?.currencies?.[0]; const chainName = config?.chainName.toLowerCase(); @@ -47,21 +60,35 @@ const useGetChainInfo = () => { }; const getChainInfo = (chainID: string): BasicChainInfo => { - const network = networks[chainID].network; - const config = network.config; - const rest = config.rest; - const rpc = config.rpc; - const chainName = config.chainName.toLowerCase(); - - const aminoCfg = network?.aminoConfig; - const cosmosAddress = networks[COSMOS_CHAIN_ID].walletInfo.bech32Address; - const prefix = config?.bech32Config.bech32PrefixAccAddr; + let network: Network; + if (isWalletConnected) { + network = networks?.[chainID]?.network; + } else { + network = allNetworks?.[chainID]; + } + const config = network?.config; + const rest = config?.rest; + const rpc = config?.rpc; + const chainName = config?.chainName.toLowerCase(); + + const aminoCfg = network && network?.aminoConfig; + const cosmosAddress = getCosmosAddress(); + const prefix = config && config?.bech32Config.bech32PrefixAccAddr; + const valPrefix = config && config?.bech32Config.bech32PrefixValAddr; const feeAmount = config?.feeCurrencies[0].gasPriceStep?.average || 0; - const address = networks[chainID]?.walletInfo.bech32Address; + const decimals = config?.feeCurrencies[0].coinDecimals || 0; + const address = networks?.[chainID]?.walletInfo?.bech32Address || ''; const feeCurrencies = config?.feeCurrencies; const explorerTxHashEndpoint = network?.explorerTxHashEndpoint; + const chainLogo = network?.logos?.menu; + const govV1 = network?.govV1; + const isCustomNetwork = network?.isCustomNetwork; + const enableModules = network?.enableModules; + const supportedWallets = network?.supportedWallets; return { + restURLs: config?.restURIs, + rpcURLs: config?.rpcURIs, baseURL: rest, chainID, aminoConfig: aminoCfg, @@ -74,6 +101,13 @@ const useGetChainInfo = () => { feeCurrencies, explorerTxHashEndpoint, chainName, + chainLogo, + decimals, + valPrefix, + govV1, + isCustomNetwork, + enableModules, + supportedWallets, }; }; @@ -128,6 +162,100 @@ const useGetChainInfo = () => { return ''; }; + const getAllChainAddresses = (chainIDs: string[]) => { + let addresses: { + chain_id: string; + address: string; + }[] = []; + chainIDs.forEach((chainID) => { + const { address } = getChainInfo(chainID); + addresses = [...addresses, { chain_id: chainID, address: address }]; + }); + + return addresses; + }; + + const getChainNamesAndLogos = () => { + if (isWalletConnected) { + const chainIDs = Object.keys(networks); + const chainNamesAndLogos = chainIDs.map((chainID) => { + const { chainName } = networks?.[chainID].network.config; + const { chainLogo, isCustomNetwork } = getChainInfo(chainID); + return { chainID, chainName, chainLogo, isCustomNetwork }; + }); + return chainNamesAndLogos; + } else { + const chainIDs = Object.keys(allNetworks); + const chainNamesAndLogos = chainIDs.map((chainID) => { + const { isCustomNetwork } = allNetworks?.[chainID]; + const { chainName } = allNetworks?.[chainID].config; + const { menu: chainLogo } = allNetworks?.[chainID].logos; + return { chainID, chainName, chainLogo, isCustomNetwork }; + }); + return chainNamesAndLogos; + } + }; + + // will return actual value of token with denomination and + // usd value based on amount and minimal denom + + const getValueFromToken = ( + chainId: string, + amount: number, + denom: string + ) => { + const denomInfo = getOriginDenomInfo(denom); + + const tokenPrice = tokensPriceInfo[denom]?.info?.[USD_CURRENCY]; + + return { + amount: amount * 10 ** -denomInfo.decimals, + displayDenom: denomInfo.originDenom, + usdValue: amount * tokenPrice, + }; + }; + + const getTokenValueByChainId = (chainID: string, amount: number) => { + const denomInfo = getDenomInfo(chainID); + + const tokenPrice = + tokensPriceInfo[denomInfo.minimalDenom]?.info?.[USD_CURRENCY]; + + return { + amount: amount * 10 ** -denomInfo.decimals, + displayDenom: denomInfo.displayDenom, + usdValue: amount * tokenPrice, + }; + }; + + const getNetworkTheme = (chainID: string) => { + const theme = allNetworks?.[chainID]?.config?.theme; + return { + primaryColor: theme?.primaryColor || '', + gradient: theme?.gradient || '', + }; + }; + + const convertToCosmosAddress = (address: string) => { + if (address?.length) { + const { prefix } = getChainInfo(COSMOS_CHAIN_ID); + const cosmosAddress = getAddressByPrefix(address, prefix); + return cosmosAddress; + } + return address || ''; + }; + + const getCustomNetworks = () => { + const customNetworks: string[] = []; + if (isWalletConnected) { + return Object.keys(networks).filter((chainID) => { + const { isCustomNetwork } = getChainInfo(chainID); + return isCustomNetwork; + }); + } + return customNetworks; + }; + return { getDenomInfo, getChainInfo, @@ -135,6 +263,14 @@ const useGetChainInfo = () => { isNativeTransaction, getChainIDFromAddress, isFeeAvailable, + getCosmosAddress, + getAllChainAddresses, + getChainNamesAndLogos, + getValueFromToken, + getTokenValueByChainId, + getNetworkTheme, + convertToCosmosAddress, + getCustomNetworks, }; }; diff --git a/frontend/src/custom-hooks/useGetChains.ts b/frontend/src/custom-hooks/useGetChains.ts new file mode 100644 index 000000000..50c7a179f --- /dev/null +++ b/frontend/src/custom-hooks/useGetChains.ts @@ -0,0 +1,63 @@ +import { ChainConfig } from '@/types/swaps'; +import { SQUID_CHAINS_API, SQUID_ID } from '@/utils/constants'; +import axios from 'axios'; +import { useEffect, useState } from 'react'; + +const useGetChains = () => { + const [chainsInfo, setChainInfo] = useState([]); + const [chainsData, setChainsData] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchChainsInfo(); + }, []); + + const fetchChainsInfo = async () => { + try { + const result = await axios.get(SQUID_CHAINS_API, { + headers: { + 'x-integrator-id': SQUID_ID, + }, + }); + const chains: ChainData[] = result.data.chains; + setChainsData(chains); + const chainsData = chains + .filter((chain) => chain.chainType === 'cosmos') // To filter cosmos chains + .map((chain): ChainConfig => { + return { + label: chain.axelarChainName, + logoURI: chain.chainIconURI || '', + chainID: chain.chainId, + }; + }) + .sort((chainA, chainB) => { + return chainA.label.localeCompare(chainB.label); + }); + setChainInfo(chainsData); + } catch (error) { + console.log('error while fetching data', error); + } finally { + setLoading(false); + } + }; + + const getChainConfig = (chainID: string) => { + const chainConfig = chainsData.filter((chain) => chain.chainId === chainID); + return chainConfig[0]; + }; + + const getChainLogoURI = (chainID: string) => { + const chainConfig = getChainConfig(chainID); + const logoURI = chainConfig?.chainIconURI || ''; + return logoURI; + }; + + return { + loading, + chainsInfo, + getChainConfig, + getChainLogoURI, + }; +}; + +export default useGetChains; diff --git a/frontend/src/custom-hooks/useGetCreateFeegrantTxLoading.ts b/frontend/src/custom-hooks/useGetCreateFeegrantTxLoading.ts new file mode 100644 index 000000000..585407d70 --- /dev/null +++ b/frontend/src/custom-hooks/useGetCreateFeegrantTxLoading.ts @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { useAppDispatch } from './StateHooks'; +import { txCreateFeegrant } from '@/store/features/feegrant/feegrantSlice'; + +const useMultiTxTracker = () => { + const dispatch = useAppDispatch(); + // tracker : Map & count: pending txns count + /* eslint-disable @typescript-eslint/no-explicit-any */ + const [chainsStatus, setChainsStatus] = useState>( + {} + ); + const [currentTxCount, setCurrentTxCount] = useState(0); + const reset = () => { + setChainsStatus({}); + setCurrentTxCount(0); + }; + const updateChainStatus = ({ + chainID, + isTxSuccess, + error, + txHash, + }: { + chainID: string; + isTxSuccess: boolean; + error?: string; + txHash?: string; + }) => { + setChainsStatus((chainsStatus) => { + chainsStatus[chainID] = { + isTxSuccess: isTxSuccess, + error: error || '', + txHash: txHash || '', + txStatus: 'idle', + }; + return chainsStatus; + }); + setCurrentTxCount((count) => count - 1); + if (currentTxCount === 1) { + reset(); + } + }; + const trackTxs = (chains: MultiChainFeegrantTx[]) => { + reset(); + setCurrentTxCount(chains.length); + chains.forEach((chain) => { + /* Track started */ + setChainsStatus((chainsStatus) => { + chainsStatus[chain.ChainID] = { + error: '', + txHash: '', + txStatus: 'pending', + }; + return chainsStatus; + }); + + /* the below curried callback can use txInputs(chain.txInputs) of this context in case needed + this will be called inside the redux slice after tx is done (full-filled or rejected) */ + const onTxComplete = ({ + isTxSuccess, + error, + txHash, + }: OnTxnCompleteInputs) => { + updateChainStatus({ + chainID: chain.ChainID, + isTxSuccess: isTxSuccess, + error: error, + txHash: txHash, + }); + }; + chain.txInputs.onTxComplete = onTxComplete; + /* dispatch the tx along with the above callBack*/ + dispatch(txCreateFeegrant(chain.txInputs)); + }); + }; + /* + trackTxns is a method to dispatch and track txns + chainSStatus to check for txns status + currentTxCount to check for how many txns are completed + */ + return { trackTxs, chainsStatus, currentTxCount }; +}; +export default useMultiTxTracker; diff --git a/frontend/src/custom-hooks/useGetDistributionMsgs.ts b/frontend/src/custom-hooks/useGetDistributionMsgs.ts new file mode 100644 index 000000000..5d05b5c76 --- /dev/null +++ b/frontend/src/custom-hooks/useGetDistributionMsgs.ts @@ -0,0 +1,119 @@ +import { useAppSelector } from './StateHooks'; +import { RootState } from '@/store/store'; +import { + EncodedWithdrawValidatorCommissionMsg, + WithdrawValidatorCommissionMsg, +} from '@/txns/distribution/withDrawValidatorCommission'; +import useGetTxInputs from './useGetTxInputs'; +import { + EncodedWithdrawAllRewardsMsg, + WithdrawAllRewardsMsg, +} from '@/txns/distribution/withDrawRewards'; +import { + DelegationsPairs, + TxWithdrawAllRewardsInputs, +} from '@/types/distribution'; +import useGetChainInfo from './useGetChainInfo'; +import { + EncodedSetWithdrawAddressMsg, + SetWithdrawAddressMsg, +} from '@/txns/distribution/setWithdrawAddress'; + +const useGetDistributionMsgs = () => { + const { txWithdrawAllRewardsInputs } = useGetTxInputs(); + const stakingData = useAppSelector( + (state: RootState) => state.staking.chains + ); + const authzStakingData = useAppSelector( + (state) => state.staking.authz.chains + ); + const authzRewards = useAppSelector( + (state) => state.distribution.authzChains + ); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const { getChainInfo } = useGetChainInfo(); + + const getWithdrawCommissionAndRewardsMsgs = ({ + chainID, + }: { + chainID: string; + }) => { + const msgs = [ + ...getWithdrawCommissionMsgs({ chainID }), + ...getWithdrawRewardsMsgs({ chainID }), + ]; + return msgs; + }; + + const getWithdrawCommissionMsgs = ({ chainID }: { chainID: string }) => { + const validator = isAuthzMode + ? authzStakingData?.[chainID].validator + : stakingData?.[chainID].validator; + const msgs = []; + const withdrawCommissionMsg = isAuthzMode + ? EncodedWithdrawValidatorCommissionMsg( + validator.validatorInfo?.operator_address || '' + ) + : WithdrawValidatorCommissionMsg( + validator.validatorInfo?.operator_address || '' + ); + msgs.push(withdrawCommissionMsg); + return msgs; + }; + + const getWithdrawRewardsMsgs = ({ chainID }: { chainID: string }) => { + const msgs = []; + + let delegationPairs: DelegationsPairs[] | TxWithdrawAllRewardsInputs; + if (isAuthzMode) { + delegationPairs = ( + authzRewards[chainID]?.delegatorRewards?.list || [] + ).map((reward) => { + const pair = { + delegator: authzAddress, + validator: reward.validator_address, + }; + return pair; + }); + for (let i = 0; i < delegationPairs.length; i++) { + const msg = delegationPairs[i]; + msgs.push(EncodedWithdrawAllRewardsMsg(msg.delegator, msg.validator)); + } + } else { + delegationPairs = txWithdrawAllRewardsInputs(chainID); + for (let i = 0; i < delegationPairs.msgs.length; i++) { + const msg = delegationPairs.msgs[i]; + msgs.push(WithdrawAllRewardsMsg(msg.delegator, msg.validator)); + } + } + + return msgs; + }; + + const getSetWithdrawAddressMsg = ({ + chainID, + withdrawAddress, + }: { + chainID: string; + withdrawAddress: string; + }) => { + const { address } = getChainInfo(chainID); + let msg; + if (isAuthzMode) { + msg = EncodedSetWithdrawAddressMsg(authzAddress, withdrawAddress); + } else { + msg = SetWithdrawAddressMsg(address, withdrawAddress); + } + return msg; + }; + + return { + getWithdrawCommissionMsgs, + getWithdrawRewardsMsgs, + getSetWithdrawAddressMsg, + getWithdrawCommissionAndRewardsMsgs, + }; +}; + +export default useGetDistributionMsgs; diff --git a/frontend/src/custom-hooks/useGetFeegrantMsgs.ts b/frontend/src/custom-hooks/useGetFeegrantMsgs.ts new file mode 100644 index 000000000..a88417cc8 --- /dev/null +++ b/frontend/src/custom-hooks/useGetFeegrantMsgs.ts @@ -0,0 +1,105 @@ +import { FieldValues } from 'react-hook-form'; +import useGetChainInfo from './useGetChainInfo'; +import { FeegrantBasicMsg, FeegrantPeriodicMsg } from '@/txns/feegrant'; +import { getAddressByPrefix } from '@/utils/address'; +import { amountToMinimalValue } from '@/utils/util'; +import { FeegrantFilterMsg } from '@/txns/feegrant/grant'; +import { getMsgListFromMsgNames } from '@/utils/feegrant'; +import { useAppSelector } from './StateHooks'; + +interface ChainGrant { + chainID: string; + msg: Msg; +} + +const useGetFeegrantMsgs = () => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + + const getFeegrantMsgs = ({ + isFiltered, + msgsList, + selectedChains, + isPeriodic, + fieldValues, + }: { + isFiltered: boolean; + msgsList: string[]; + selectedChains: string[]; + isPeriodic: boolean; + fieldValues: FieldValues; + }) => { + const chainWiseGrants: ChainGrant[] = []; + selectedChains.forEach((chain) => { + const chainID = nameToChainIDs?.[chain.toLowerCase()]; + const { address: granterAddress, prefix } = getChainInfo(chainID); + const granteeAddress = fieldValues?.grantee_address || ''; + const grantee = getAddressByPrefix(granteeAddress, prefix); + const { minimalDenom, decimals } = getDenomInfo(chainID); + const expiration = fieldValues.expiration.toISOString(); + let msg: Msg; + if (isFiltered) { + const msgTypeURLs = getMsgListFromMsgNames(msgsList); + const allowanceType = isPeriodic ? 'Periodic' : 'Basic'; + msg = FeegrantFilterMsg( + granterAddress, + grantee, + minimalDenom, + amountToMinimalValue(Number(fieldValues.spend_limit), decimals), + Number(fieldValues.period), + amountToMinimalValue( + Number(fieldValues.period_spend_limit), + decimals + ), + expiration, + msgTypeURLs, + allowanceType + ); + chainWiseGrants.push({ + chainID: chainID, + msg: msg, + }); + } else if (isPeriodic) { + msg = FeegrantPeriodicMsg( + granterAddress, + grantee, + minimalDenom, + amountToMinimalValue(Number(fieldValues.spend_limit), decimals), + Number(fieldValues.period), + amountToMinimalValue( + Number(fieldValues.period_spend_limit), + decimals + ), + expiration + ); + chainWiseGrants.push({ + chainID: chainID, + msg: msg, + }); + } else { + msg = FeegrantBasicMsg( + granterAddress, + grantee, + minimalDenom, + amountToMinimalValue( + Number(fieldValues.spend_limit), + decimals + ).toString(), + expiration + ); + chainWiseGrants.push({ + chainID: chainID, + msg: msg, + }); + } + }); + + return { + chainWiseGrants, + }; + }; + + return { getFeegrantMsgs }; +}; + +export default useGetFeegrantMsgs; diff --git a/frontend/src/custom-hooks/useGetFeegranter.tsx b/frontend/src/custom-hooks/useGetFeegranter.tsx new file mode 100644 index 000000000..bbf823ac2 --- /dev/null +++ b/frontend/src/custom-hooks/useGetFeegranter.tsx @@ -0,0 +1,47 @@ +import useGetChainInfo from './useGetChainInfo'; +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { getAddressByPrefix } from '@/utils/address'; +import { isFeegrantAvailable } from '@/utils/feegrant'; +import { setError } from '@/store/features/common/commonSlice'; + +const useGetFeegranter = () => { + const feegranter = useAppSelector((state) => state.feegrant.feegrantAddress); + const chainFeegrants = useAppSelector((state) => state.feegrant.chains); + const isFeegrantMode = useAppSelector( + (state) => state.feegrant.feegrantModeEnabled + ); + const { getChainInfo } = useGetChainInfo(); + const dispatch = useAppDispatch(); + + const getFeegranter = (chainID: string, txnMsg: string) => { + if (!isFeegrantMode) { + return ''; + } + const feegrants = chainFeegrants?.[chainID]?.grantsToMeAddressMapping; + if (!feegranter?.length) { + return ''; + } + const { prefix } = getChainInfo(chainID); + const feegranterAddress = getAddressByPrefix(feegranter, prefix); + if (!feegrants?.[feegranterAddress]) { + return ''; + } + const feegrant = feegrants?.[feegranterAddress]?.[0]; + if (isFeegrantAvailable(feegrant, txnMsg)) { + return feegranterAddress; + } else { + dispatch( + setError({ + type: 'info', + message: + 'You are not having feegrant to this transaction. Fee will be deducted from your account', + }) + ); + } + return ''; + }; + + return { getFeegranter }; +}; + +export default useGetFeegranter; diff --git a/frontend/src/custom-hooks/useGetGrantAuthzMsgs.ts b/frontend/src/custom-hooks/useGetGrantAuthzMsgs.ts new file mode 100644 index 000000000..d59b2f3d0 --- /dev/null +++ b/frontend/src/custom-hooks/useGetGrantAuthzMsgs.ts @@ -0,0 +1,114 @@ +import { AuthzGenericGrantMsg, AuthzSendGrantMsg } from '@/txns/authz'; +import useGetChainInfo from './useGetChainInfo'; +import { MAP_TXN_MSG_TYPES } from '@/utils/authorizations'; +import { getAddressByPrefix } from '@/utils/address'; +import { AuthzStakeGrantMsg } from '@/txns/authz/grant'; +import { amountToMinimalValue } from '@/utils/util'; +import { useAppSelector } from './StateHooks'; + +interface ChainGrants { + chainID: string; + msgs: Msg[]; +} + +const MAP_STAKE_AUTHZ_TYPE: Record = { + delegate: 1, + undelegate: 2, + redelegate: 3, +}; + +const useGetGrantAuthzMsgs = () => { + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + const nameToChainIDs = useAppSelector((state) => state.common.nameToChainIDs); + + const getGrantAuthzMsgs = ({ + grantsList, + selectedChains, + granteeAddress, + }: { + grantsList: Grant[]; + selectedChains: string[]; + granteeAddress: string; + }) => { + const chainWiseGrants: ChainGrants[] = []; + + selectedChains.forEach((chain) => { + const chainID = nameToChainIDs?.[chain.toLowerCase()] || ''; + const { address: granterAddress, prefix } = getChainInfo(chainID); + const { minimalDenom, decimals } = getDenomInfo(chainID); + const msgs: Msg[] = []; + const sendAuthz = 'send'; + const stakeAuthzs = ['delegate', 'undelegate', 'redelegate']; + grantsList.forEach((grant) => { + const grantee = getAddressByPrefix(granteeAddress, prefix); + const typeUrl = MAP_TXN_MSG_TYPES[grant.msg]; + const expiration = grant.expiration.toISOString(); + if ( + grant.msg === sendAuthz && + 'spend_limit' in grant && + grant.spend_limit + ) { + const msg = AuthzSendGrantMsg( + granterAddress, + grantee, + minimalDenom, + amountToMinimalValue(Number(grant.spend_limit), decimals), + expiration + ); + msgs.push(msg); + } else if (stakeAuthzs.includes(grant.msg)) { + if ( + stakeAuthzs.includes(grant.msg) && + 'validators_list' in grant && + 'isDenyList' in grant && + grant.validators_list?.length + ) { + const msg = AuthzStakeGrantMsg({ + expiration: expiration, + granter: granterAddress, + grantee: grantee, + stakeAuthzType: MAP_STAKE_AUTHZ_TYPE[grant.msg], + allowList: grant.isDenyList ? undefined : grant.validators_list, + denyList: grant.isDenyList ? grant.validators_list : undefined, + denom: minimalDenom, + maxTokens: grant?.max_tokens + ? amountToMinimalValue( + Number(grant?.max_tokens), + decimals + ).toString() + : undefined, + }); + msgs.push(msg); + } else { + const msg = AuthzGenericGrantMsg( + granterAddress, + grantee, + typeUrl, + expiration + ); + msgs.push(msg); + } + } else { + const msg = AuthzGenericGrantMsg( + granterAddress, + grantee, + typeUrl, + expiration + ); + msgs.push(msg); + } + }); + chainWiseGrants.push({ + chainID: chainID, + msgs: msgs, + }); + }); + return { + chainWiseGrants, + }; + }; + + return { getGrantAuthzMsgs }; +}; + +export default useGetGrantAuthzMsgs; diff --git a/frontend/src/custom-hooks/useGetMultiChainTxLoading.ts b/frontend/src/custom-hooks/useGetMultiChainTxLoading.ts new file mode 100644 index 000000000..bfe888e84 --- /dev/null +++ b/frontend/src/custom-hooks/useGetMultiChainTxLoading.ts @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { useAppDispatch } from './StateHooks'; +import { txCreateAuthzGrant } from '@/store/features/authz/authzSlice'; + +const useMultiTxTracker = () => { + const dispatch = useAppDispatch(); + // tracker : Map & count: pending txns count + /* eslint-disable @typescript-eslint/no-explicit-any */ + const [ChainsStatus, setChainsStatus] = useState>( + {} + ); + const [currentTxCount, setCurrentTxCount] = useState(0); + const reset = () => { + setChainsStatus({}); + setCurrentTxCount(0); + }; + const updateChainStatus = ({ + chainID, + isTxSuccess, + error, + txHash, + }: { + chainID: string; + isTxSuccess: boolean; + error?: string; + txHash?: string; + }) => { + setChainsStatus((chainsStatus) => { + chainsStatus[chainID] = { + isTxSuccess: isTxSuccess, + error: error || '', + txHash: txHash || '', + txStatus: 'idle', + }; + return chainsStatus; + }); + setCurrentTxCount((count) => count - 1); + if (currentTxCount === 1) { + reset(); + } + }; + const trackTxs = (chains: MultiChainTx[]) => { + reset(); + setCurrentTxCount(chains.length); + chains.forEach((chain) => { + /* Track started */ + setChainsStatus((chainsStatus) => { + chainsStatus[chain.ChainID] = { + error: '', + txHash: '', + txStatus: 'pending', + }; + return chainsStatus; + }); + + /* the below curried callback can use txInputs(chain.txInputs) of this context in case needed + this will be called inside the redux slice after tx is done (full-filled or rejected) */ + const onTxComplete = ({ + isTxSuccess, + error, + txHash, + }: OnTxnCompleteInputs) => { + updateChainStatus({ + chainID: chain.ChainID, + isTxSuccess: isTxSuccess, + error: error, + txHash: txHash, + }); + }; + chain.txInputs.onTxComplete = onTxComplete; + /* dispatch the tx along with the above callBack*/ + dispatch(txCreateAuthzGrant(chain.txInputs)); + }); + }; + /* + trackTxns is a method to dispatch and track txns + chainSStatus to check for txns status + currentTxCount to check for how many txns are completed + */ + return { trackTxs, ChainsStatus, currentTxCount }; +}; +export default useMultiTxTracker; diff --git a/frontend/src/custom-hooks/useGetPubkey.ts b/frontend/src/custom-hooks/useGetPubkey.ts new file mode 100644 index 000000000..2195fc411 --- /dev/null +++ b/frontend/src/custom-hooks/useGetPubkey.ts @@ -0,0 +1,32 @@ +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; +import { useState } from 'react'; + +const useGetPubkey = () => { + const [pubkeyLoading, setPubkeyLoading] = useState(false); + const getPubkey = async (address: string, baseURLs: string[]) => { + try { + setPubkeyLoading(true); + const { status, data } = await axiosGetRequestWrapper( + baseURLs, + `/cosmos/auth/v1beta1/accounts/${address}` + ); + + if (status === 200) { + return data.account.pub_key.key || ''; + } else { + return ''; + } + } catch (error) { + console.log(error); + return ''; + } finally { + setPubkeyLoading(false); + } + }; + return { + pubkeyLoading, + getPubkey, + }; +}; + +export default useGetPubkey; diff --git a/frontend/src/custom-hooks/useGetShowAuthzAlert.ts b/frontend/src/custom-hooks/useGetShowAuthzAlert.ts new file mode 100644 index 000000000..37525b562 --- /dev/null +++ b/frontend/src/custom-hooks/useGetShowAuthzAlert.ts @@ -0,0 +1,26 @@ +import { useAppSelector } from './StateHooks'; +import useAuthzGrants from './useAuthzGrants'; +import { getAuthzAlertData } from '@/utils/localStorage'; + +const useGetShowAuthzAlert = () => { + const showAuthzGrantsAlert = useAppSelector( + (state) => state.authz.authzAlert.display + ); + const { getSendAuthzGrants } = useAuthzGrants(); + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const chainIDs = Object.keys(nameToChainIDs).map( + (chainName) => nameToChainIDs[chainName] + ); + + const sendGrantsData = getSendAuthzGrants(chainIDs); + const showAuthzAlert = + showAuthzGrantsAlert && + (sendGrantsData.ibcTransfer > 0 || sendGrantsData.send > 0) && + getAuthzAlertData() && + !isAuthzMode; + + return showAuthzAlert; +}; + +export default useGetShowAuthzAlert; diff --git a/frontend/src/custom-hooks/useGetTransactions.ts b/frontend/src/custom-hooks/useGetTransactions.ts new file mode 100644 index 000000000..2f425dd95 --- /dev/null +++ b/frontend/src/custom-hooks/useGetTransactions.ts @@ -0,0 +1,45 @@ +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { + getAllTransactions, + getTransaction, +} from '@/store/features/recent-transactions/recentTransactionsSlice'; +import useGetChainInfo from './useGetChainInfo'; + +const useGetTransactions = ({ chainID }: { chainID: string }) => { + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { address } = getChainInfo(chainID); + const txns = useAppSelector((state) => state.recentTransactions.txns.data); + const getTransactions = () => { + return { + txns, + }; + }; + + const fetchTransactions = (limit: number, offset: number) => { + dispatch( + getAllTransactions({ + address, + chainID, + limit: limit, + offset: offset, + }) + ); + }; + + const fetchTransaction = (txhash: string) => { + if (chainID) { + dispatch( + getTransaction({ + address, + chainID, + txhash, + }) + ); + } + }; + + return { getTransactions, fetchTransactions, fetchTransaction }; +}; + +export default useGetTransactions; diff --git a/frontend/src/custom-hooks/useGetTxInputs.ts b/frontend/src/custom-hooks/useGetTxInputs.ts index ada399b89..27a9b419b 100644 --- a/frontend/src/custom-hooks/useGetTxInputs.ts +++ b/frontend/src/custom-hooks/useGetTxInputs.ts @@ -2,11 +2,18 @@ import { RootState } from '@/store/store'; import { useAppSelector } from './StateHooks'; import { DelegationsPairs, + TxSetWithdrawAddressInputs, + TxWithDrawValidatorCommissionAndRewardsInputs, TxWithdrawAllRewardsInputs, } from '@/types/distribution'; import useGetChainInfo from './useGetChainInfo'; import { TxReStakeInputs } from '@/types/staking'; import { Delegate } from '@/txns/staking'; +import useAddressConverter from './useAddressConverter'; +import { EncodeDelegate } from '@/txns/staking/delegate'; +import useGetFeegranter from './useGetFeegranter'; +import { MAP_TXN_MSG_TYPES } from '@/utils/feegrant'; +import { SetWithdrawAddressMsg } from '@/txns/distribution/setWithdrawAddress'; const useGetTxInputs = () => { const stakingChains = useAppSelector( @@ -15,7 +22,14 @@ const useGetTxInputs = () => { const rewardsChains = useAppSelector( (state: RootState) => state.distribution.chains ); + const authzRewardsChains = useAppSelector( + (state) => state.distribution.authzChains + ); + const { convertAddress } = useAddressConverter(); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); const { getDenomInfo, getChainInfo } = useGetChainInfo(); + const { getFeegranter } = useGetFeegranter(); const txWithdrawAllRewardsInputs = ( chainID: string @@ -32,10 +46,19 @@ const useGetTxInputs = () => { }); const { minimalDenom, decimals } = getDenomInfo(chainID); const basicChainInfo = getChainInfo(chainID); - const { aminoConfig, prefix, rest, feeAmount, address, cosmosAddress } = - basicChainInfo; + const { + aminoConfig, + prefix, + rest, + feeAmount, + address, + cosmosAddress, + rpc, + } = basicChainInfo; return { + isAuthzMode: false, + basicChainInfo, msgs: delegationPairs, denom: minimalDenom, chainID, @@ -43,9 +66,10 @@ const useGetTxInputs = () => { prefix, rest, feeAmount: feeAmount * 10 ** decimals, - feegranter: '', + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['withdraw_rewards']), address, cosmosAddress, + rpc, }; }; @@ -65,6 +89,7 @@ const useGetTxInputs = () => { const { minimalDenom, decimals } = denomInfo; return { + basicChainInfo, aminoConfig, prefix, rest, @@ -90,10 +115,19 @@ const useGetTxInputs = () => { const { minimalDenom, decimals } = getDenomInfo(chainID); const basicChainInfo = getChainInfo(chainID); - const { aminoConfig, prefix, rest, feeAmount, address, cosmosAddress } = - basicChainInfo; + const { + aminoConfig, + prefix, + rest, + feeAmount, + address, + rpc, + cosmosAddress, + } = basicChainInfo; return { + isAuthzMode: false, + basicChainInfo, msgs: delegationPairs, denom: minimalDenom, chainID, @@ -101,9 +135,10 @@ const useGetTxInputs = () => { prefix, rest, feeAmount: feeAmount * 10 ** decimals, - feegranter: '', + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['withdraw_rewards']), address, cosmosAddress, + rpc, }; }; @@ -132,12 +167,13 @@ const useGetTxInputs = () => { } return { + isAuthzMode: false, msgs: msgs, basicChainInfo, memo: '', denom: minimalDenom, feeAmount: basicChainInfo.feeAmount * 10 ** decimals, - feegranter: '', + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['delegate']), }; }; @@ -166,12 +202,13 @@ const useGetTxInputs = () => { } return { + isAuthzMode: false, msgs: msgs, basicChainInfo, memo: '', denom: minimalDenom, feeAmount: basicChainInfo.feeAmount * 10 ** decimals, - feegranter: '', + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['delegate']), }; }; @@ -186,6 +223,7 @@ const useGetTxInputs = () => { const basicChainInfo = getChainInfo(chainID); const { minimalDenom } = getDenomInfo(chainID); return { + isAuthzMode: false, basicChainInfo, from: basicChainInfo.address, to: recipient, @@ -193,7 +231,7 @@ const useGetTxInputs = () => { amount: amount * 10 ** decimals, denom: minimalDenom, feeAmount: basicChainInfo.feeAmount * 10 ** decimals, - feegranter: '', + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['send']), memo, prefix: basicChainInfo.prefix, }; @@ -220,11 +258,148 @@ const useGetTxInputs = () => { from: sourceBasicChainInfo.address, to, rest: sourceBasicChainInfo.rest, + restURLs: sourceBasicChainInfo.restURLs, }; return transfersRequestInputs; }; + const txAuthzRestakeMsgs = (chainID: string): Msg[] => { + const { minimalDenom } = getDenomInfo(chainID); + const rewards = authzRewardsChains[chainID]?.delegatorRewards; + const msgs: Msg[] = []; + if (!isAuthzMode) return []; + const delegator = convertAddress(chainID, authzAddress); + + for (const delegation of rewards?.list || []) { + for (const reward of delegation.reward || []) { + if (reward.denom === minimalDenom) { + const amount = parseInt(reward.amount); + if (amount < 1) continue; + const msg = EncodeDelegate( + delegator, + delegation.validator_address, + amount, + minimalDenom + ); + msgs.push(msg); + } + } + } + return msgs; + }; + + const txAuthzRestakeValidatorMsgs = ( + chainID: string, + validatorAddress: string + ): Msg[] => { + if (!isAuthzMode) return []; + const { minimalDenom } = getDenomInfo(chainID); + const rewards = authzRewardsChains[chainID]?.delegatorRewards; + const msgs: Msg[] = []; + const delegator = convertAddress(chainID, authzAddress); + + for (const delegation of rewards.list) { + if (delegation?.validator_address === validatorAddress) { + for (const reward of delegation.reward || []) { + if (reward.denom === minimalDenom) { + const amount = parseInt(reward.amount, 10); + if (amount < 1) continue; + msgs.push( + EncodeDelegate(delegator, validatorAddress, amount, minimalDenom) + ); + } + } + } + } + + return msgs; + }; + + const txSetWithdrawAddressInputs = ( + chainID: string, + delegatorAddress: string, + withdrawAddress: string + ): TxSetWithdrawAddressInputs => { + const msg = SetWithdrawAddressMsg(delegatorAddress, withdrawAddress); + + const { minimalDenom, decimals } = getDenomInfo(chainID); + const basicChainInfo = getChainInfo(chainID); + const { + aminoConfig, + prefix, + rest, + feeAmount, + address, + rpc, + cosmosAddress, + } = basicChainInfo; + + return { + isAuthzMode: false, + basicChainInfo, + msgs: [msg], + denom: minimalDenom, + chainID, + aminoConfig, + prefix, + rest, + feeAmount: feeAmount * 10 ** decimals, + feegranter: getFeegranter( + chainID, + MAP_TXN_MSG_TYPES['set_withdraw_address'] + ), + address, + cosmosAddress, + rpc, + }; + }; + + const txWithdrawCommissionAndRewardsInputs = ( + chainID: string, + msgs: Msg[] + ): TxWithDrawValidatorCommissionAndRewardsInputs => { + const { minimalDenom, decimals } = getDenomInfo(chainID); + const basicChainInfo = getChainInfo(chainID); + const { + aminoConfig, + prefix, + rest, + feeAmount, + address, + rpc, + cosmosAddress, + } = basicChainInfo; + + const withdraw_rewards_feegrant = getFeegranter( + chainID, + MAP_TXN_MSG_TYPES['withdraw_rewards'] + ); + const withdraw_commission_feegrant = getFeegranter( + chainID, + MAP_TXN_MSG_TYPES['withdraw_commission'] + ); + + return { + isAuthzMode: false, + basicChainInfo, + msgs: msgs, + denom: minimalDenom, + chainID, + aminoConfig, + prefix, + rest, + feeAmount: feeAmount * 10 ** decimals, + feegranter: + withdraw_rewards_feegrant && withdraw_commission_feegrant + ? withdraw_commission_feegrant + : '', + address, + cosmosAddress, + rpc, + }; + }; + return { txWithdrawAllRewardsInputs, txRestakeInputs, @@ -233,6 +408,10 @@ const useGetTxInputs = () => { txSendInputs, getVoteTxInputs, txTransferInputs, + txAuthzRestakeMsgs, + txAuthzRestakeValidatorMsgs, + txSetWithdrawAddressInputs, + txWithdrawCommissionAndRewardsInputs, }; }; diff --git a/frontend/src/custom-hooks/useGetValidatorInfo.ts b/frontend/src/custom-hooks/useGetValidatorInfo.ts new file mode 100644 index 000000000..c9a2041c2 --- /dev/null +++ b/frontend/src/custom-hooks/useGetValidatorInfo.ts @@ -0,0 +1,328 @@ +import { useAppSelector } from './StateHooks'; +import { RootState } from '@/store/store'; +import { ValidatorProfileInfo } from '@/types/staking'; +import { getValidatorRank } from '@/utils/util'; +import { parseBalance } from '@/utils/denom'; +import useGetAllChainsInfo from './useGetAllChainsInfo'; +import { + COIN_GECKO_IDS, + OASIS_CONFIG, + POLYGON_CONFIG, + VITWIT_VALIDATOR_NAMES, + WITVAL, +} from '@/utils/constants'; + +const removedChains = ['crescent-1', 'archway-1', 'celestia']; + +const useGetValidatorInfo = () => { + const stakingData = useAppSelector( + (state: RootState) => state.staking.chains + ); + const allNetworksInfo = useAppSelector( + (state: RootState) => state.common.allNetworksInfo + ); + + const allChainIds = Object.keys(allNetworksInfo); + + const chainIDs = allChainIds.filter((c) => !removedChains.includes(c)); + + const tokensPriceInfo = useAppSelector( + (state) => state.common.allTokensInfoState.info + ); + const nonCosmosData = useAppSelector( + (state) => state.staking.witvalNonCosmosValidators + ); + const { getAllDenomInfo } = useGetAllChainsInfo(); + + const getValidatorInfo = ({ + chainID, + moniker, + }: { + chainID: string; + moniker: string; + }) => { + if ( + stakingData?.[chainID]?.validators.active && + Object.values(stakingData?.[chainID]?.validators.active).length > 0 + ) { + const validator = Object.values( + stakingData?.[chainID]?.validators.active + ).find((v) => { + const isMatchingMoniker = + v.description.moniker.trim().toLowerCase() === + moniker.trim().toLowerCase(); + + if (isMatchingMoniker) { + return true; + } + + // Otherwise, check if it matches the Witval moniker + // For few networks supported by Vitwit Validator, Moniker name is still Witval, so considering that validator also + const isWitval = + v.description.moniker.trim().toLowerCase() === WITVAL && + moniker.trim().toLowerCase() === WITVAL; + return isWitval; + }); + + if (validator) { + return validator; + } + } + + if ( + stakingData?.[chainID]?.validators.inactive && + Object.values(stakingData?.[chainID]?.validators.inactive).length > 0 + ) { + const validator = Object.values( + stakingData?.[chainID]?.validators.inactive + ).find((v) => { + return ( + v.description.moniker.trim().toLowerCase() === + moniker.trim().toLowerCase() + ); + }); + + if (validator) { + return validator; + } + + return null; + } + }; + + const getChainwiseValidatorInfo = ({ moniker }: { moniker: string }) => { + const chainWiseValidatorData: Record = {}; + let validatorDescription: string = ''; + let validatorWebsite: string = ''; + let validatorIdentity: string = ''; + + chainIDs.forEach((chainID) => { + const validatorInfo = getValidatorInfo({ chainID, moniker }); + + if (validatorInfo) { + const { decimals, minimalDenom } = getAllDenomInfo(chainID); + const activeSorted = stakingData?.[chainID]?.validators.activeSorted; + const inactiveSorted = + stakingData?.[chainID]?.validators.inactiveSorted; + const operatorAddress = validatorInfo?.operator_address || ''; + const rank = getValidatorRank(operatorAddress, [ + ...activeSorted, + ...inactiveSorted, + ]); + const description = validatorInfo?.description?.details; + const website = validatorInfo?.description?.website; + const identity = validatorInfo?.description?.identity; + const commission = + Number(validatorInfo?.commission?.commission_rates?.rate) * 100; + const delegatorShares = validatorInfo?.delegator_shares; + const validatorStatus = validatorInfo?.status; + const totalStaked = parseBalance( + [ + { + amount: delegatorShares, + denom: minimalDenom, + }, + ], + decimals, + minimalDenom + ); + const tokens = totalStaked; + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info || + tokensPriceInfo?.[COIN_GECKO_IDS?.[minimalDenom]]?.info; + const totalStakedInUSD = usdPriceInfo + ? (totalStaked * usdPriceInfo.usd).toString() + : '-'; + + if (!validatorDescription?.length && description?.length) { + validatorDescription = description; + } + + if (!validatorWebsite?.length && website?.length) { + validatorWebsite = website; + } + + if (!validatorIdentity?.length && identity?.length) { + validatorIdentity = identity; + } + + chainWiseValidatorData[chainID] = { + commission, + rank, + totalStakedInUSD, + chainID, + tokens, + operatorAddress, + validatorStatus, + validatorInfo, + }; + } + }); + return { + chainWiseValidatorData, + validatorDescription, + validatorIdentity, + validatorWebsite, + }; + }; + + const getValidatorStats = ({ + data, + moniker, + }: { + data: Record; + moniker: string; + }) => { + let totalStaked = 0; + let totalDelegators = 0; + let totalCommission = 0; + let activeNetworks = 0; + let totalNetworks = 0; + + Object.keys(data).forEach((chainID) => { + const validator = data?.[chainID]; + const totalStakedInUSD = Number(validator?.totalStakedInUSD || 0); + + if (!isNaN(totalStakedInUSD)) { + totalStaked += totalStakedInUSD; + } + const delegatorsCount = + stakingData[validator.chainID].validatorProfiles?.[ + validator.operatorAddress + ]; + + totalDelegators += Number(delegatorsCount?.totalDelegators || 0); + totalCommission += Number(validator?.commission) || 0; + if (validator.validatorStatus === 'BOND_STATUS_BONDED') { + activeNetworks += 1; + } + totalNetworks += 1; + }); + if (VITWIT_VALIDATOR_NAMES.includes(moniker.toLowerCase())) { + { + const { + commission, + totalDelegators: delegators, + // totalStakedInUSD: totalStaked, + } = getPolygonValidatorInfo(); + totalCommission += Number(commission || 0); + // totalDelegators += totalStaked; + // totalStaked+=totalS + totalDelegators += delegators; + activeNetworks += 1; + totalNetworks += 1; + } + + { + const { + commission, + totalDelegators: delegators, + // totalStakedInUSD: totalStaked, + } = getOasisValidatorInfo(); + totalCommission += Number(commission || 0); + // totalDelegators += totalStaked; + totalDelegators += delegators; + activeNetworks += 1; + totalNetworks += 1; + } + } + const avgCommission = totalCommission / totalNetworks; + + return { + totalStaked, + totalDelegators, + avgCommission, + totalNetworks, + activeNetworks, + }; + }; + + const getPolygonValidatorInfo = () => { + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[POLYGON_CONFIG.coinGeckoId]?.info; + + const polygonData = nonCosmosData.chains?.['polygon']; + const polygonDelegators = Number(nonCosmosData.delegators['polygon']); + let totalStakedInUSD = 0; + let commission = ''; + let totalDelegators = 0; + let totalStakedTokens = 0; + let operatorAddress = ''; + + if (polygonData) { + totalStakedTokens = + Number(polygonData?.result?.totalStaked) / + 10 ** POLYGON_CONFIG.decimals; + totalStakedInUSD = usdPriceInfo + ? totalStakedTokens * usdPriceInfo.usd + : 0; + commission = polygonData?.result?.commissionPercent; + totalDelegators = polygonDelegators || 0; + operatorAddress = polygonData?.result?.owner; + } + + return { + totalStakedInUSD, + commission, + totalDelegators, + totalStakedTokens, + operatorAddress, + }; + }; + + const getOasisValidatorInfo = () => { + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[OASIS_CONFIG.coinGeckoId]?.info; + const oasisDelegations = nonCosmosData.delegators['oasis']; + let totalStakedInUSD = 0; + let commission = ''; + let totalDelegators = 0; + let totalStakedTokens = 0; + let operatorAddress = ''; + if (oasisDelegations) { + const delegations = oasisDelegations?.data?.list; + delegations?.forEach( + (delegation: { + amount: string; + delegator: string; + shares: string; + validator: string; + }) => { + totalStakedTokens += Number(delegation?.amount || 0); + } + ); + + totalStakedInUSD = usdPriceInfo + ? totalStakedTokens * usdPriceInfo.usd + : 0; + commission = OASIS_CONFIG.witval.commission.toString(); + totalDelegators = oasisDelegations?.data?.totalSize; + operatorAddress = OASIS_CONFIG.witval.operatorAddress; + } else { + totalDelegators = 8; + totalStakedTokens = 11641594; + commission = OASIS_CONFIG.witval.commission.toString(); + + totalStakedInUSD = usdPriceInfo + ? totalStakedTokens * usdPriceInfo.usd + : 0; + } + + return { + totalStakedInUSD, + commission, + totalDelegators, + totalStakedTokens, + operatorAddress, + }; + }; + + return { + getChainwiseValidatorInfo, + getValidatorStats, + getPolygonValidatorInfo, + getOasisValidatorInfo, + }; +}; + +export default useGetValidatorInfo; diff --git a/frontend/src/custom-hooks/useGetWithdrawPermissions.tsx b/frontend/src/custom-hooks/useGetWithdrawPermissions.tsx new file mode 100644 index 000000000..7d5eb3c31 --- /dev/null +++ b/frontend/src/custom-hooks/useGetWithdrawPermissions.tsx @@ -0,0 +1,41 @@ +import useAddressConverter from './useAddressConverter'; +import { useAppSelector } from './StateHooks'; +import { haveAuthorization } from './useAuthzStakingExecHelper'; +import { msgWithdrawRewards } from '@/txns/distribution/withDrawRewards'; +import { msgWithdrawValidatorCommission } from '@/txns/distribution/withDrawValidatorCommission'; + +const useGetWithdrawPermissions = () => { + const { convertAddress } = useAddressConverter(); + const authzChains = useAppSelector((state) => state.authz.chains); + + const getWithdrawPermissions = ({ + chainID, + granter, + }: { + chainID: string; + granter: string; + }) => { + const address = convertAddress(chainID, granter); + const grants: Authorization[] = + authzChains?.[chainID]?.GrantsToMeAddressMapping?.[address] || []; + + const { haveGrant: withdrawRewardsAllowed } = haveAuthorization(grants, { + generic: { + msg: msgWithdrawRewards, + }, + }); + + const { haveGrant: withdrawCommissionAllowed } = haveAuthorization(grants, { + generic: { + msg: msgWithdrawValidatorCommission, + }, + }); + + return { withdrawRewardsAllowed, withdrawCommissionAllowed }; + }; + return { + getWithdrawPermissions, + }; +}; + +export default useGetWithdrawPermissions; diff --git a/frontend/src/custom-hooks/useInitAllValidator.ts b/frontend/src/custom-hooks/useInitAllValidator.ts new file mode 100644 index 000000000..fbf31ba59 --- /dev/null +++ b/frontend/src/custom-hooks/useInitAllValidator.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from './StateHooks'; +import { + getWitvalOasisDelegations, + getWitvalPolygonDelegatorsCount, + getWitvalPolygonValidator, +} from '@/store/features/staking/stakeSlice'; +import { OASIS_CONFIG, POLYGON_API, POLYGON_CONFIG } from '@/utils/constants'; + +const useInitAllValidator = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch( + getWitvalPolygonValidator({ + baseURL: POLYGON_API, + id: 50, + }) + ); + dispatch( + getWitvalPolygonDelegatorsCount({ + baseURL: POLYGON_CONFIG.baseURL, + id: 50, + }) + ); + dispatch( + getWitvalOasisDelegations({ + baseURL: OASIS_CONFIG.baseURL, + operatorAddress: 'oasis1qzc687uuywnel4eqtdn6x3t9hkdvf6sf2gtv4ye9', + }) + ); + }, []); +}; + +export default useInitAllValidator; diff --git a/frontend/src/custom-hooks/useInitAuthz.ts b/frontend/src/custom-hooks/useInitAuthz.ts new file mode 100644 index 000000000..053322614 --- /dev/null +++ b/frontend/src/custom-hooks/useInitAuthz.ts @@ -0,0 +1,47 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useAppDispatch } from './StateHooks'; +import { + getGrantsByMe, + getGrantsToMe, +} from '@/store/features/authz/authzSlice'; +import useGetChainInfo from './useGetChainInfo'; + +const useInitAuthz = ({ + chainIDs, + shouldFetch, +}: { + chainIDs: string[]; + shouldFetch: boolean; +}) => { + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const networksCount = useMemo(() => chainIDs?.length, [chainIDs]); + const [dataFetched, setDataFetched] = useState(false); + const fetchedChains = useRef<{ [key: string]: boolean }>({}); + + useEffect(() => { + if (networksCount > 0 && !dataFetched && shouldFetch) { + chainIDs.forEach((chainID) => { + if (!fetchedChains.current[chainID]) { + const { address, baseURL, restURLs, enableModules } = + getChainInfo(chainID); + const authzInputs = { + baseURLs: restURLs, + address, + baseURL, + chainID, + }; + if (enableModules?.authz) { + dispatch(getGrantsByMe(authzInputs)); + dispatch(getGrantsToMe(authzInputs)); + } + fetchedChains.current[chainID] = true; + } + }); + + setDataFetched(true); + } + }, [chainIDs, networksCount, dataFetched, getChainInfo, dispatch]); +}; + +export default useInitAuthz; diff --git a/frontend/src/custom-hooks/useInitBalances.ts b/frontend/src/custom-hooks/useInitBalances.ts deleted file mode 100644 index 57d7df770..000000000 --- a/frontend/src/custom-hooks/useInitBalances.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect } from 'react'; -import { useAppDispatch, useAppSelector } from './StateHooks'; -import { RootState } from '@/store/store'; -import { getBalances } from '@/store/features/bank/bankSlice'; - -const useInitBalances = ({ chainIDs }: { chainIDs: string[] }) => { - const dispatch = useAppDispatch(); - const networks = useAppSelector((state: RootState) => state.wallet.networks); - - useEffect(() => { - chainIDs.forEach((chainID) => { - if (networks.hasOwnProperty(chainID)) { - const allChainInfo = networks[chainID]; - const chainInfo = allChainInfo.network; - const address = allChainInfo?.walletInfo?.bech32Address; - const basicChainInputs = { - baseURL: chainInfo.config.rest, - address, - chainID, - }; - dispatch(getBalances(basicChainInputs)); - } - }); - }, [chainIDs]); -}; - -export default useInitBalances; diff --git a/frontend/src/custom-hooks/useInitFeegrant.ts b/frontend/src/custom-hooks/useInitFeegrant.ts new file mode 100644 index 000000000..6d495aa58 --- /dev/null +++ b/frontend/src/custom-hooks/useInitFeegrant.ts @@ -0,0 +1,60 @@ +import { useEffect, useRef, useState } from 'react'; +import { useAppDispatch } from './StateHooks'; +import useGetChainInfo from './useGetChainInfo'; +import { + getGrantsByMe, + getGrantsToMe, +} from '@/store/features/feegrant/feegrantSlice'; + +/** + * This custom hook is used to dispatch the feegrantsByMe and feegrantsToMe + * + */ +const useInitFeegrant = ({ + chainIDs, + shouldFetch, +}: { + chainIDs: string[]; + shouldFetch: boolean; +}) => { + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const networksCount = chainIDs.length; + const [dataFetched, setDataFetched] = useState(false); + const fetchedChains = useRef<{ [key: string]: boolean }>({}); + + useEffect(() => { + if (networksCount > 0 && !dataFetched && shouldFetch) { + let allFetched = true; + + chainIDs.forEach((chainID) => { + if (!fetchedChains.current[chainID]) { + const { address, baseURL, restURLs, enableModules } = + getChainInfo(chainID); + const feegrantInputs = { + baseURLs: restURLs, + address, + baseURL, + chainID, + }; + if (enableModules.feegrant) { + dispatch(getGrantsByMe(feegrantInputs)); + dispatch(getGrantsToMe(feegrantInputs)); + } + fetchedChains.current[chainID] = true; // Mark this chain as fetched + } + if (!fetchedChains.current[chainID]) { + allFetched = false; + } + }); + + if (allFetched) { + setDataFetched(true); + } + } + }, [chainIDs, networksCount, dataFetched, getChainInfo, dispatch]); + + return null; +}; + +export default useInitFeegrant; diff --git a/frontend/src/custom-hooks/useInitStaking.ts b/frontend/src/custom-hooks/useInitStaking.ts new file mode 100644 index 000000000..b903954a6 --- /dev/null +++ b/frontend/src/custom-hooks/useInitStaking.ts @@ -0,0 +1,3 @@ +const useInitStaking = () => {}; + +export default useInitStaking; diff --git a/frontend/src/custom-hooks/useInitTransactions.tsx b/frontend/src/custom-hooks/useInitTransactions.tsx new file mode 100644 index 000000000..1932bc806 --- /dev/null +++ b/frontend/src/custom-hooks/useInitTransactions.tsx @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from './StateHooks'; +import { getAllTransactions } from '@/store/features/recent-transactions/recentTransactionsSlice'; +import useGetChainInfo from './useGetChainInfo'; + +const useInitTransactions = ({ chainID }: { chainID: string }) => { + const dispatch = useAppDispatch(); + const { getChainInfo } = useGetChainInfo(); + const { address } = getChainInfo(chainID); + useEffect(() => { + if (chainID) { + dispatch( + getAllTransactions({ + address, + chainID, + limit: 5, + offset: 0, + }) + ); + } + }, [chainID]); +}; + +export default useInitTransactions; diff --git a/frontend/src/custom-hooks/useShortCuts.ts b/frontend/src/custom-hooks/useShortCuts.ts new file mode 100644 index 000000000..9ef2d21bf --- /dev/null +++ b/frontend/src/custom-hooks/useShortCuts.ts @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import { useAppDispatch } from './StateHooks'; +import { setChangeNetworkDialogOpen } from '@/store/features/common/commonSlice'; + +const useShortCuts = () => { + const dispatch = useAppDispatch(); + + // Open select network dialog on '/' key press + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === '/') { + const activeElement = document.activeElement as HTMLElement; + if ( + activeElement.tagName !== 'INPUT' && + activeElement.tagName !== 'TEXTAREA' + ) { + event.preventDefault(); + dispatch( + setChangeNetworkDialogOpen({ open: true, showSearch: true }) + ); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [dispatch]); +}; + +export default useShortCuts; diff --git a/frontend/src/custom-hooks/useSingleStaking.tsx b/frontend/src/custom-hooks/useSingleStaking.tsx new file mode 100644 index 000000000..cc546d3f3 --- /dev/null +++ b/frontend/src/custom-hooks/useSingleStaking.tsx @@ -0,0 +1,151 @@ +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { RootState } from '@/store/store'; +import useGetChainInfo from './useGetChainInfo'; +import { getValidator } from '@/store/features/staking/stakeSlice'; +import useGetAssetsAmount from './useGetAssetsAmount'; + +/* eslint-disable react-hooks/rules-of-hooks */ +const useSingleStaking = (chainID: string) => { + const dispatch = useAppDispatch(); + const networks = useAppSelector((state: RootState) => state.wallet.networks); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + + const { + getChainInfo, + getDenomInfo, + // getValueFromToken, getTokenValueByChainId + } = useGetChainInfo(); + + const rewardsChains = useAppSelector((state: RootState) => + isAuthzMode ? state.distribution.authzChains : state.distribution.chains + ); + + // const totalData = useAppSelector((state: RootState) => state.staking) + + const [ + totalStakedAmount, + availableAmount, + rewardsAmount, + totalUnStakedAmount, + ] = useGetAssetsAmount([chainID]); + + // const { getTokensByChainID } = useGetAssets(); + + // get total staking data data from the state + const stakeData = useAppSelector((state: RootState) => + isAuthzMode ? state.staking.authz.chains : state.staking.chains + ); + const bankData = useAppSelector((state: RootState) => + isAuthzMode ? state.bank.authz.balances : state.bank.balances + ); + const commonStakingData = useAppSelector((state) => state.staking.chains); + + const getAvaiailableAmount = (chainID: string) => { + let amount = 0; + + const { decimals, minimalDenom } = getDenomInfo(chainID); + + bankData[chainID]?.list?.forEach((element) => { + if (element?.denom === minimalDenom) amount += Number(element?.amount); + }); + + return Number(amount / 10 ** decimals); + }; + + const totalValStakedAssets = (chainID: string, valAddr: string) => { + let amount = 0; + stakeData[chainID]?.delegations?.delegations?.delegation_responses?.forEach( + (d) => { + if (d?.delegation?.validator_address === valAddr) { + amount = Number(d?.balance?.amount); + } + } + ); + + const { decimals } = getDenomInfo(chainID); + return Number(amount / 10 ** decimals); + }; + + const fetchValidatorDetails = (valoperAddress: string, chainID: string) => { + const { restURLs } = getChainInfo(chainID); + dispatch( + getValidator({ + baseURLs: restURLs, + chainID, + valoperAddress: valoperAddress, + }) + ); + }; + + const chainLogo = (chainID: string) => + networks[chainID]?.network?.logos?.menu || ''; + + const getStakingAssets = () => { + return { + totalStakedAmount, + rewardsAmount, + totalUnStakedAmount, + availableAmount, + }; + }; + + const getAllDelegations = (chainID: string) => { + return { [chainID]: stakeData[chainID] }; + }; + + // Get total staked amount of chain + + const getAmountWithDecimal = (amount: number, chainID: string) => { + const { decimals, displayDenom } = getDenomInfo(chainID); + + return ( + (amount / 10 ** decimals).toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }) + + ' ' + + displayDenom + ); + }; + + const getDenomWithChainID = (chainID: string) => { + const { displayDenom } = getDenomInfo(chainID); + + return displayDenom; + }; + + const chainTotalRewards = (chainID: string) => { + let totalRewardsAmount = 0; + let displayDenomName = ''; + const rewards = + rewardsChains?.[chainID]?.delegatorRewards?.totalRewards || 0; + + const { decimals, displayDenom } = getDenomInfo(chainID); + if (rewards > 0) { + totalRewardsAmount += rewards / 10 ** decimals; + } + + displayDenomName = displayDenom; + + return totalRewardsAmount.toFixed(4) + ' ' + displayDenomName; + }; + + const getValidators = () => { + return commonStakingData[chainID]?.validators || {}; + }; + + return { + getStakingAssets, + getAllDelegations, + fetchValidatorDetails, + getAmountWithDecimal, + chainTotalRewards, + chainLogo, + getValidators, + getDenomWithChainID, + getAvaiailableAmount, + totalValStakedAssets, + }; +}; + +export default useSingleStaking; diff --git a/frontend/src/custom-hooks/useSortedAssets.ts b/frontend/src/custom-hooks/useSortedAssets.ts index d8da137c4..f6eadacd4 100644 --- a/frontend/src/custom-hooks/useSortedAssets.ts +++ b/frontend/src/custom-hooks/useSortedAssets.ts @@ -12,23 +12,33 @@ export interface Options { showRewards?: boolean; showAvailable?: boolean; showValuedTokens?: boolean; + AuthzSkipIBC?: boolean; } const useSortedAssets = ( chainIDs: string[], options: Options -): [ParsedAsset[]] => { +): [ParsedAsset[], ParsedAsset[]] => { const networks = useAppSelector((state: RootState) => state.wallet.networks); const balanceChains = useAppSelector( (state: RootState) => state.bank.balances ); + const authzBalanceChains = useAppSelector( + (state) => state.bank.authz.balances + ); const stakingChains = useAppSelector( (state: RootState) => state.staking.chains ); + const authzStakingChains = useAppSelector( + (state: RootState) => state.staking.authz.chains + ); + const rewardsChains = useAppSelector( (state: RootState) => state.distribution.chains ); - + const authzRewardsChains = useAppSelector( + (state: RootState) => state.distribution.authzChains + ); const tokensPriceInfo = useAppSelector( (state) => state.common.allTokensInfoState.info ); @@ -43,7 +53,7 @@ const useSortedAssets = ( const network = networks?.[chainID]?.network; const currency = config?.currencies?.[0]; const chainName = config?.chainName.toLowerCase(); - const nativeMinimalDenom = currency.coinMinimalDenom; + const nativeMinimalDenom = currency?.coinMinimalDenom; const chainBalances = balanceChains?.[chainID]?.list || []; const chainLogoURL = network?.logos?.menu; @@ -67,6 +77,8 @@ const useSortedAssets = ( const rewardsAmountInMinDenoms: number = rewardsChains?.[chainID]?.delegatorRewards?.totalRewards || 0; const stakedAmountInDenoms = stakedAmountInMinDenoms / 10 ** decimals; + const unbondedAmountInDenoms = + unbondedAmountInMinDenoms / 10 ** decimals; const rewardsAmountInDenoms = rewardsAmountInMinDenoms / 10 ** decimals; @@ -88,7 +100,7 @@ const useSortedAssets = ( (balanceAmountInDenoms + stakedAmountInDenoms + rewardsAmountInDenoms + - unbondedAmountInMinDenoms), + unbondedAmountInDenoms), usdPrice: usdDenomPrice, inflation: inflation, chainID: chainID, @@ -137,9 +149,130 @@ const useSortedAssets = ( sortedAssets.sort((x, y) => y.usdValue - x.usdValue); return sortedAssets; - }, [chainIDs, balanceChains, networks, tokensPriceInfo, stakingChains]); + }, [ + chainIDs, + balanceChains, + networks, + tokensPriceInfo, + stakingChains, + rewardsChains, + ]); + + const authzSortedAssets = useMemo(() => { + let sortedAssets: ParsedAsset[] = []; + + chainIDs.forEach((chainID) => { + const config = networks?.[chainID]?.network?.config; + const network = networks?.[chainID]?.network; + const currency = config?.currencies?.[0]; + const chainName = config?.chainName.toLowerCase(); + const nativeMinimalDenom = currency?.coinMinimalDenom; + const chainBalances = authzBalanceChains?.[chainID]?.list || []; + const chainLogoURL = network?.logos?.menu; + + chainBalances.forEach((balance) => { + const denomInfo = chainDenomsData[chainName]?.filter((denomInfo) => { + return denomInfo.denom === balance.denom; + }); + let asset: ParsedAsset | undefined; + if (balance.denom === nativeMinimalDenom) { + const config = networks?.[chainID]?.network?.config; + const currency = config?.currencies?.[0]; + const minimalDenom = currency?.coinMinimalDenom; + const coinDenom = currency?.coinDenom; + const decimals = currency?.coinDecimals || 0; + // minimalDenom + const stakedAmountInMinDenoms: number = + authzStakingChains?.[chainID]?.delegations?.totalStaked || 0; + const unbondedAmountInMinDenoms: number = + authzStakingChains?.[chainID]?.unbonding?.totalUnbonded || 0; + + const rewardsAmountInMinDenoms: number = + authzRewardsChains?.[chainID]?.delegatorRewards?.totalRewards || 0; + const stakedAmountInDenoms = stakedAmountInMinDenoms / 10 ** decimals; + const unbondedAmountInDenoms = + unbondedAmountInMinDenoms / 10 ** decimals; + const rewardsAmountInDenoms = + rewardsAmountInMinDenoms / 10 ** decimals; + + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[minimalDenom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + const inflation = usdPriceInfo?.usd_24h_change || 0; + + const balanceAmountInDenoms = parseBalance( + authzBalanceChains?.[chainID]?.list || [], + decimals, + minimalDenom + ); + asset = { + type: 'native', + chainName: chainName, + usdValue: + usdDenomPrice * + (balanceAmountInDenoms + + stakedAmountInDenoms + + rewardsAmountInDenoms + + unbondedAmountInDenoms), + usdPrice: usdDenomPrice, + inflation: inflation, + chainID: chainID, + displayDenom: coinDenom, + balance: balanceAmountInDenoms, + staked: stakedAmountInDenoms, + rewards: rewardsAmountInDenoms, + denom: minimalDenom, + chainLogoURL, + decimals, + }; + } else if (denomInfo?.length && !options.AuthzSkipIBC) { + const usdPriceInfo: TokenInfo | undefined = + tokensPriceInfo?.[denomInfo[0].origin_denom]?.info; + const usdDenomPrice = usdPriceInfo?.usd || 0; + const inflation = usdPriceInfo?.usd_24h_change || 0; + + const balanceAmount = parseBalance( + [balance], + denomInfo[0].decimals, + balance.denom + ); + const usdDenomValue = usdDenomPrice * balanceAmount; + asset = { + originDenomChainInfo: getOriginDenomInfo(denomInfo[0].origin_denom), + type: 'ibc', + usdValue: usdDenomValue, + usdPrice: usdDenomPrice, + balance: balanceAmount, + denom: balance.denom, + displayDenom: denomInfo[0].symbol, + chainName: chainName, + denomInfo: denomInfo, + inflation: inflation, + chainID: chainID, + chainLogoURL, + decimals: denomInfo[0].decimals, + }; + } + + if (asset && filterAsset(asset, options)) { + sortedAssets = [...sortedAssets, asset]; + } + }); + }); + + sortedAssets.sort((x, y) => y.usdValue - x.usdValue); + + return sortedAssets; + }, [ + chainIDs, + authzBalanceChains, + networks, + tokensPriceInfo, + authzStakingChains, + authzRewardsChains, + ]); - return [sortedAssets]; + return [sortedAssets, authzSortedAssets]; }; export default useSortedAssets; diff --git a/frontend/src/custom-hooks/useStaking.ts b/frontend/src/custom-hooks/useStaking.ts new file mode 100644 index 000000000..dc244ceb4 --- /dev/null +++ b/frontend/src/custom-hooks/useStaking.ts @@ -0,0 +1,434 @@ +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { RootState } from '@/store/store'; +import useGetChainInfo from './useGetChainInfo'; +import { + getValidator, + txCancelUnbonding, + txDelegate, + txReDelegate, + txRestake, + txUnDelegate, +} from '@/store/features/staking/stakeSlice'; +import { txWithdrawAllRewards } from '@/store/features/distribution/distributionSlice'; +import useGetAssetsAmount from './useGetAssetsAmount'; +import useGetTxInputs from './useGetTxInputs'; +import useGetFeegranter from './useGetFeegranter'; +import { MAP_TXN_MSG_TYPES } from '@/utils/feegrant'; +import useAuthzStakingExecHelper from './useAuthzStakingExecHelper'; +import { UnbondingEncode } from '@/txns/staking/unbonding'; +import { TxStatus } from '@/types/enums'; + +/* eslint-disable react-hooks/rules-of-hooks */ +const useStaking = () => { + const dispatch = useAppDispatch(); + const { getFeegranter } = useGetFeegranter(); + const { txAuthzDelegate, txAuthzReDelegate, txAuthzUnDelegate } = + useAuthzStakingExecHelper(); + const { txAuthzRestake, txAuthzClaim, txAuthzCancelUnbond } = + useAuthzStakingExecHelper(); + + const networks = useAppSelector((state: RootState) => state.wallet.networks); + const isAuthzMode = useAppSelector((state) => state.authz.authzModeEnabled); + const authzAddress = useAppSelector((state) => state.authz.authzAddress); + const nameToChainIDs = useAppSelector( + (state: RootState) => state.wallet.nameToChainIDs + ); + const chainIDs = Object.values(nameToChainIDs); + + const { getChainInfo, getDenomInfo } = useGetChainInfo(); + + const rewardsChains = useAppSelector((state: RootState) => + isAuthzMode ? state.distribution.authzChains : state.distribution.chains + ); + + const [ + totalStakedAmount, + availableAmount, + rewardsAmount, + totalUnStakedAmount, + ] = useGetAssetsAmount(chainIDs); + + const stakeData = useAppSelector((state: RootState) => + isAuthzMode ? state.staking.authz.chains : state.staking.chains + ); + + const delegationsLoading = useAppSelector((state: RootState) => + isAuthzMode + ? state.staking.authz.delegationsLoading + : state.staking.delegationsLoading + ); + + const undelegationsLoading = useAppSelector((state: RootState) => + isAuthzMode + ? state.staking.authz.undelegationsLoading + : state.staking.undelegationsLoading + ); + + const cancelUnbdongTxLoading = (chainID: string) => { + return stakeData[chainID].cancelUnbondingTxStatus === TxStatus.PENDING + ? true + : false; + }; + + const totalUnbondedAmount = useAppSelector((state: RootState) => + isAuthzMode + ? state.staking.authz.totalUndelegationsAmount + : state.staking.totalUndelegationsAmount + ); + + const { + txWithdrawAllRewardsInputs, + txWithdrawValidatorRewardsInputs, + txRestakeInputs, + txAuthzRestakeMsgs, + } = useGetTxInputs(); + + const fetchValidatorDetails = (valoperAddress: string, chainID: string) => { + const { restURLs } = getChainInfo(chainID); + dispatch( + getValidator({ + baseURLs: restURLs, + chainID, + valoperAddress: valoperAddress, + }) + ); + }; + + const chainLogo = (chainID: string) => + networks[chainID]?.network?.logos?.menu || ''; + const chainName = (chainID: string) => { + const { chainName } = getChainInfo(chainID); + return chainName; + }; + + const getStakingAssets = () => { + return { + totalStakedAmount, + rewardsAmount, + totalUnStakedAmount, + availableAmount, + }; + }; + + const getAllDelegations = () => { + return stakeData; + }; + + // Get total staked amount of chain + + const getAmountWithDecimal = (amount: number, chainID: string) => { + const { decimals, displayDenom } = getDenomInfo(chainID); + return (amount / 10 ** decimals).toFixed(6) + ' ' + displayDenom; + }; + + const getAmountObjectWithDecimal = (amount: number, chainID: string) => { + const { decimals, displayDenom } = getDenomInfo(chainID); + return { + amount: (amount / 10 ** decimals).toFixed(6), + denom: displayDenom, + }; + }; + + const chainTotalRewards = (chainID: string) => { + let totalRewardsAmount = 0; + let displayDenomName = ''; + chainIDs.forEach((cId) => { + if (cId === chainID) { + const rewards = + rewardsChains?.[chainID]?.delegatorRewards?.totalRewards || 0; + + const { decimals, displayDenom } = getDenomInfo(chainID); + if (rewards > 0) { + totalRewardsAmount += rewards / 10 ** decimals; + } + + displayDenomName = displayDenom; + + return false; + } + }); + + return totalRewardsAmount.toFixed(4) + ' ' + displayDenomName; + }; + + const chainTotalValRewards = (validator: string, chainID: string) => { + let totalRewardsAmount = 0; + let displayDenomName = ''; + + chainIDs.forEach((cId) => { + if (cId === chainID) { + const rewards = rewardsChains?.[chainID]?.delegatorRewards; + rewards?.list?.forEach((r) => { + if (r.validator_address === validator) { + const { decimals, displayDenom, minimalDenom } = + getDenomInfo(chainID); + r?.reward?.forEach((r1) => { + if (r1?.denom === minimalDenom) { + totalRewardsAmount = Number(r1?.amount || 0) / 10 ** decimals; + displayDenomName = displayDenom; + } + }); + } + + return false; + }); + + return false; + } + }); + + return totalRewardsAmount.toFixed(4) + ' ' + displayDenomName; + }; + + // tx: withdraw claim rewards without authz and fee grant + + const txWithdrawCliamRewards = (chainID: string) => { + const txInputs = txWithdrawAllRewardsInputs(chainID); + txInputs.isTxAll = true; + if (txInputs.msgs.length) dispatch(txWithdrawAllRewards(txInputs)); + }; + + const transactionRestake = (chainID: string) => { + if (isAuthzMode) { + const { address } = getChainInfo(chainID); + const msgs = txAuthzRestakeMsgs(chainID); + txAuthzRestake({ + grantee: address, + granter: authzAddress, + msgs: msgs, + chainID: chainID, + isTxAll: true, + }); + return; + } + const txInputs = txRestakeInputs(chainID); + txInputs.isTxAll = true; + if (txInputs.msgs.length) dispatch(txRestake(txInputs)); + }; + + const txWithdrawValRewards = (validator: string, chainID: string) => { + const { address } = getChainInfo(chainID); + if (isAuthzMode) { + txAuthzClaim({ + grantee: address, + granter: authzAddress, + pairs: [{ validator, delegator: authzAddress }], + chainID: chainID, + }); + return; + } + const delegatorAddress = networks[chainID]?.walletInfo?.bech32Address; + const txInputs = txWithdrawValidatorRewardsInputs( + chainID, + validator, + delegatorAddress + ); + txInputs.isTxAll = false; + if (txInputs.msgs.length) dispatch(txWithdrawAllRewards(txInputs)); + }; + + const txAllChainTxStatus = useAppSelector( + (state: RootState) => state.distribution.chains + ); + + const txAllChainStakeTxStatus = useAppSelector( + (state: RootState) => state.staking.chains + ); + + const getClaimTxStatus = () => { + return txAllChainTxStatus; + }; + + const txCancelUnbond = ( + chainID: string, + delegator: string, + validator: string, + amount: number, + creationHeight: string + ) => { + const basicChainInfo = getChainInfo(chainID); + const { currencies } = networks[chainID]?.network?.config; + + const currency = currencies[0]; + + const { feeAmount: avgFeeAmount, address } = getChainInfo(chainID); + const feeAmount = avgFeeAmount * 10 ** currency?.coinDecimals; + + if (isAuthzMode) { + const msg = UnbondingEncode( + delegator, + validator, + amount * 10 ** currency.coinDecimals, + currency.coinMinimalDenom, + creationHeight + ); + txAuthzCancelUnbond({ + grantee: address, + granter: authzAddress, + chainID: chainID, + msg: msg, + }); + } else { + dispatch( + txCancelUnbonding({ + isAuthzMode: false, + basicChainInfo: basicChainInfo, + delegator: delegator, + validator: validator, + amount: amount * 10 ** currency?.coinDecimals, + denom: currency?.coinMinimalDenom, + feeAmount: feeAmount, + feegranter: getFeegranter( + chainID, + MAP_TXN_MSG_TYPES['cancel_unbonding'] + ), + creationHeight: creationHeight, + }) + ); + } + }; + + const txDelegateTx = (validator: string, amount: number, chainID: string) => { + const basicChainInfo = getChainInfo(chainID); + const { currencies } = networks[chainID]?.network?.config; + + const currency = currencies[0]; + + const { feeAmount: avgFeeAmount, address } = getChainInfo(chainID); + const feeAmount = avgFeeAmount * 10 ** currency?.coinDecimals; + + const txDelegateInputs = { + validator: validator, + amount: amount * 10 ** currency?.coinDecimals, + denom: currency?.coinMinimalDenom, + }; + + if (isAuthzMode) { + txAuthzDelegate({ + ...txDelegateInputs, + grantee: address, + granter: authzAddress, + chainID: basicChainInfo.chainID, + }); + } else { + dispatch( + txDelegate({ + ...txDelegateInputs, + isAuthzMode: false, + basicChainInfo: basicChainInfo, + delegator: basicChainInfo.address, + feeAmount: feeAmount, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['delegate']), + }) + ); + } + }; + + const txUnDelegateTx = ( + validator: string, + amount: number, + chainID: string + ) => { + const basicChainInfo = getChainInfo(chainID); + const { currencies } = networks[chainID]?.network?.config; + + const currency = currencies[0]; + + const { feeAmount: avgFeeAmount, address } = getChainInfo(chainID); + const feeAmount = avgFeeAmount * 10 ** currency?.coinDecimals; + + const txUndelegateInputs = { + validator: validator, + amount: amount * 10 ** currency?.coinDecimals, + denom: currency?.coinMinimalDenom, + }; + if (isAuthzMode) { + txAuthzUnDelegate({ + ...txUndelegateInputs, + grantee: address, + granter: authzAddress, + chainID: basicChainInfo.chainID, + }); + } else { + dispatch( + txUnDelegate({ + ...txUndelegateInputs, + isAuthzMode: false, + basicChainInfo: basicChainInfo, + delegator: basicChainInfo.address, + + feeAmount: feeAmount, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['undelegate']), + }) + ); + } + }; + + const txReDelegateTx = ( + srcVal: string, + destVal: string, + amount: number, + chainID: string + ) => { + const basicChainInfo = getChainInfo(chainID); + const { currencies } = networks[chainID]?.network?.config; + + const currency = currencies[0]; + + const { feeAmount: avgFeeAmount, address } = getChainInfo(chainID); + const feeAmount = avgFeeAmount * 10 ** currency?.coinDecimals; + + if (isAuthzMode) { + txAuthzReDelegate({ + grantee: address, + granter: authzAddress, + srcValidator: srcVal, + validator: destVal, + amount: amount * 10 ** currency.coinDecimals, + denom: currency.coinMinimalDenom, + chainID: basicChainInfo.chainID, + }); + } else { + dispatch( + txReDelegate({ + isAuthzMode: false, + basicChainInfo: basicChainInfo, + delegator: basicChainInfo.address, + destVal: destVal, + srcVal: srcVal, + amount: amount * 10 ** currency?.coinDecimals, + denom: currency?.coinMinimalDenom, + feeAmount: feeAmount, + feegranter: getFeegranter(chainID, MAP_TXN_MSG_TYPES['redelegate']), + }) + ); + } + }; + + return { + getStakingAssets, + getAllDelegations, + fetchValidatorDetails, + getAmountWithDecimal, + chainTotalRewards, + chainLogo, + txWithdrawCliamRewards, + getClaimTxStatus, + txCancelUnbond, + txDelegateTx, + txAllChainStakeTxStatus, + txUnDelegateTx, + txReDelegateTx, + txWithdrawValRewards, + chainTotalValRewards, + delegationsLoading, + undelegationsLoading, + totalUnbondedAmount, + transactionRestake, + chainName, + getAmountObjectWithDecimal, + cancelUnbdongTxLoading, + }; +}; + +export default useStaking; diff --git a/frontend/src/custom-hooks/useSwaps.ts b/frontend/src/custom-hooks/useSwaps.ts new file mode 100644 index 000000000..95bd94bc8 --- /dev/null +++ b/frontend/src/custom-hooks/useSwaps.ts @@ -0,0 +1,169 @@ +import { useState } from 'react'; +import { + CosmosTransferAction, + GetRoute, + RouteData, + Swap, +} from '@0xsquid/sdk/dist/types'; +import { SWAP_ROUTE_ERROR } from '@/utils/constants'; +import useChain from './useChain'; +import { useAppSelector } from './StateHooks'; +import { SwapPathObject } from '@/types/swaps'; +import useGetChains from './useGetChains'; +import { fetchSwapRoute } from '@/store/features/swaps/swapsService'; + +interface GetRouteInputs { + sourceChainID: string; + sourceDenom: string; + destChainID: string; + destDenom: string; + amount: number; + fromAddress: string; + toAddress: string; + slippage: number; +} + +const useSwaps = () => { + const [routeLoading, setRouteLoading] = useState(false); + const [routeError, setRouteError] = useState(''); + const { getChainNameFromID } = useChain(); + const { getChainLogoURI } = useGetChains(); + const fromAmount = useAppSelector((state) => state.swaps.amountIn); + const toAmount = useAppSelector((state) => state.swaps.amountOut); + const getSwapRoute = async ({ + amount, + destChainID, + destDenom, + sourceChainID, + sourceDenom, + fromAddress, + toAddress, + slippage, + }: GetRouteInputs) => { + const params: GetRoute = { + fromAddress: fromAddress, + fromAmount: amount.toString(), + fromChain: sourceChainID, + fromToken: sourceDenom, + toAddress: toAddress, + toChain: destChainID, + toToken: destDenom, + slippage: slippage, + quoteOnly: false, + }; + try { + setRouteLoading(true); + setRouteError(''); + const res = await fetchSwapRoute(params); + setRouteLoading(false); + return { + resAmount: res.route.estimate.toAmount, + route: res.route, + }; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + if (error?.code === 'ERR_NETWORK') { + console.log(error.message); + setRouteError(error?.message || SWAP_ROUTE_ERROR); + } else { + const errMsg = + error?.response?.data?.errors?.[0]?.message || SWAP_ROUTE_ERROR; + console.log('error occured while fetch route', errMsg); + setRouteError(errMsg || SWAP_ROUTE_ERROR); + } + } finally { + setRouteLoading(false); + } + return { + resAmount: 0, + route: null, + }; + }; + + const getSwapPathData = (swapRoute: RouteData) => { + const pathData: SwapPathObject[] = []; + const fromTokenData = swapRoute.params.fromToken; + const toTokenData = swapRoute.params.toToken; + const fromTokenLogo = fromTokenData.logoURI; + const toTokenLogo = toTokenData.logoURI; + const fromChainId = fromTokenData.chainId; + const toChainId = toTokenData.chainId; + const fromTokenSymbol = fromTokenData.symbol; + const toTokenSymbol = toTokenData.symbol; + const { chainName: fromChainName } = getChainNameFromID( + fromChainId.toString() + ); + const { chainName: toChainName } = getChainNameFromID(toChainId.toString()); + const fromChainRoute = swapRoute.estimate.route.fromChain; + fromChainRoute.forEach((route) => { + if (route.type === 'Swap') { + const routePath = route as Swap; + const pathObject: SwapPathObject = { + type: 'swap', + value: { + dex: routePath.dex, + fromToken: { + symbol: routePath.fromToken.symbol, + logo: routePath.fromToken.logoURI, + }, + toToken: { + symbol: routePath.toToken.symbol, + logo: routePath.toToken.logoURI, + }, + }, + }; + pathData.push(pathObject); + } else if (route.type === 'Transfer') { + const routePath = route as CosmosTransferAction; + const { chainName: fromChainName } = getChainNameFromID( + routePath.fromChain + ); + const { chainName: toChainName } = getChainNameFromID( + routePath.toChain + ); + const fromChainLogo = getChainLogoURI( + routePath.fromToken.chainId.toString() + ); + const toChainLogo = getChainLogoURI( + routePath.toToken.chainId.toString() + ); + const pathObject: SwapPathObject = { + type: 'transfer', + value: { + fromChainName, + toChainName, + tokenLogo: routePath.fromToken.logoURI || routePath.toToken.logoURI, + fromChainLogo, + toChainLogo, + tokenSymbol: routePath.fromToken.symbol || routePath.toToken.symbol, + }, + }; + pathData.push(pathObject); + } + }); + return { + fromChainData: { + tokenLogo: fromTokenLogo, + amount: fromAmount, + tokenSymbol: fromTokenSymbol, + chainName: fromChainName, + }, + toChainData: { + tokenLogo: toTokenLogo, + amount: toAmount, + tokenSymbol: toTokenSymbol, + chainName: toChainName, + }, + pathData, + }; + }; + + return { + getSwapRoute, + getSwapPathData, + routeLoading, + routeError, + }; +}; + +export default useSwaps; diff --git a/frontend/src/custom-hooks/useValidator.ts b/frontend/src/custom-hooks/useValidator.ts new file mode 100644 index 000000000..d9c774a87 --- /dev/null +++ b/frontend/src/custom-hooks/useValidator.ts @@ -0,0 +1,57 @@ +import { useCallback } from 'react'; +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { RootState } from '@/store/store'; +import useGetChainInfo from './useGetChainInfo'; +import { getValidator } from '@/store/features/staking/stakeSlice'; + +const useValidator = () => { + const dispatch = useAppDispatch(); + + const isWalletConnected = useAppSelector( + (state: RootState) => state.wallet.connected + ); + const { getChainInfo } = useGetChainInfo(); + const stakeData = useAppSelector((state: RootState) => state.staking.chains); + + const fetchValidator = useCallback( + (valoperAddress: string, chainID: string) => { + if (isWalletConnected && valoperAddress && chainID) { + const { restURLs } = getChainInfo(chainID); + console.log( + 'staking------', + stakeData[chainID]?.validator[valoperAddress] + ); + if ( + ![ + ...(stakeData[chainID]?.validators?.activeSorted || []), + ...(stakeData[chainID]?.validators?.inactiveSorted || []), + ].includes(valoperAddress) + ) { + if (!stakeData[chainID]?.validator[valoperAddress]) + dispatch( + getValidator({ baseURLs: restURLs, chainID, valoperAddress }) + ); + } + } + }, + [] + ); + + const getValidatorDetails = useCallback( + (valoperAddress: string, chainID: string) => { + return ( + stakeData[chainID]?.validators.active?.[valoperAddress] || + stakeData[chainID]?.validators.inactive?.[valoperAddress] || + stakeData[chainID]?.validator?.[valoperAddress] + ); + }, + [stakeData] + ); + + return { + fetchValidator, + getValidatorDetails, + }; +}; + +export default useValidator; diff --git a/frontend/src/custom-hooks/useVerifyAccount.tsx b/frontend/src/custom-hooks/useVerifyAccount.tsx new file mode 100644 index 000000000..0f667c3da --- /dev/null +++ b/frontend/src/custom-hooks/useVerifyAccount.tsx @@ -0,0 +1,61 @@ +import { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from './StateHooks'; +import { + resetVerifyAccountRes, + setVerifyDialogOpen, + verifyAccount, +} from '@/store/features/multisig/multisigSlice'; +import { setAuthToken } from '@/utils/localStorage'; +import { setError } from '@/store/features/common/commonSlice'; +import { isVerified } from '@/utils/util'; +import { COSMOS_CHAIN_ID } from '@/utils/constants'; +import { getAddressByPrefix } from '@/utils/address'; + +const useVerifyAccount = ({ address }: { address: string }) => { + const dispatch = useAppDispatch(); + const verifyAccountRes = useAppSelector( + (state) => state.multisig.verifyAccountRes + ); + const cosmosAddresss = getAddressByPrefix(address, 'cosmos'); + useEffect(() => { + if (verifyAccountRes.status === 'idle') { + setAuthToken({ + chainID: COSMOS_CHAIN_ID, + address: cosmosAddresss, + signature: verifyAccountRes.token, + }); + dispatch(setVerifyDialogOpen(false)); + dispatch( + setError({ + type: 'success', + message: 'Verified, You can now perform actions', + }) + ); + dispatch(resetVerifyAccountRes()); + } else if (verifyAccountRes.status === 'rejected') { + dispatch( + setError({ + type: 'error', + message: verifyAccountRes.error, + }) + ); + } + }, [verifyAccountRes]); + + const verifyOwnership = () => { + dispatch( + verifyAccount({ chainID: COSMOS_CHAIN_ID, address: cosmosAddresss }) + ); + }; + + const isAccountVerified = () => { + const verified = isVerified({ + chainID: COSMOS_CHAIN_ID, + address: cosmosAddresss, + }); + return verified; + }; + return { verifyOwnership, isAccountVerified }; +}; + +export default useVerifyAccount; diff --git a/frontend/src/example-files/delegate.csv b/frontend/src/example-files/delegate.csv new file mode 100644 index 000000000..76642ca5f --- /dev/null +++ b/frontend/src/example-files/delegate.csv @@ -0,0 +1,3 @@ +cosmosvaloper46gthgesdfsdfetgh3c48nenyng4n56nabcd, 100000uatom +cosmosvaloper46gthgesdfsddfdsfgdsfgdenyng4n56nefgh, 10000000uatom +cosmosvaloper46gthgesdfsdfetgh3c48nenyng4n56nabcd, 10000uatom \ No newline at end of file diff --git a/frontend/src/example-files/deposit.csv b/frontend/src/example-files/deposit.csv new file mode 100644 index 000000000..fe986277d --- /dev/null +++ b/frontend/src/example-files/deposit.csv @@ -0,0 +1,3 @@ +25, 1250000uatom +26, 1000000uatom +27, 3400000uatom \ No newline at end of file diff --git a/frontend/src/example-files/redelegate.csv b/frontend/src/example-files/redelegate.csv new file mode 100644 index 000000000..4552047c1 --- /dev/null +++ b/frontend/src/example-files/redelegate.csv @@ -0,0 +1,2 @@ +cosmosvaloper46gthgesdfsdfetgh3c48nenyng4n56nabcd, cosmosvaloper46gthgesdfsddfdsfgdsfgdenyng4n56nefgh, 100000uatom +cosmosvaloper46gthgesdfsddfdsfgdsfgdenyng4n56nefgh, cosmosvaloper46gthgesdfsdfetgh3c48nenyng4n56nabcd, 10000000uatom \ No newline at end of file diff --git a/frontend/src/example-files/send.csv b/frontend/src/example-files/send.csv new file mode 100644 index 000000000..87558c67d --- /dev/null +++ b/frontend/src/example-files/send.csv @@ -0,0 +1,3 @@ +cosmos1n6cwfef3ldeel95t58cuwppu4eqpqm3p5ncs7q, 100000uatom +cosmos17utzkcz9ecfv489kuhjs5scf832a6deeamvj25, 2300372uatom +cosmos1mmygwmjxzfm98kuy52aaqaznzx723nrentwgpy, 4590007uatom \ No newline at end of file diff --git a/frontend/src/example-files/undelegate.csv b/frontend/src/example-files/undelegate.csv new file mode 100644 index 000000000..76642ca5f --- /dev/null +++ b/frontend/src/example-files/undelegate.csv @@ -0,0 +1,3 @@ +cosmosvaloper46gthgesdfsdfetgh3c48nenyng4n56nabcd, 100000uatom +cosmosvaloper46gthgesdfsddfdsfgdsfgdenyng4n56nefgh, 10000000uatom +cosmosvaloper46gthgesdfsdfetgh3c48nenyng4n56nabcd, 10000uatom \ No newline at end of file diff --git a/frontend/src/example-files/vote.csv b/frontend/src/example-files/vote.csv new file mode 100644 index 000000000..0902b6296 --- /dev/null +++ b/frontend/src/example-files/vote.csv @@ -0,0 +1,4 @@ +25, yes +26, abstain +27, no +28, veto \ No newline at end of file diff --git a/frontend/src/store/features/auth/authService.ts b/frontend/src/store/features/auth/authService.ts index a2cd47b11..836d9f3a7 100644 --- a/frontend/src/store/features/auth/authService.ts +++ b/frontend/src/store/features/auth/authService.ts @@ -1,14 +1,20 @@ -import Axios, { AxiosResponse } from 'axios'; -import { cleanURL } from '@/utils/util'; -import { QueryAccountResponse } from 'cosmjs-types/cosmos/auth/v1beta1/query'; +import { AxiosResponse } from 'axios'; +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; +import { addChainIDParam } from '@/utils/util'; const accountInfoURL = '/cosmos/auth/v1beta1/accounts/'; const fetchAccountInfo = ( - baseURL: string, - address: string -): Promise> => - Axios.get(`${cleanURL(baseURL)}${accountInfoURL}${address}`); + baseURLs: string[], + address: string, + chainID: string + /* eslint-disable @typescript-eslint/no-explicit-any */ +): Promise> => { + let endPoint = `${accountInfoURL}${address}`; + endPoint = addChainIDParam(endPoint, chainID); + + return axiosGetRequestWrapper(baseURLs, endPoint); +}; const result = { accountInfo: fetchAccountInfo, diff --git a/frontend/src/store/features/auth/authSlice.ts b/frontend/src/store/features/auth/authSlice.ts index 1521217c0..1be861548 100644 --- a/frontend/src/store/features/auth/authSlice.ts +++ b/frontend/src/store/features/auth/authSlice.ts @@ -25,8 +25,13 @@ const initialState: AuthState = {}; export const getAccountInfo = createAsyncThunk( 'auth/accountInfo', - async (data: { chainID: string; baseURL: string; address: string }) => { - const response = await authService.accountInfo(data.baseURL, data.address); + async (data: { + chainID: string; + baseURL: string; + address: string; + baseURLs: string[]; + }) => { + const response = await authService.accountInfo(data.baseURLs, data.address, data.chainID); return response.data; } ); diff --git a/frontend/src/store/features/authz/authzSlice.ts b/frontend/src/store/features/authz/authzSlice.ts new file mode 100644 index 000000000..3b4bfbfed --- /dev/null +++ b/frontend/src/store/features/authz/authzSlice.ts @@ -0,0 +1,573 @@ +'use client'; + +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import authzService from './service'; +import { TxStatus } from '../../../types/enums'; +import { cloneDeep } from 'lodash'; +import { getAddressByPrefix } from '@/utils/address'; +import { signAndBroadcast } from '@/utils/signing'; +import { setError, setTxAndHash } from '../common/commonSlice'; +import { NewTransaction } from '@/utils/transaction'; +import { FAILED, GAS_FEE, SUCCESS } from '@/utils/constants'; +import { ERR_UNKNOWN } from '@/utils/errors'; +import { AxiosError } from 'axios'; +import { trackEvent } from '@/utils/util'; + +interface ChainAuthz { + grantsToMe: Authorization[]; + grantsByMe: Authorization[]; + getGrantsToMeLoading: { + status: TxStatus; + errMsg: string; + }; + getGrantsByMeLoading: { + status: TxStatus; + errMsg: string; + }; + + /* + this is mapping of address to list of authorizations (chain level) + example : { + "pasg1..." : ["stakeAuthorization...", "sendAuthorization..."] + } + */ + + GrantsToMeAddressMapping: Record; + GrantsByMeAddressMapping: Record; + tx: { + status: TxStatus; + errMsg: string; + type?: string; + }; +} + +interface GetAuthRevokeInputs { + basicChainInfo: BasicChainInfo; + feegranter: string; + denom: string; + msgs: Msg[]; + feeAmount: number; +} +const defaultState: ChainAuthz = { + grantsToMe: [], + grantsByMe: [], + getGrantsByMeLoading: { + status: TxStatus.INIT, + errMsg: '', + }, + getGrantsToMeLoading: { + status: TxStatus.INIT, + errMsg: '', + }, + GrantsByMeAddressMapping: {}, + GrantsToMeAddressMapping: {}, + tx: { + status: TxStatus.INIT, + errMsg: '', + }, +}; + +interface AuthzState { + authzModeEnabled: boolean; + authzAddress: string; + chains: Record; + getGrantsToMeLoading: number; + getGrantsByMeLoading: number; + /* + this is mapping of address to chain id to list of authorizations (inter chain level) + example : { + "cosmos1..." : { + "cosmoshub-4": ["stakeAuthorization...", "sendAuthorization..."] + } + } + */ + AddressToChainAuthz: Record>; + multiChainAuthzGrantTx: { + status: TxStatus; + }; + authzAlert: { + display: boolean; + }; +} + +const initialState: AuthzState = { + authzModeEnabled: false, + authzAddress: '', + chains: {}, + getGrantsByMeLoading: 0, + getGrantsToMeLoading: 0, + AddressToChainAuthz: {}, + multiChainAuthzGrantTx: { + status: TxStatus.INIT, + }, + authzAlert: { + display: true, + }, +}; + +export const getGrantsToMe = createAsyncThunk( + 'authz/grantsToMe', + async (data: GetGrantsInputs) => { + const response = await authzService.grantsToMe( + data.baseURLs, + data.address, + data.chainID + ); + + return { + data: response.data, + }; + } +); + +export const getGrantsByMe = createAsyncThunk( + 'authz/grantsByMe', + async (data: GetGrantsInputs) => { + const response = await authzService.grantsByMe( + data.baseURLs, + data.address, + data.chainID + ); + return { + data: response.data, + }; + } +); + +export const txCreateMultiChainAuthzGrant = createAsyncThunk( + 'authz/create-multichain-grant', + async (data: TxGrantMultiChainAuthzInputs, { rejectWithValue, dispatch }) => { + try { + const promises = data.data.map((chainGrant) => { + return dispatch(txCreateAuthzGrant(chainGrant)); + }); + await Promise.all(promises); + data.data.forEach((chainGrant) => { + dispatch(txCreateAuthzGrant(chainGrant)); + }); + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.response); + } + } +); + +export const txCreateAuthzGrant = createAsyncThunk( + 'authz/create-grant', + async ( + data: TxGrantAuthzInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + data.msgs, + GAS_FEE, + '', + `${data.feeAmount}${data.denom}`, + data.basicChainInfo.rest, + data.feegranter?.length > 0 ? data.feegranter : undefined, + '', + data?.basicChainInfo?.restURLs + ); + + // TODO: Store txn, (This is throwing error because of BigInt in message) + // const tx = NewTransaction( + // result, + // data.msgs, + // data.basicChainInfo.chainID, + // data.basicChainInfo.address + // ); + // dispatch( + // addTransactions({ + // chainID: data.basicChainInfo.chainID, + // address: data.basicChainInfo.cosmosAddress, + // transactions: [tx], + // }) + // ); + + // dispatch( + // setTxAndHash({ + // tx: undefined, + // hash: tx.transactionHash, + // }) + // ); + + if (result?.code === 0) { + dispatch( + getGrantsByMe({ + baseURL: data.basicChainInfo.baseURL, + baseURLs: data.basicChainInfo.restURLs, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + }) + ); + trackEvent('AUTHZ', 'GRANT_AUTHZ', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + trackEvent('AUTHZ', 'GRANT_AUTHZ', FAILED); + return rejectWithValue(result?.rawLog); + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('AUTHZ', 'GRANT_AUTHZ', FAILED); + return rejectWithValue(error?.message || ERR_UNKNOWN); + } + } +); + +export const txAuthzExec = createAsyncThunk( + 'authz/tx-exec', + async ( + data: TxAuthzExecInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + data.msgs, + GAS_FEE, + data.memo, + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + '', + data?.basicChainInfo?.restURLs + ); + if (result?.code === 0) { + const tx = NewTransaction( + result, + data.msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.cosmosAddress + ); + trackEvent('AUTHZ', 'EXEC_AUTHZ', SUCCESS); + dispatch( + setTxAndHash({ + hash: result?.transactionHash, + tx, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + trackEvent('AUTHZ', 'GRANT_AUTHZ', FAILED); + dispatch( + setError({ + type: 'error', + message: result?.rawLog || 'transaction Failed', + }) + ); + return rejectWithValue(result?.rawLog); + } + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('AUTHZ', 'GRANT_AUTHZ', FAILED); + dispatch( + setError({ + type: 'error', + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + } +); + +export const txAuthzRevoke = createAsyncThunk( + 'authz/tx-revoke', + async ( + data: GetAuthRevokeInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + data.msgs, + GAS_FEE, + '', + `${data.feeAmount}${data.denom}`, + data.basicChainInfo.rest, + undefined, + '', + data?.basicChainInfo?.restURLs + // data.feegranter?.length > 0 ? data.feegranter : undefined + ); + if (result?.code === 0) { + const tx = NewTransaction( + result, + data.msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.cosmosAddress + ); + trackEvent('AUTHZ', 'GRANT_AUTHZ', SUCCESS); + dispatch( + setTxAndHash({ + tx: tx, + hash: result?.transactionHash, + }) + ); + dispatch( + getGrantsByMe({ + baseURL: data.basicChainInfo.baseURL, + baseURLs: data.basicChainInfo.restURLs, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + dispatch( + setError({ + type: 'error', + message: result?.rawLog || '', + }) + ); + trackEvent('AUTHZ', 'REVOKE_AUTHZ', FAILED); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + trackEvent('AUTHZ', 'REVOKE_AUTHZ', FAILED); + dispatch( + setError({ + type: 'error', + message: ERR_UNKNOWN, + }) + ); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const authzSlice = createSlice({ + name: 'authz', + initialState, + reducers: { + enableAuthzMode: (state, action: PayloadAction<{ address: string }>) => { + state.authzModeEnabled = true; + state.authzAddress = action.payload.address; + }, + exitAuthzMode: (state) => { + state.authzModeEnabled = false; + state.authzAddress = ''; + }, + resetState: (state) => { + /* eslint-disable @typescript-eslint/no-unused-vars */ + state = cloneDeep(initialState); + }, + resetTxStatus: (state, action: PayloadAction<{ chainID: string }>) => { + const { chainID } = action.payload; + state.chains[chainID].tx = { + errMsg: '', + status: TxStatus.INIT, + }; + }, + setAuthzAlert: (state, action: PayloadAction) => { + if (state.authzAlert.display) { + state.authzAlert.display = action.payload; + } + }, + }, + extraReducers: (builder) => { + builder + .addCase(getGrantsToMe.pending, (state, action) => { + state.getGrantsToMeLoading++; + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + state.chains[chainID].getGrantsToMeLoading.status = TxStatus.PENDING; + state.chains[chainID].grantsToMe = []; + state.chains[chainID].GrantsToMeAddressMapping = {}; + const allAddressToAuthz = state.AddressToChainAuthz; + const addresses = Object.keys(allAddressToAuthz); + addresses.forEach((address) => { + allAddressToAuthz[address][chainID] = []; + }); + + state.AddressToChainAuthz = allAddressToAuthz; + }) + .addCase(getGrantsToMe.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + const allAddressToAuthz = state.AddressToChainAuthz; + const addresses = Object.keys(allAddressToAuthz); + addresses.forEach((address) => { + allAddressToAuthz[address][chainID] = []; + }); + + state.AddressToChainAuthz = allAddressToAuthz; + + state.getGrantsToMeLoading--; + + const grants = action.payload.data.grants || []; + state.chains[chainID].grantsToMe = grants; + const addressMapping: Record = {}; + const allChainsAddressToGrants = state.AddressToChainAuthz; + + grants && + grants.forEach((grant: Authorization) => { + const granter = grant.granter; + const cosmosAddress = getAddressByPrefix(granter, 'cosmos'); + if (!addressMapping[granter]) addressMapping[granter] = []; + if (!allChainsAddressToGrants[cosmosAddress]) + allChainsAddressToGrants[cosmosAddress] = {}; + if (!allChainsAddressToGrants[cosmosAddress][chainID]) + allChainsAddressToGrants[cosmosAddress][chainID] = []; + allChainsAddressToGrants[cosmosAddress][chainID] = [ + ...allChainsAddressToGrants[cosmosAddress][chainID], + grant, + ]; + addressMapping[granter] = [...addressMapping[granter], grant]; + }); + state.AddressToChainAuthz = allChainsAddressToGrants; + state.chains[chainID].GrantsToMeAddressMapping = addressMapping; + state.chains[chainID].getGrantsToMeLoading = { + status: TxStatus.IDLE, + errMsg: '', + }; + }) + .addCase(getGrantsToMe.rejected, (state, action) => { + state.getGrantsToMeLoading--; + const chainID = action.meta.arg.chainID; + + state.chains[chainID].getGrantsToMeLoading = { + status: TxStatus.REJECTED, + errMsg: + action.error.message || + 'An error occurred while fetching authz grants to me', + }; + }); + builder + .addCase(getGrantsByMe.pending, (state, action) => { + state.getGrantsByMeLoading++; + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + state.chains[chainID].getGrantsByMeLoading.status = TxStatus.PENDING; + state.chains[chainID].grantsByMe = []; + state.chains[chainID].GrantsByMeAddressMapping = {}; + }) + .addCase(getGrantsByMe.fulfilled, (state, action) => { + state.getGrantsByMeLoading--; + const chainID = action.meta.arg.chainID; + const grants = action.payload.data.grants || []; + state.chains[chainID].grantsByMe = grants; + const addressMapping: Record = {}; + grants.forEach((grant: Authorization) => { + const granter = grant.grantee; + if (!addressMapping[granter]) addressMapping[granter] = []; + addressMapping[granter] = [...addressMapping[granter], grant]; + }); + state.chains[chainID].GrantsByMeAddressMapping = addressMapping; + state.chains[chainID].getGrantsByMeLoading = { + status: TxStatus.IDLE, + errMsg: '', + }; + }) + .addCase(getGrantsByMe.rejected, (state, action) => { + state.getGrantsByMeLoading--; + const chainID = action.meta.arg.chainID; + + state.chains[chainID].getGrantsByMeLoading = { + status: TxStatus.REJECTED, + errMsg: + action.error.message || + 'An error occurred while fetching authz grants by me', + }; + }); + builder + .addCase(txAuthzExec.pending, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + const actionType = action.meta.arg.type; + state.chains[chainID].tx.status = TxStatus.PENDING; + state.chains[chainID].tx.errMsg = ''; + state.chains[chainID].tx.type = actionType; + }) + .addCase(txAuthzExec.fulfilled, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + state.chains[chainID].tx.status = TxStatus.IDLE; + state.chains[chainID].tx.type = ''; + }) + .addCase(txAuthzExec.rejected, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + state.chains[chainID].tx.status = TxStatus.REJECTED; + state.chains[chainID].tx.errMsg = action.error.message || 'rejected'; + state.chains[chainID].tx.type = ''; + }); + builder + .addCase(txCreateAuthzGrant.pending, (state, action) => { + const { chainID } = action.meta.arg.basicChainInfo; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + state.chains[chainID].tx.status = TxStatus.PENDING; + state.chains[chainID].tx.errMsg = ''; + }) + .addCase(txCreateAuthzGrant.fulfilled, (state, action) => { + const { chainID } = action.meta.arg.basicChainInfo; + const { txHash } = action.payload; + state.chains[chainID].tx.status = TxStatus.IDLE; + state.chains[chainID].tx.errMsg = ''; + action.meta.arg.onTxComplete?.({ + isTxSuccess: true, + txHash: txHash, + }); + }) + .addCase(txCreateAuthzGrant.rejected, (state, action) => { + const { chainID } = action.meta.arg.basicChainInfo; + state.chains[chainID].tx.status = TxStatus.REJECTED; + state.chains[chainID].tx.errMsg = + typeof action.payload === 'string' ? action.payload : ''; + action.meta.arg.onTxComplete?.({ + isTxSuccess: false, + error: + typeof action.payload === 'string' ? action.payload : ERR_UNKNOWN, + }); + }); + + builder + .addCase(txCreateMultiChainAuthzGrant.pending, (state) => { + state.multiChainAuthzGrantTx.status = TxStatus.PENDING; + }) + .addCase(txCreateMultiChainAuthzGrant.fulfilled, (state) => { + state.multiChainAuthzGrantTx.status = TxStatus.IDLE; + }) + .addCase(txCreateMultiChainAuthzGrant.rejected, (state) => { + state.multiChainAuthzGrantTx.status = TxStatus.REJECTED; + }); + + builder + .addCase(txAuthzRevoke.pending, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + state.chains[chainID].tx.status = TxStatus.PENDING; + state.chains[chainID].tx.errMsg = ''; + }) + .addCase(txAuthzRevoke.fulfilled, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + state.chains[chainID].tx.status = TxStatus.IDLE; + }) + .addCase(txAuthzRevoke.rejected, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + state.chains[chainID].tx.status = TxStatus.REJECTED; + state.chains[chainID].tx.errMsg = action.error.message || 'rejected'; + }); + }, +}); + +export const { + enableAuthzMode, + exitAuthzMode, + resetState, + resetTxStatus, + setAuthzAlert, +} = authzSlice.actions; + +export default authzSlice.reducer; diff --git a/frontend/src/store/features/authz/service.ts b/frontend/src/store/features/authz/service.ts new file mode 100644 index 000000000..84f290652 --- /dev/null +++ b/frontend/src/store/features/authz/service.ts @@ -0,0 +1,49 @@ +import { AxiosResponse } from 'axios'; +import { addChainIDParam, convertPaginationToParams } from '@/utils/util'; +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; + +const grantToMeURL = '/cosmos/authz/v1beta1/grants/grantee/'; +const grantByMeURL = '/cosmos/authz/v1beta1/grants/granter/'; + +const fetchGrantsToMe = ( + baseURLs: string[], + grantee: string, + chainID:string, + pagination?: KeyLimitPagination, +): Promise> => { + let endPoint = `${grantToMeURL}${grantee}`; + + const parsed = convertPaginationToParams(pagination); + if (parsed !== '') { + endPoint += `?${parsed}`; + } + + endPoint = addChainIDParam(endPoint, chainID); + + return axiosGetRequestWrapper(baseURLs, endPoint); +}; + +const fetchGrantsByMe = ( + baseURLs: string[], + grantee: string, + chainID: string, + pagination?: KeyLimitPagination, +): Promise> => { + let endPoint = `${grantByMeURL}${grantee}`; + + const parsed = convertPaginationToParams(pagination); + if (parsed !== '') { + endPoint += `?${parsed}`; + } + + endPoint = addChainIDParam(endPoint, chainID); + + return axiosGetRequestWrapper(baseURLs, endPoint); +}; + +const result = { + grantsByMe: fetchGrantsByMe, + grantsToMe: fetchGrantsToMe, +}; + +export default result; diff --git a/frontend/src/store/features/bank/bankService.ts b/frontend/src/store/features/bank/bankService.ts index b9b7b52f4..f71146e1b 100644 --- a/frontend/src/store/features/bank/bankService.ts +++ b/frontend/src/store/features/bank/bankService.ts @@ -1,34 +1,42 @@ -'use client' +'use client'; -import Axios, { AxiosResponse } from 'axios'; -import { convertPaginationToParams, cleanURL } from '../../../utils/util'; +import { AxiosResponse } from 'axios'; +import { + addChainIDParam, + convertPaginationToParams, +} from '../../../utils/util'; +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; const balancesURL = '/cosmos/bank/v1beta1/balances/'; const balanceURL = (address: string, denom: string) => `/cosmos/bank/v1beta1/balances/${address}/by_denom?denom=${denom}`; const fetchBalances = ( - baseURL: string, + baseURLs: string[], address: string, + chainID: string, pagination?: KeyLimitPagination ): Promise => { - let uri = `${cleanURL(baseURL)}${balancesURL}${address}`; + let resourceEndPoint = `${balancesURL}${address}`; const parsed = convertPaginationToParams(pagination); if (parsed !== '') { - uri += `?${parsed}`; + resourceEndPoint += `?${parsed}`; } + resourceEndPoint = addChainIDParam(resourceEndPoint, chainID); - return Axios.get(uri); + return axiosGetRequestWrapper(baseURLs, resourceEndPoint); }; const fetchBalance = ( - baseURL: string, + baseURLs: string[], address: string, - denom: string + denom: string, + chainID: string ): Promise => { - const uri = `${cleanURL(baseURL)}${balanceURL(address, denom)}`; + let resourceEndPoint = `${balanceURL(address, denom)}`; + resourceEndPoint = addChainIDParam(resourceEndPoint, chainID); - return Axios.get(uri); + return axiosGetRequestWrapper(baseURLs, resourceEndPoint); }; const result = { diff --git a/frontend/src/store/features/bank/bankSlice.ts b/frontend/src/store/features/bank/bankSlice.ts index 6bf2bace0..d33794662 100644 --- a/frontend/src/store/features/bank/bankSlice.ts +++ b/frontend/src/store/features/bank/bankSlice.ts @@ -1,6 +1,6 @@ 'use client'; -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { SendMsg } from '../../../txns/bank'; import bankService from './bankService'; import { signAndBroadcast } from '../../../utils/signing'; @@ -8,9 +8,10 @@ import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; import { GAS_FEE } from '../../../utils/constants'; import { TxStatus } from '../../../types/enums'; import { ERR_UNKNOWN } from '@/utils/errors'; -import { addTransactions } from '../transactionHistory/transactionHistorySlice'; import { NewTransaction } from '@/utils/transaction'; -import { setTxAndHash } from '../common/commonSlice'; +import { setError, setTxAndHash } from '../common/commonSlice'; +import cloneDeep from 'lodash/cloneDeep'; +import { trackEvent } from '@/utils/util'; interface Balance { list: Coin[]; @@ -26,6 +27,17 @@ interface BankState { multiSendTx: { status: TxStatus; }; + authz: { + balancesLoading: number; + balances: { [key: string]: Balance }; + tx: { + status: TxStatus; + }; + multiSendTx: { + status: TxStatus; + }; + }; + showIBCSendAlert: boolean; } const initialState: BankState = { @@ -35,19 +47,52 @@ const initialState: BankState = { status: TxStatus.INIT, }, multiSendTx: { status: TxStatus.INIT }, + authz: { + balancesLoading: 0, + balances: {}, + tx: { + status: TxStatus.INIT, + }, + multiSendTx: { status: TxStatus.INIT }, + }, + showIBCSendAlert: false, }; export const getBalances = createAsyncThunk( 'bank/balances', async (data: { + baseURLs: string[]; + baseURL: string; + address: string; + chainID: string; + pagination?: KeyLimitPagination; + }) => { + const response = await bankService.balances( + data.baseURLs, + data.address, + data.chainID, + data.pagination + ); + return { + chainID: data.chainID, + data: response.data, + }; + } +); + +export const getAuthzBalances = createAsyncThunk( + 'bank/authz-balances', + async (data: { + baseURLs: string[]; baseURL: string; address: string; chainID: string; pagination?: KeyLimitPagination; }) => { const response = await bankService.balances( - data.baseURL, + data.baseURLs, data.address, + data.chainID, data.pagination ); return { @@ -63,15 +108,8 @@ export const multiTxns = createAsyncThunk( data: MultiTxnsInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { - const { - chainID, - cosmosAddress, - prefix, - aminoConfig, - feeAmount, - address, - rest, - } = data.basicChainInfo; + const { chainID, prefix, aminoConfig, feeAmount, address, rest, restURLs } = + data.basicChainInfo; try { const result = await signAndBroadcast( chainID, @@ -82,26 +120,41 @@ export const multiTxns = createAsyncThunk( data.memo, `${feeAmount}${data.denom}`, rest, - data.feegranter?.length > 0 ? data.feegranter : undefined + data.feegranter?.length > 0 ? data.feegranter : undefined, + '', + restURLs ); const tx = NewTransaction(result, data.msgs, chainID, address); - dispatch( - addTransactions({ - chainID: chainID, - address: cosmosAddress, - transactions: [tx], - }) - ); dispatch(setTxAndHash({ tx, hash: tx.transactionHash })); if (result?.code === 0) { - dispatch(getBalances({ baseURL: rest, chainID, address })); + dispatch( + getBalances({ + baseURL: rest, + chainID, + address, + baseURLs: restURLs, + }) + ); + + trackEvent('TRANSFER', 'MULTI_SEND', 'SUCCESS'); + return fulfillWithValue({ txHash: result?.transactionHash }); } else { + trackEvent('TRANSFER', 'MULTI_SEND', 'FAILED'); + return rejectWithValue(result?.rawLog); } /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ } catch (error: any) { - return rejectWithValue(error.message); + trackEvent('TRANSFER', 'MULTI_SEND', 'FAILED'); + const errMessage = error?.response?.data?.error || error?.message; + dispatch( + setError({ + type: 'error', + message: errMessage || ERR_UNKNOWN, + }) + ); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -109,48 +162,90 @@ export const multiTxns = createAsyncThunk( export const txBankSend = createAsyncThunk( 'bank/tx-bank-send', async ( - data: TxSendInputs, + data: TxSendInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { - const { chainID, cosmosAddress } = data.basicChainInfo; + const { chainID } = data.basicChainInfo; try { - const msg = SendMsg(data.from, data.to, data.amount, data.denom); - const result = await signAndBroadcast( + let msgs: Msg[] = []; + + if (data.isAuthzMode) { + msgs = data.msgs; + } else { + msgs = [SendMsg(data.from, data.to, data.amount, data.denom)]; + } + + let result; + + try { + result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + msgs, + GAS_FEE, + data.memo, + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs + ); + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + return rejectWithValue(error?.message); + } + + const tx = NewTransaction( + result, + msgs, chainID, - data.basicChainInfo.aminoConfig, - data.prefix, - [msg], - GAS_FEE, - data.memo, - `${data.feeAmount}${data.denom}`, - data.basicChainInfo.rest, - data.feegranter?.length > 0 ? data.feegranter : undefined - ); - const tx = NewTransaction(result, [msg], chainID, data.from); - dispatch( - addTransactions({ - chainID: data.basicChainInfo.chainID, - address: cosmosAddress, - transactions: [tx], - }) + data.basicChainInfo.address ); dispatch(setTxAndHash({ tx, hash: tx.transactionHash })); if (result?.code === 0) { - dispatch( - getBalances({ - baseURL: data.basicChainInfo.rest, - chainID, - address: data.basicChainInfo.address, - }) - ); + if (data.isAuthzMode) { + dispatch( + getAuthzBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.rest, + chainID, + address: data.authzChainGranter, + }) + ); + } else { + dispatch( + getBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.rest, + chainID, + address: data.basicChainInfo.address, + }) + ); + } + + trackEvent('TRANSFER', 'SEND', 'SUCCESS'); + return fulfillWithValue({ txHash: result?.transactionHash }); } else { + trackEvent('TRANSFER', 'SEND', 'FAILED'); + return rejectWithValue(result?.rawLog); } /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ } catch (error: any) { - return rejectWithValue(error.message); + trackEvent('TRANSFER', 'SEND', 'FAILED'); + const errMessage = error?.response?.data?.error || error?.message; + dispatch( + setError({ + type: 'error', + message: errMessage || ERR_UNKNOWN, + }) + ); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -171,6 +266,23 @@ export const bankSlice = createSlice({ resetMultiSendTxRes: (state) => { state.multiSendTx = { status: TxStatus.INIT }; }, + resetState: (state) => { + /* eslint-disable-next-line */ + state = cloneDeep(initialState); + }, + resetAuthz: (state) => { + state.authz = { + balancesLoading: 0, + balances: {}, + tx: { + status: TxStatus.INIT, + }, + multiSendTx: { status: TxStatus.INIT }, + }; + }, + setIBCSendAlert: (state, action: PayloadAction) => { + state.showIBCSendAlert = action.payload; + }, }, extraReducers: (builder) => { @@ -208,12 +320,47 @@ export const bankSlice = createSlice({ state.balancesLoading--; }); + builder + .addCase(getAuthzBalances.pending, (state, action) => { + const chainID = action.meta.arg.chainID; + if (!state.authz.balances[chainID]) { + state.authz.balances[chainID] = { + list: [], + status: TxStatus.INIT, + errMsg: '', + }; + } + state.authz.balances[chainID].status = TxStatus.PENDING; + state.authz.balancesLoading++; + }) + .addCase(getAuthzBalances.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + + const result = { + list: action.payload.data?.balances, + status: TxStatus.IDLE, + errMsg: '', + }; + state.authz.balances[chainID] = result; + state.authz.balancesLoading--; + }) + .addCase(getAuthzBalances.rejected, (state, action) => { + const chainID = action.meta.arg.chainID; + state.authz.balances[chainID] = { + status: TxStatus.REJECTED, + errMsg: action?.error?.message || ERR_UNKNOWN, + list: [], + }; + state.authz.balancesLoading--; + }); + builder .addCase(txBankSend.pending, (state) => { state.tx.status = TxStatus.PENDING; }) - .addCase(txBankSend.fulfilled, (state) => { + .addCase(txBankSend.fulfilled, (state, action) => { state.tx.status = TxStatus.IDLE; + action.meta.arg?.onTxSuccessCallBack?.(); }) .addCase(txBankSend.rejected, (state) => { state.tx.status = TxStatus.REJECTED; @@ -235,5 +382,11 @@ export const bankSlice = createSlice({ }, }); -export const { claimRewardInBank, resetMultiSendTxRes } = bankSlice.actions; +export const { + claimRewardInBank, + resetMultiSendTxRes, + resetState, + resetAuthz, + setIBCSendAlert, +} = bankSlice.actions; export default bankSlice.reducer; diff --git a/frontend/src/store/features/common/commonSlice.ts b/frontend/src/store/features/common/commonSlice.ts index 699fb1cb1..474add7ae 100644 --- a/frontend/src/store/features/common/commonSlice.ts +++ b/frontend/src/store/features/common/commonSlice.ts @@ -4,6 +4,8 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import commonService from './commonService'; import { AxiosError } from 'axios'; import { ERR_UNKNOWN } from '../../../utils/errors'; +import { networks } from '../../../utils/chainsInfo'; +import { getLocalNetworks } from '@/utils/localStorage'; const initialState: CommonState = { errState: { @@ -31,9 +33,16 @@ const initialState: CommonState = { info: {}, status: 'idle', }, + changeNetworkDialog: { + open: false, + showSearch: false, + }, selectedNetwork: { chainName: '', }, + allNetworksInfo: {}, + nameToChainIDs: {}, + addNetworkOpen: false, }; export const getTokenPrice = createAsyncThunk( @@ -96,9 +105,33 @@ export const commonSlice = createSlice({ type: '', }; }, + setChangeNetworkDialogOpen: ( + state, + action: PayloadAction<{ open: boolean; showSearch: boolean }> + ) => { + state.changeNetworkDialog.open = action.payload.open; + state.changeNetworkDialog.showSearch = action.payload.showSearch; + }, + setAddNetworkDialogOpen: (state, action: PayloadAction) => { + state.addNetworkOpen = action.payload; + }, setSelectedNetwork: (state, action: PayloadAction) => { state.selectedNetwork.chainName = action.payload.chainName; }, + setAllNetworksInfo: (state) => { + state.allNetworksInfo = {}; + const networksList = [...networks, ...getLocalNetworks()]; + for (let i = 0; i < networksList.length; i++) { + state.allNetworksInfo[networksList?.[i]?.config?.chainId] = + networksList?.[i]; + state.nameToChainIDs[ + networksList?.[i]?.config?.chainName + ?.toLowerCase() + .split(' ') + .join('') + ] = networksList?.[i]?.config?.chainId; + } + }, }, extraReducers: (builder) => { builder @@ -152,6 +185,9 @@ export const { setTxAndHash, resetTxAndHash, setSelectedNetwork, + setAllNetworksInfo, + setChangeNetworkDialogOpen, + setAddNetworkDialogOpen, } = commonSlice.actions; export default commonSlice.reducer; diff --git a/frontend/src/store/features/cosmwasm/cosmwasmService.ts b/frontend/src/store/features/cosmwasm/cosmwasmService.ts new file mode 100644 index 000000000..5027560cd --- /dev/null +++ b/frontend/src/store/features/cosmwasm/cosmwasmService.ts @@ -0,0 +1,119 @@ +'use client'; + +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; +import { addChainIDParam } from '@/utils/util'; +import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'; + +const getContractURL = (baseURL: string, address: string) => + `${baseURL}/cosmwasm/wasm/v1/contract/${address}`; + +const codesURL = '/cosmwasm/wasm/v1/code'; +const contractsByCodeURL = (codeId: string) => + `/cosmwasm/wasm/v1/code/${codeId}/contracts`; + +const getContractQueryURL = ( + baseURL: string, + address: string, + queryData: string +) => `${baseURL}/cosmwasm/wasm/v1/contract/${address}/smart/${queryData}`; + +export const getContract = async ( + baseURLs: string[], + address: string, + chainID: string +): Promise => { + for (const url of baseURLs) { + let uri = getContractURL(url, address); + uri = addChainIDParam(uri, chainID); + try { + const response = await fetch(uri); + if (response.status === 500) { + const errorBody = await response.json(); + throw new Error(errorBody?.message || 'Failed to fetch contract', { + cause: 500, + }); + } else if (response.ok) { + return response; + } + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + if (error.cause === 500) throw new Error(error.message); + continue; + } + } + throw new Error('Failed to fetch contract'); +}; + +export const queryContract = async ( + baseURLs: string[], + address: string, + queryData: string, + chainID: string +): Promise => { + for (const url of baseURLs) { + let requestURI = getContractQueryURL(url, address, queryData); + requestURI = addChainIDParam(requestURI, chainID); + try { + const response = await fetch(requestURI); + const responseJson = await response.json(); + if (response.status === 500) { + throw new Error(responseJson?.message || 'Failed to query contract', { + cause: 500, + }); + } else if ( + response.status === 400 && + responseJson?.error.includes('expected') + ) { + throw new Error(responseJson?.error || 'Failed to query contract', { + cause: 500, + }); + } else if (response.ok) { + return responseJson; + } + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + if (error.cause === 500) throw new Error(error.message); + continue; + } + } + throw new Error('Failed to query contract'); +}; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const connectWithSigner = async (urls: string[], offlineSigner: any) => { + for (const url of urls) { + try { + const signer = await SigningCosmWasmClient.connectWithSigner( + url, + offlineSigner + ); + return signer; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + console.error(`Error connecting to ${url}: ${error.message}`); + } + } + throw new Error('Unable to connect to any RPC URLs'); +}; + +export const getCodes = async (baseURLs: string[], chainID: string) => { + const requestURI = addChainIDParam(codesURL, chainID); + return axiosGetRequestWrapper(baseURLs, requestURI); +}; + +export const getContractsByCode = async ( + baseURLs: string[], + codeId: string, + chainID: string +) => { + let requestURI = contractsByCodeURL(codeId); + requestURI = addChainIDParam(requestURI, chainID); + + return axiosGetRequestWrapper(baseURLs, requestURI); +}; + +const result = { + contract: getContract, +}; + +export default result; diff --git a/frontend/src/store/features/cosmwasm/cosmwasmSlice.ts b/frontend/src/store/features/cosmwasm/cosmwasmSlice.ts new file mode 100644 index 000000000..e4015b275 --- /dev/null +++ b/frontend/src/store/features/cosmwasm/cosmwasmSlice.ts @@ -0,0 +1,527 @@ +'use client'; + +import { TxStatus } from '@/types/enums'; +import { ERR_UNKNOWN } from '@/utils/errors'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { cloneDeep } from 'lodash'; +import { setError } from '../common/commonSlice'; +import axios from 'axios'; +import { addChainIDParam, cleanURL, trackEvent } from '@/utils/util'; +import { parseTxResult } from '@/utils/signing'; +import { getCodes, getContractsByCode } from './cosmwasmService'; +import { FAILED, SUCCESS } from '@/utils/constants'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const contractInfoEmptyState = { + admin: '', + label: '', + code_id: '', + creator: '', + created: { + block_height: '', + tx_index: '', + }, + ibc_port_id: '', + extension: null, +}; + +interface Chain { + contractAddress: string; + contractInfo: ContractInfo; + txUpload: { + status: TxStatus; + error: string; + txHash: string; + txResponse: ParsedUploadTxnResponse; + }; + txInstantiate: { + status: TxStatus; + error: string; + txHash: string; + txResponse: ParsedInstatiateTxnResponse; + }; + txExecute: { + status: TxStatus; + error: string; + txHash: string; + txResponse: ParsedExecuteTxnResponse; + }; + query: { + status: TxStatus; + error: string; + queryOutput: string; + }; + codes: { + status: TxStatus; + error: string; + data: { + codes: CodeInfo[]; + }; + }; + contracts: { + status: TxStatus; + error: string; + data: { + contracts: string[]; + codeId: string; + }; + }; +} + +interface Chains { + [key: string]: Chain; +} + +interface CosmwasmState { + chains: Chains; + defaultState: Chain; +} + +const initialState: CosmwasmState = { + chains: {}, + defaultState: { + contractAddress: '', + contractInfo: contractInfoEmptyState, + txUpload: { + status: TxStatus.INIT, + error: '', + txResponse: { + code: 0, + fee: [], + transactionHash: '', + rawLog: '', + memo: '', + codeId: '', + }, + txHash: '', + }, + txInstantiate: { + status: TxStatus.INIT, + error: '', + txHash: '', + txResponse: { + code: 0, + fee: [], + transactionHash: '', + rawLog: '', + memo: '', + codeId: '', + contractAddress: '', + }, + }, + txExecute: { + status: TxStatus.INIT, + error: '', + txHash: '', + txResponse: { + code: 0, + fee: [], + transactionHash: '', + rawLog: '', + memo: '', + }, + }, + query: { + queryOutput: '', + status: TxStatus.INIT, + error: '', + }, + codes: { + data: { codes: [] }, + error: '', + status: TxStatus.INIT, + }, + contracts: { + data: { + codeId: '', + contracts: [], + }, + error: '', + status: TxStatus.INIT, + }, + }, +}; + +export const queryContractInfo = createAsyncThunk( + 'cosmwasm/query-contract', + async (data: QueryContractInfoInputs, { rejectWithValue, dispatch }) => { + try { + const response = await data.getQueryContract(data); + return { + data: response.data, + chainID: data.chainID, + }; + } catch (error: any) { + const errMsg = error?.message || 'Failed to query contract'; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + +export const getAllCodes = createAsyncThunk( + 'cosmwasm/get-codes', + async ( + data: { baseURLs: string[]; chainID: string }, + { rejectWithValue, dispatch } + ) => { + try { + const response = await getCodes(data.baseURLs, data.chainID); + return { + data: response.data, + chainID: data.chainID, + }; + } catch (error: any) { + const errMsg = error?.message || 'Failed to fetch codes'; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + +export const getAllContractsByCode = createAsyncThunk( + 'cosmwasm/get-contract-by-code', + async ( + data: { baseURLs: string[]; codeId: string; chainID: string }, + { rejectWithValue, dispatch } + ) => { + try { + const response = await getContractsByCode( + data.baseURLs, + data.codeId, + data.chainID + ); + return { + data: { + contracts: response.data, + code: data.codeId, + }, + chainID: data.chainID, + }; + } catch (error: any) { + const errMsg = error?.message || 'Failed to fetch contracts'; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + +export const executeContract = createAsyncThunk( + 'cosmwasm/execute-contract', + async (data: ExecuteContractInputs, { rejectWithValue, dispatch }) => { + try { + const response = await data.getExecutionOutput(data); + let txnUrl = + cleanURL(data.baseURLs[0]) + + '/cosmos/tx/v1beta1/txs/' + + response.txHash; + txnUrl = addChainIDParam(txnUrl, data.chainID); + const txn = await axios.get(txnUrl); + const { + code, + transactionHash, + fee = [], + memo = '', + rawLog = '', + } = parseTxResult(txn?.data?.tx_response); + if (code === 0) { + trackEvent('COSMWASM', 'EXECUTE_CONTRACT', SUCCESS); + } else { + trackEvent('COSMWASM', 'EXECUTE_CONTRACT', FAILED); + } + return { + data: { code, transactionHash, fee, memo, rawLog }, + chainID: data.chainID, + }; + } catch (error: any) { + trackEvent('COSMWASM', 'EXECUTE_CONTRACT', FAILED); + const errMsg = error?.message || 'Failed to execute contract'; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + +export const uploadCode = createAsyncThunk( + 'cosmwasm/upload-code', + async (data: UploadCodeInputs, { rejectWithValue, dispatch }) => { + try { + const response = await data.uploadContract(data); + let txnUrl = + cleanURL(data.baseURLs[0]) + + '/cosmos/tx/v1beta1/txs/' + + response.txHash; + txnUrl = addChainIDParam(txnUrl, data.chainID); + const txn = await axios.get(txnUrl); + const { + code, + transactionHash, + fee = [], + memo = '', + rawLog = '', + } = parseTxResult(txn?.data?.tx_response); + if (code === 0) { + trackEvent('COSMWASM', 'UPLOAD_CODE', SUCCESS); + } else { + trackEvent('COSMWASM', 'UPLOAD_CODE', FAILED); + } + return { + data: { + code, + transactionHash, + fee, + memo, + rawLog, + codeId: response.codeId, + }, + chainID: data.chainID, + }; + } catch (error: any) { + trackEvent('COSMWASM', 'UPLOAD_CODE', FAILED); + const errMsg = error?.message || 'Failed to execute contract'; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + +export const txInstantiateContract = createAsyncThunk( + 'cosmwasm/instantiate-contract', + async (data: InstantiateContractInputs, { rejectWithValue, dispatch }) => { + try { + const response = await data.instantiateContract(data); + let txnUrl = + cleanURL(data.baseURLs[0]) + + '/cosmos/tx/v1beta1/txs/' + + response.txHash; + txnUrl = addChainIDParam(txnUrl, data.chainID); + const txn = await axios.get(txnUrl); + const { + code, + transactionHash, + fee = [], + memo = '', + rawLog = '', + } = parseTxResult(txn?.data?.tx_response); + if (code === 0) { + trackEvent('COSMWASM', 'INSTANTIATE_CONTRACT', SUCCESS); + } else { + trackEvent('COSMWASM', 'INSTANTIATE_CONTRACT', FAILED); + } + return { + data: { + code, + transactionHash, + fee, + memo, + rawLog, + codeId: response.codeId, + contractAddress: response.contractAddress, + }, + chainID: data.chainID, + }; + } catch (error: any) { + trackEvent('COSMWASM', 'INSTANTIATE_CONTRACT', FAILED); + const errMsg = error?.message || 'Failed to execute contract'; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + +export const cosmwasmSlice = createSlice({ + name: 'cosmwasm', + initialState, + reducers: { + setContract: ( + state, + action: PayloadAction<{ + contractAddress: string; + contractInfo: ContractInfo; + chainID: string; + }> + ) => { + const chainID = action.payload.chainID; + if (!state.chains[chainID]) { + state.chains[chainID] = cloneDeep(initialState.defaultState); + } + state.chains[chainID].contractInfo = action.payload.contractInfo; + state.chains[chainID].contractAddress = action.payload.contractAddress; + }, + }, + extraReducers: (builder) => { + builder + .addCase(queryContractInfo.pending, (state, action) => { + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) { + state.chains[chainID] = cloneDeep(initialState.defaultState); + } + state.chains[chainID].query.status = TxStatus.PENDING; + state.chains[chainID].query.error = ''; + }) + .addCase(queryContractInfo.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].query.status = TxStatus.IDLE; + state.chains[chainID].query.error = ''; + state.chains[chainID].query.queryOutput = action.payload.data; + }) + .addCase(queryContractInfo.rejected, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].query.status = TxStatus.REJECTED; + state.chains[chainID].query.error = action.error.message || ERR_UNKNOWN; + state.chains[chainID].query.queryOutput = '{}'; + }); + + builder + .addCase(getAllCodes.pending, (state, action) => { + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) { + state.chains[chainID] = cloneDeep(initialState.defaultState); + } + state.chains[chainID].codes.status = TxStatus.PENDING; + state.chains[chainID].codes.error = ''; + }) + .addCase(getAllCodes.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].codes.status = TxStatus.IDLE; + state.chains[chainID].codes.error = ''; + state.chains[chainID].codes.data.codes = + action.payload.data?.code_infos || []; + }) + .addCase(getAllCodes.rejected, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].codes.status = TxStatus.REJECTED; + state.chains[chainID].codes.error = action.error.message || ERR_UNKNOWN; + state.chains[chainID].codes.data.codes = []; + }); + builder + .addCase(getAllContractsByCode.pending, (state, action) => { + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) { + state.chains[chainID] = cloneDeep(initialState.defaultState); + } + state.chains[chainID].contracts.status = TxStatus.PENDING; + state.chains[chainID].contracts.error = ''; + }) + .addCase(getAllContractsByCode.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].contracts.status = TxStatus.IDLE; + state.chains[chainID].contracts.error = ''; + state.chains[chainID].contracts.data.contracts = + action.payload.data?.contracts?.contracts || []; + state.chains[chainID].contracts.data.codeId = + action.payload.data?.code || ''; + }) + .addCase(getAllContractsByCode.rejected, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].contracts.status = TxStatus.REJECTED; + state.chains[chainID].contracts.error = + action.error.message || ERR_UNKNOWN; + state.chains[chainID].contracts.data.contracts = []; + state.chains[chainID].contracts.data.codeId = ''; + }); + builder + .addCase(executeContract.pending, (state, action) => { + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) { + state.chains[chainID] = cloneDeep(initialState.defaultState); + } + state.chains[chainID].txExecute.status = TxStatus.PENDING; + state.chains[chainID].txExecute.error = ''; + }) + .addCase(executeContract.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].txExecute.status = TxStatus.IDLE; + state.chains[chainID].txExecute.error = ''; + state.chains[chainID].txExecute.txResponse = action.payload.data; + state.chains[chainID].txExecute.txHash = + action.payload.data.transactionHash; + }) + .addCase(executeContract.rejected, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].txExecute.status = TxStatus.REJECTED; + state.chains[chainID].txExecute.error = + action.error.message || ERR_UNKNOWN; + }); + builder + .addCase(uploadCode.pending, (state, action) => { + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) { + state.chains[chainID] = cloneDeep(initialState.defaultState); + } + state.chains[chainID].txUpload.status = TxStatus.PENDING; + state.chains[chainID].txUpload.error = ''; + }) + .addCase(uploadCode.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].txUpload.status = TxStatus.IDLE; + state.chains[chainID].txUpload.error = ''; + state.chains[chainID].txUpload.txResponse = action.payload.data; + state.chains[chainID].txUpload.txHash = + action.payload.data.transactionHash; + }) + .addCase(uploadCode.rejected, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].txUpload.status = TxStatus.REJECTED; + state.chains[chainID].txUpload.error = + action.error.message || ERR_UNKNOWN; + }); + builder + .addCase(txInstantiateContract.pending, (state, action) => { + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) { + state.chains[chainID] = cloneDeep(initialState.defaultState); + } + state.chains[chainID].txInstantiate.status = TxStatus.PENDING; + state.chains[chainID].txInstantiate.error = ''; + }) + .addCase(txInstantiateContract.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].txInstantiate.status = TxStatus.IDLE; + state.chains[chainID].txInstantiate.error = ''; + state.chains[chainID].txInstantiate.txResponse = action.payload.data; + state.chains[chainID].txInstantiate.txHash = + action.payload.data.transactionHash; + }) + .addCase(txInstantiateContract.rejected, (state, action) => { + const chainID = action.meta.arg.chainID; + state.chains[chainID].txInstantiate.status = TxStatus.REJECTED; + state.chains[chainID].txInstantiate.error = + action.error.message || ERR_UNKNOWN; + }); + }, +}); + +export const { setContract } = cosmwasmSlice.actions; + +export default cosmwasmSlice.reducer; diff --git a/frontend/src/store/features/distribution/distributionService.ts b/frontend/src/store/features/distribution/distributionService.ts index 4690f4515..64ddefb77 100644 --- a/frontend/src/store/features/distribution/distributionService.ts +++ b/frontend/src/store/features/distribution/distributionService.ts @@ -1,26 +1,46 @@ 'use client'; -import Axios, { AxiosResponse } from 'axios'; -import { convertPaginationToParams, cleanURL } from '../../../utils/util'; +import { AxiosResponse } from 'axios'; +import { + addChainIDParam, + convertPaginationToParams, +} from '../../../utils/util'; +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; const delegatorTotalRewardsURL = (address: string) => `/cosmos/distribution/v1beta1/delegators/${address}/rewards`; +const withdrawAddressURL = (delegator: string) => + `/cosmos/distribution/v1beta1/delegators/${delegator}/withdraw_address`; export const fetchDelegatorTotalRewards = ( - baseURL: string, + baseURLs: string[], address: string, - pagination: KeyLimitPagination + pagination: KeyLimitPagination, + chainID: string ): Promise => { - let uri = `${cleanURL(baseURL)}${delegatorTotalRewardsURL(address)}`; + let endPoint = `${delegatorTotalRewardsURL(address)}`; + const parsed = convertPaginationToParams(pagination); if (parsed !== '') { - uri += `?${parsed}`; + endPoint += `?${parsed}`; } + endPoint = addChainIDParam(endPoint, chainID); - return Axios.get(uri); + return axiosGetRequestWrapper(baseURLs, endPoint); }; +export const fetchWithdrawAddress = ( + baseURLs: string[], + delegator: string, + chainID: string +): Promise => { + let endPoint = `${withdrawAddressURL(delegator)}`; + endPoint = addChainIDParam(endPoint, chainID); + + return axiosGetRequestWrapper(baseURLs, endPoint); +}; const result = { delegatorRewards: fetchDelegatorTotalRewards, + withdrawAddress: fetchWithdrawAddress, }; export default result; diff --git a/frontend/src/store/features/distribution/distributionSlice.ts b/frontend/src/store/features/distribution/distributionSlice.ts index e9d0be115..eb20ee4ba 100644 --- a/frontend/src/store/features/distribution/distributionSlice.ts +++ b/frontend/src/store/features/distribution/distributionSlice.ts @@ -6,6 +6,9 @@ import { ChainsMap, DelegatorTotalRewardsRequest, DistributionStoreInitialState, + TxSetWithdrawAddressInputs, + TxWithDrawValidatorCommissionAndRewardsInputs, + TxWithDrawValidatorCommissionInputs, TxWithdrawAllRewardsInputs, } from '@/types/distribution'; import { getDenomBalance } from '@/utils/denom'; @@ -15,13 +18,13 @@ import { ERR_UNKNOWN } from '@/utils/errors'; import { signAndBroadcast } from '@/utils/signing'; import { WithdrawAllRewardsMsg } from '@/txns/distribution/withDrawRewards'; import { TxStatus } from '@/types/enums'; -import { GAS_FEE } from '@/utils/constants'; +import { FAILED, GAS_FEE, SUCCESS } from '@/utils/constants'; import { NewTransaction } from '@/utils/transaction'; -import { addTransactions } from '../transactionHistory/transactionHistorySlice'; -import { getBalances } from '../bank/bankSlice'; - +import { getAuthzBalances, getBalances } from '../bank/bankSlice'; +import { trackEvent } from '@/utils/util'; const initialState: DistributionStoreInitialState = { chains: {}, + authzChains: {}, defaultState: { delegatorRewards: { list: [], @@ -34,6 +37,15 @@ const initialState: DistributionStoreInitialState = { status: TxStatus.INIT, txHash: '', }, + txWithdrawCommission: { + status: TxStatus.INIT, + errMsg: '', + }, + txSetWithdrawAddress: { + status: TxStatus.INIT, + errMsg: '', + }, + withdrawAddress: '', isTxAll: false, }, }; @@ -41,52 +53,380 @@ const initialState: DistributionStoreInitialState = { export const txWithdrawAllRewards = createAsyncThunk( 'distribution/withdraw-all-rewards', async ( - data: TxWithdrawAllRewardsInputs, + data: TxWithdrawAllRewardsInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { try { - const msgs = []; - for (let i = 0; i < data.msgs.length; i++) { - const msg = data.msgs[i]; - msgs.push(WithdrawAllRewardsMsg(msg.delegator, msg.validator)); + let msgs = []; + if (data.isAuthzMode) { + msgs = data.msgs; + } else { + for (let i = 0; i < data.msgs.length; i++) { + const msg = data.msgs[i]; + msgs.push(WithdrawAllRewardsMsg(msg.delegator, msg.validator)); + } } + const result = await signAndBroadcast( - data.chainID, - data.aminoConfig, - data.prefix, + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, msgs, GAS_FEE, '', - `${data.feeAmount}${data.denom}`, - data.rest, - data.feegranter?.length > 0 ? data.feegranter : undefined + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs ); - const tx = NewTransaction(result, msgs, data.chainID, data.address); - dispatch( - addTransactions({ - chainID: data.chainID, - address: data.cosmosAddress, - transactions: [tx], - }) + const tx = NewTransaction( + result, + msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.address ); if (result?.code === 0) { + if (data.isAuthzMode) { + dispatch( + getAuthzBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.rest, + chainID: data.basicChainInfo.chainID, + address: data.authzChainGranter, + }) + ); + + dispatch( + getAuthzDelegatorTotalRewards({ + baseURL: data.basicChainInfo.rest, + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: data.basicChainInfo.chainID, + denom: data.denom, + }) + ); + } else { + dispatch( + getBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.rest, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + }) + ); + + dispatch( + getDelegatorTotalRewards({ + baseURL: data.basicChainInfo.rest, + baseURLs: data.basicChainInfo.restURLs, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + denom: data.denom, + }) + ); + } + dispatch( + setTxAndHash({ + hash: result?.transactionHash, + tx: tx, + }) + ); + trackEvent('DISTRIBUTION', 'WITHDRAW_REWARDS', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + dispatch( + setError({ + type: 'error', + message: result?.rawLog || '', + }) + ); + trackEvent('DISTRIBUTION', 'WITHDRAW_REWARDS', FAILED); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + if (error instanceof AxiosError) { + dispatch( + setError({ + type: 'error', + message: error.message, + }) + ); + trackEvent('DISTRIBUTION', 'WITHDRAW_REWARDS', FAILED); + return rejectWithValue(error.message); + } + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const txSetWithdrawAddress = createAsyncThunk( + 'distribution/set-withdraw-address', + async ( + data: TxSetWithdrawAddressInputs | TxAuthzExecInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const msgs = data.msgs; + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + msgs, + GAS_FEE, + '', + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs + ); + const tx = NewTransaction( + result, + msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.address + ); + + if (result?.code === 0) { + if (data.isAuthzMode) { + dispatch( + getAuthzWithdrawAddress({ + baseURLs: data.basicChainInfo.restURLs, + chainID: data.basicChainInfo.chainID, + delegator: data.authzChainGranter, + }) + ); + } else { + dispatch( + getWithdrawAddress({ + baseURLs: data.basicChainInfo.restURLs, + chainID: data.basicChainInfo.chainID, + delegator: data.basicChainInfo.address, + }) + ); + } dispatch( - getBalances({ - baseURL: data.rest, - chainID: data.chainID, - address: data.address, + setTxAndHash({ + hash: result?.transactionHash, + tx: tx, }) ); + trackEvent('DISTRIBUTION', 'SET_WITHDRAW_ADDRESS', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + dispatch( + setError({ + type: 'error', + message: result?.rawLog || '', + }) + ); + trackEvent('DISTRIBUTION', 'SET_WITHDRAW_ADDRESS', FAILED); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + trackEvent('DISTRIBUTION', 'SET_WITHDRAW_ADDRESS', FAILED); + if (error instanceof AxiosError) { + dispatch( + setError({ + type: 'error', + message: error.message, + }) + ); + return rejectWithValue(error.message); + } + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const txWithdrawValidatorCommission = createAsyncThunk( + 'distribution/withdraw-validator-commission', + async ( + data: TxWithDrawValidatorCommissionInputs | TxAuthzExecInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const msgs = data.msgs; + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + msgs, + GAS_FEE, + '', + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs + ); + const tx = NewTransaction( + result, + msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.address + ); + + if (result?.code === 0) { + if (data.isAuthzMode) { + dispatch( + getAuthzBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.rest, + chainID: data.basicChainInfo.chainID, + address: data.authzChainGranter, + }) + ); + + dispatch( + getAuthzDelegatorTotalRewards({ + baseURL: data.basicChainInfo.rest, + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: data.basicChainInfo.chainID, + denom: data.denom, + }) + ); + } else { + dispatch( + getBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.rest, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + }) + ); + + dispatch( + getDelegatorTotalRewards({ + baseURL: data.basicChainInfo.rest, + baseURLs: data.basicChainInfo.restURLs, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + denom: data.denom, + }) + ); + } + + dispatch( + setTxAndHash({ + hash: result?.transactionHash, + tx: tx, + }) + ); + trackEvent('DISTRIBUTION', 'WITHDRAW_VALIDATOR_COMMISSION', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + dispatch( + setError({ + type: 'error', + message: result?.rawLog || '', + }) + ); + trackEvent('DISTRIBUTION', 'WITHDRAW_VALIDATOR_COMMISSION', FAILED); + return rejectWithValue(result?.rawLog); + } + } catch (error) { + trackEvent('DISTRIBUTION', 'WITHDRAW_VALIDATOR_COMMISSION', FAILED); + if (error instanceof AxiosError) { dispatch( - getDelegatorTotalRewards({ - baseURL: data.rest, - address: data.address, - chainID: data.chainID, - denom: data.denom, + setError({ + type: 'error', + message: error.message, }) ); + return rejectWithValue(error.message); + } + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const txWithdrawValidatorCommissionAndRewards = createAsyncThunk( + 'distribution/withdraw-validator-commission-rewards', + async ( + data: TxWithDrawValidatorCommissionAndRewardsInputs | TxAuthzExecInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const msgs = data.msgs; + + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + msgs, + GAS_FEE, + '', + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs + ); + const tx = NewTransaction( + result, + msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.address + ); + + if (result?.code === 0) { + if (data.isAuthzMode) { + dispatch( + getAuthzBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.rest, + chainID: data.basicChainInfo.chainID, + address: data.authzChainGranter, + }) + ); + + dispatch( + getAuthzDelegatorTotalRewards({ + baseURL: data.basicChainInfo.rest, + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: data.basicChainInfo.chainID, + denom: data.denom, + }) + ); + } else { + dispatch( + getBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.rest, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + }) + ); + + dispatch( + getDelegatorTotalRewards({ + baseURL: data.basicChainInfo.rest, + baseURLs: data.basicChainInfo.restURLs, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + denom: data.denom, + }) + ); + } + trackEvent( + 'DISTRIBUTION', + 'WITHDRAW_VALIDATOR_COMMISSION_REWARDS', + SUCCESS + ); dispatch( setTxAndHash({ hash: result?.transactionHash, @@ -101,9 +441,19 @@ export const txWithdrawAllRewards = createAsyncThunk( message: result?.rawLog || '', }) ); + trackEvent( + 'DISTRIBUTION', + 'WITHDRAW_VALIDATOR_COMMISSION_REWARDS', + FAILED + ); return rejectWithValue(result?.rawLog); } } catch (error) { + trackEvent( + 'DISTRIBUTION', + 'WITHDRAW_VALIDATOR_COMMISSION_REWARDS', + FAILED + ); if (error instanceof AxiosError) { dispatch( setError({ @@ -122,9 +472,56 @@ export const getDelegatorTotalRewards = createAsyncThunk( 'distribution/totalRewards', async (data: DelegatorTotalRewardsRequest) => { const response = await distService.delegatorRewards( - data.baseURL, + data.baseURLs, + data.address, + data.pagination, + data.chainID + ); + return { + data: response.data, + chainID: data.chainID, + }; + } +); + +export const getAuthzDelegatorTotalRewards = createAsyncThunk( + 'distribution/authz-totalRewards', + async (data: DelegatorTotalRewardsRequest) => { + const response = await distService.delegatorRewards( + data.baseURLs, data.address, - data.pagination + data.pagination, + data.chainID + ); + return { + data: response.data, + chainID: data.chainID, + }; + } +); + +export const getWithdrawAddress = createAsyncThunk( + 'distribution/withdraw-address', + async (data: { baseURLs: string[]; chainID: string; delegator: string }) => { + const response = await distService.withdrawAddress( + data.baseURLs, + data.delegator, + data.chainID + ); + return { + data: response.data, + chainID: data.chainID, + }; + } +); + +export const getAuthzWithdrawAddress = createAsyncThunk( + 'distribution/authz-withdraw-address', + async (data: { baseURLs: string[]; chainID: string; delegator: string }) => { + const response = await distService.withdrawAddress( + data.baseURLs, + data.delegator, + data.chainID ); return { data: response.data, @@ -157,6 +554,31 @@ export const distSlice = createSlice({ initialState.defaultState.delegatorRewards ); }, + resetState: (state) => { + /* eslint-disable-next-line */ + state = cloneDeep(initialState); + }, + resetAuthz: (state) => { + state.authzChains = {}; + }, + resetTxWithdrawRewards: (state, action) => { + const chainID = action.payload.chainID; + if (state.chains?.[chainID]?.tx) { + state.chains[chainID].tx = { + txHash: '', + status: TxStatus.INIT, + }; + } + }, + resetTxSetWithdrawAddress: (state, action) => { + const chainID = action.payload.chainID; + if (state.chains?.[chainID]?.txSetWithdrawAddress) { + state.chains[chainID].txSetWithdrawAddress = { + errMsg: '', + status: TxStatus.INIT, + }; + } + }, }, extraReducers: (builder) => { builder @@ -195,27 +617,175 @@ export const distSlice = createSlice({ action.error.message || ''; } }); + builder - .addCase(txWithdrawAllRewards.pending, (state, action) => { + .addCase(getAuthzDelegatorTotalRewards.pending, (state, action) => { + const chainID = action.meta?.arg?.chainID; + if (!state.authzChains[chainID]) + state.authzChains[chainID] = cloneDeep(initialState.defaultState); + state.authzChains[chainID].delegatorRewards.status = TxStatus.PENDING; + state.authzChains[chainID].delegatorRewards.errMsg = ''; + state.authzChains[chainID].delegatorRewards.totalRewards = 0; + state.authzChains[chainID].delegatorRewards.list = []; + state.authzChains[chainID].delegatorRewards.pagination = {}; + }) + .addCase(getAuthzDelegatorTotalRewards.fulfilled, (state, action) => { + const chainID = action.meta?.arg?.chainID; + const denom = action.meta.arg.denom; + if (state.authzChains[chainID]) { + state.authzChains[chainID].delegatorRewards.status = TxStatus.IDLE; + state.authzChains[chainID].delegatorRewards.list = + action.payload.data.rewards; + const totalRewardsList = action?.payload?.data?.total; + state.authzChains[chainID].delegatorRewards.totalRewards = + getDenomBalance(totalRewardsList, denom); + state.authzChains[chainID].delegatorRewards.pagination = + action.payload.data.pagination; + state.authzChains[chainID].delegatorRewards.errMsg = ''; + } + }) + .addCase(getAuthzDelegatorTotalRewards.rejected, (state, action) => { const chainID = action.meta?.arg?.chainID; + if (state.authzChains[chainID]) { + state.authzChains[chainID].delegatorRewards.status = + TxStatus.REJECTED; + state.authzChains[chainID].delegatorRewards.errMsg = + action.error.message || ''; + } + }); + builder + .addCase(txWithdrawAllRewards.pending, (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; const isTxAll = action.meta.arg.isTxAll; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(initialState.defaultState); state.chains[chainID].isTxAll = !!isTxAll; state.chains[chainID].tx.status = TxStatus.PENDING; state.chains[chainID].tx.txHash = ''; }) .addCase(txWithdrawAllRewards.fulfilled, (state, action) => { - const chainID = action.meta?.arg?.chainID; + const chainID = action.meta?.arg?.basicChainInfo.chainID; state.chains[chainID].tx.status = TxStatus.IDLE; state.chains[chainID].tx.txHash = action.payload.txHash; }) .addCase(txWithdrawAllRewards.rejected, (state, action) => { - const chainID = action.meta?.arg?.chainID; + const chainID = action.meta?.arg?.basicChainInfo.chainID; state.chains[chainID].tx.status = TxStatus.REJECTED; state.chains[chainID].tx.txHash = ''; }); + + builder + .addCase(txWithdrawValidatorCommission.pending, (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(initialState.defaultState); + state.chains[chainID].txWithdrawCommission.status = TxStatus.PENDING; + state.chains[chainID].txWithdrawCommission.errMsg = ''; + }) + .addCase(txWithdrawValidatorCommission.fulfilled, (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + state.chains[chainID].txWithdrawCommission.status = TxStatus.IDLE; + state.chains[chainID].txWithdrawCommission.errMsg = ''; + }) + .addCase(txWithdrawValidatorCommission.rejected, (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + state.chains[chainID].txWithdrawCommission.status = TxStatus.REJECTED; + state.chains[chainID].txWithdrawCommission.errMsg = + action.error.message || ''; + }); + + builder + .addCase( + txWithdrawValidatorCommissionAndRewards.pending, + (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(initialState.defaultState); + state.chains[chainID].txWithdrawCommission.status = TxStatus.PENDING; + state.chains[chainID].txWithdrawCommission.errMsg = ''; + } + ) + .addCase( + txWithdrawValidatorCommissionAndRewards.fulfilled, + (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + state.chains[chainID].txWithdrawCommission.status = TxStatus.IDLE; + state.chains[chainID].txWithdrawCommission.errMsg = ''; + } + ) + .addCase( + txWithdrawValidatorCommissionAndRewards.rejected, + (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + state.chains[chainID].txWithdrawCommission.status = TxStatus.REJECTED; + state.chains[chainID].txWithdrawCommission.errMsg = + action.error.message || ''; + } + ); + + builder + .addCase(txSetWithdrawAddress.pending, (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(initialState.defaultState); + state.chains[chainID].txSetWithdrawAddress.status = TxStatus.PENDING; + state.chains[chainID].txSetWithdrawAddress.errMsg = ''; + }) + .addCase(txSetWithdrawAddress.fulfilled, (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + state.chains[chainID].txSetWithdrawAddress.status = TxStatus.IDLE; + state.chains[chainID].txSetWithdrawAddress.errMsg = ''; + }) + .addCase(txSetWithdrawAddress.rejected, (state, action) => { + const chainID = action.meta?.arg?.basicChainInfo.chainID; + state.chains[chainID].txSetWithdrawAddress.status = TxStatus.REJECTED; + state.chains[chainID].txSetWithdrawAddress.errMsg = + action.error.message || ''; + }); + + builder + .addCase(getWithdrawAddress.pending, (state, action) => { + const chainID = action.meta?.arg?.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(initialState.defaultState); + state.chains[chainID].withdrawAddress = ''; + }) + .addCase(getWithdrawAddress.fulfilled, (state, action) => { + const chainID = action.meta?.arg?.chainID; + state.chains[chainID].withdrawAddress = + action.payload.data.withdraw_address; + }) + .addCase(getWithdrawAddress.rejected, (state, action) => { + const chainID = action.meta?.arg?.chainID; + state.chains[chainID].withdrawAddress = ''; + }); + + builder + .addCase(getAuthzWithdrawAddress.pending, (state, action) => { + const chainID = action.meta?.arg?.chainID; + if (!state.authzChains[chainID]) + state.authzChains[chainID] = cloneDeep(initialState.defaultState); + state.authzChains[chainID].withdrawAddress = ''; + }) + .addCase(getAuthzWithdrawAddress.fulfilled, (state, action) => { + const chainID = action.meta?.arg?.chainID; + state.authzChains[chainID].withdrawAddress = + action.payload.data.withdraw_address; + }) + .addCase(getAuthzWithdrawAddress.rejected, (state, action) => { + const chainID = action.meta?.arg?.chainID; + state.authzChains[chainID].withdrawAddress = ''; + }); }, }); -export const { resetTx, resetDefaultState, resetChainRewards } = - distSlice.actions; +export const { + resetTx, + resetDefaultState, + resetChainRewards, + resetState, + resetAuthz, + resetTxSetWithdrawAddress, + resetTxWithdrawRewards, +} = distSlice.actions; export default distSlice.reducer; diff --git a/frontend/src/store/features/feegrant/feegrantService.ts b/frontend/src/store/features/feegrant/feegrantService.ts new file mode 100644 index 000000000..cab537363 --- /dev/null +++ b/frontend/src/store/features/feegrant/feegrantService.ts @@ -0,0 +1,47 @@ +import { AxiosResponse } from 'axios'; +import { addChainIDParam, convertPaginationToParams } from '@/utils/util'; +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; + +const grantToMeURL = '/cosmos/feegrant/v1beta1/allowances/'; +const grantByMeURL = '/cosmos/feegrant/v1beta1/issued/'; + +const fetchGrantsToMe = ( + baseURLs: string[], + grantee: string, + chainID: string, + pagination?: KeyLimitPagination +): Promise> => { + let endPoint = `${grantToMeURL}${grantee}`; + + const parsed = convertPaginationToParams(pagination); + if (parsed !== '') { + endPoint += `?${parsed}`; + } + endPoint = addChainIDParam(endPoint, chainID); + + return axiosGetRequestWrapper(baseURLs, endPoint); +}; + +const fetchGrantsByMe = ( + baseURLs: string[], + grantee: string, + chainID: string, + pagination?: KeyLimitPagination +): Promise> => { + let endPoint = `${grantByMeURL}${grantee}`; + + const parsed = convertPaginationToParams(pagination); + if (parsed !== '') { + endPoint += `?${parsed}`; + } + endPoint = addChainIDParam(endPoint, chainID); + + return axiosGetRequestWrapper(baseURLs, endPoint); +}; + +const result = { + grantsByMe: fetchGrantsByMe, + grantsToMe: fetchGrantsToMe, +}; + +export default result; diff --git a/frontend/src/store/features/feegrant/feegrantSlice.ts b/frontend/src/store/features/feegrant/feegrantSlice.ts new file mode 100644 index 000000000..646712340 --- /dev/null +++ b/frontend/src/store/features/feegrant/feegrantSlice.ts @@ -0,0 +1,414 @@ +import { TxStatus } from '@/types/enums'; +import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import feegrantService from './feegrantService'; +import { cloneDeep } from 'lodash'; +import { getAddressByPrefix } from '@/utils/address'; +import { FeegrantRevokeMsg } from '@/txns/feegrant'; +import { signAndBroadcast } from '@/utils/signing'; +import { FAILED, GAS_FEE, SUCCESS } from '@/utils/constants'; +import { ERR_UNKNOWN } from '@/utils/errors'; +import { NewTransaction } from '@/utils/transaction'; +import { setTxAndHash } from '../common/commonSlice'; +import { trackEvent } from '@/utils/util'; + +interface ChainAllowance { + grantsToMe: Allowance[]; + grantsByMe: Allowance[]; + getGrantsToMeLoading: { + status: TxStatus; + errMsg: string; + }; + getGrantsByMeLoading: { + status: TxStatus; + errMsg: string; + }; + grantsToMeAddressMapping: Record; + grantsByMeAddressMapping: Record; + tx: { + status: TxStatus; + errMsg: string; + }; +} + +const defaultState: ChainAllowance = { + grantsToMe: [], + grantsByMe: [], + getGrantsByMeLoading: { + status: TxStatus.INIT, + errMsg: '', + }, + getGrantsToMeLoading: { + status: TxStatus.INIT, + errMsg: '', + }, + grantsByMeAddressMapping: {}, + grantsToMeAddressMapping: {}, + tx: { + status: TxStatus.INIT, + errMsg: '', + }, +}; + +interface FeegrantState { + feegrantModeEnabled: boolean; + feegrantAddress: string; + chains: Record; + getGrantsToMeLoading: number; + getGrantsByMeLoading: number; + addressToChainFeegrant: Record>; +} + +const initialState: FeegrantState = { + feegrantModeEnabled: false, + feegrantAddress: '', + chains: {}, + getGrantsByMeLoading: 0, + getGrantsToMeLoading: 0, + addressToChainFeegrant: {}, +}; + +export const getGrantsToMe = createAsyncThunk( + 'feegrant/grantsToMe', + async (data: GetFeegrantsInputs) => { + const response = await feegrantService.grantsToMe( + data.baseURLs, + data.address, + data.chainID, + data.pagination + ); + + return { + data: response.data, + }; + } +); + +export const getGrantsByMe = createAsyncThunk( + 'feegrant/grantsByMe', + async (data: GetFeegrantsInputs) => { + const response = await feegrantService.grantsByMe( + data.baseURLs, + data.address, + data.chainID, + data.pagination + ); + + return { + data: response.data, + }; + } +); + +export const txCreateFeegrant = createAsyncThunk( + 'feegrant/create-grant', + async ( + data: TxCreateFeegrantInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + [data.msg], + GAS_FEE, + '', + `${data.feeAmount}${data.denom}`, + data.basicChainInfo.rest, + data.feegranter?.length > 0 ? data.feegranter : undefined, + '', + data?.basicChainInfo?.restURLs + ); + + // TODO: Store txn, (This is throwing error because of BigInt in message) + // const tx = NewTransaction( + // result, + // data.msgs, + // data.basicChainInfo.chainID, + // data.basicChainInfo.address + // ); + // dispatch( + // addTransactions({ + // chainID: data.basicChainInfo.chainID, + // address: data.basicChainInfo.cosmosAddress, + // transactions: [tx], + // }) + // ); + + // dispatch( + // setTxAndHash({ + // tx: undefined, + // hash: tx.transactionHash, + // }) + // ); + + if (result?.code === 0) { + dispatch( + getGrantsByMe({ + baseURLs: data.basicChainInfo.restURLs, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + }) + ); + trackEvent('FEEGRANT', 'CREATE_FEEGRANT', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + trackEvent('FEEGRANT', 'CREATE_FEEGRANT', FAILED); + return rejectWithValue(result?.rawLog); + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('FEEGRANT', 'CREATE_FEEGRANT', FAILED); + return rejectWithValue(error?.message || ERR_UNKNOWN); + } + } +); + +export const txRevoke = createAsyncThunk( + 'feegrant/tx-revoke', + async ( + data: FeeGrantRevokeInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const msg = FeegrantRevokeMsg(data.granter, data.grantee); + + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + [msg], + GAS_FEE, + '', + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter, + '', + data?.basicChainInfo?.restURLs + ); + + if (result?.code === 0) { + const tx = NewTransaction( + result, + [msg], + data.basicChainInfo.chainID, + data.basicChainInfo.cosmosAddress + ); + dispatch( + setTxAndHash({ + tx: tx, + hash: result?.transactionHash, + }) + ); + trackEvent('FEEGRANT', 'REVOKE_FEEGRANT', SUCCESS); + dispatch( + getGrantsByMe({ + baseURLs: data.baseURLs, + address: data?.basicChainInfo.address, + chainID: data?.basicChainInfo?.chainID, + }) + ); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + trackEvent('FEEGRANT', 'REVOKE_FEEGRANT', FAILED); + return rejectWithValue(result?.rawLog); + } + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('FEEGRANT', 'REVOKE_FEEGRANT', FAILED); + console.log('error while revoke fee grant txn ', error); + return rejectWithValue(error?.message); + } + } +); + +export const feegrantSlice = createSlice({ + name: 'feegrant', + initialState, + reducers: { + enableFeegrantMode: (state, action: PayloadAction<{ address: string }>) => { + state.feegrantModeEnabled = true; + state.feegrantAddress = action.payload.address; + }, + exitFeegrantMode: (state) => { + state.feegrantModeEnabled = false; + state.feegrantAddress = ''; + }, + resetState: (state) => { + /* eslint-disable @typescript-eslint/no-unused-vars */ + state = cloneDeep(initialState); + }, + resetTxStatus: (state, action: PayloadAction<{ chainID: string }>) => { + const { chainID } = action.payload; + state.chains[chainID].tx = { + errMsg: '', + status: TxStatus.INIT, + }; + }, + }, + extraReducers: (builder) => { + builder + .addCase(getGrantsToMe.pending, (state, action) => { + state.getGrantsToMeLoading++; + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + state.chains[chainID].getGrantsToMeLoading.status = TxStatus.PENDING; + state.chains[chainID].grantsToMe = []; + state.chains[chainID].grantsToMeAddressMapping = {}; + const allAddressToFeegrant = state.addressToChainFeegrant; + const addresses = Object.keys(allAddressToFeegrant); + addresses.forEach((address) => { + allAddressToFeegrant[address][chainID] = []; + }); + + state.addressToChainFeegrant = allAddressToFeegrant; + }) + .addCase(getGrantsToMe.fulfilled, (state, action) => { + const chainID = action.meta.arg.chainID; + const allAddressToFeegrant = state.addressToChainFeegrant; + const addresses = Object.keys(allAddressToFeegrant); + addresses.forEach((address) => { + allAddressToFeegrant[address][chainID] = []; + }); + + state.addressToChainFeegrant = allAddressToFeegrant; + + state.getGrantsToMeLoading--; + + const grants = action.payload.data.allowances; + state.chains[chainID].grantsToMe = grants; + const addressMapping: Record = {}; + const allChainsAddressToGrants = state.addressToChainFeegrant; + + grants && + grants.forEach((grant: Allowance) => { + const granter = grant.granter; + const cosmosAddress = getAddressByPrefix(granter, 'cosmos'); + if (!addressMapping[granter]) addressMapping[granter] = []; + if (!allChainsAddressToGrants[cosmosAddress]) + allChainsAddressToGrants[cosmosAddress] = {}; + if (!allChainsAddressToGrants[cosmosAddress][chainID]) + allChainsAddressToGrants[cosmosAddress][chainID] = []; + allChainsAddressToGrants[cosmosAddress][chainID] = [ + ...allChainsAddressToGrants[cosmosAddress][chainID], + grant, + ]; + addressMapping[granter] = [...addressMapping[granter], grant]; + }); + state.addressToChainFeegrant = allChainsAddressToGrants; + state.chains[chainID].grantsToMeAddressMapping = addressMapping; + state.chains[chainID].getGrantsToMeLoading = { + status: TxStatus.IDLE, + errMsg: '', + }; + }) + .addCase(getGrantsToMe.rejected, (state, action) => { + state.getGrantsToMeLoading--; + const chainID = action.meta.arg.chainID; + + state.chains[chainID].getGrantsToMeLoading = { + status: TxStatus.REJECTED, + errMsg: + action.error.message || + 'An error occurred while fetching feegrants to me', + }; + }); + builder + .addCase(getGrantsByMe.pending, (state, action) => { + state.getGrantsByMeLoading++; + const chainID = action.meta.arg.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + state.chains[chainID].getGrantsByMeLoading.status = TxStatus.PENDING; + state.chains[chainID].grantsByMe = []; + state.chains[chainID].grantsByMeAddressMapping = {}; + }) + .addCase(getGrantsByMe.fulfilled, (state, action) => { + state.getGrantsByMeLoading--; + const chainID = action.meta.arg.chainID; + const grants = action.payload.data.allowances; + state.chains[chainID].grantsByMe = grants; + const addressMapping: Record = {}; + grants && + grants.forEach((grant: Allowance) => { + const granter = grant.grantee; + if (!addressMapping[granter]) addressMapping[granter] = []; + addressMapping[granter] = [...addressMapping[granter], grant]; + }); + state.chains[chainID].grantsByMeAddressMapping = addressMapping; + state.chains[chainID].getGrantsByMeLoading = { + status: TxStatus.IDLE, + errMsg: '', + }; + }) + .addCase(getGrantsByMe.rejected, (state, action) => { + state.getGrantsByMeLoading--; + const chainID = action.meta.arg.chainID; + + state.chains[chainID].getGrantsByMeLoading = { + status: TxStatus.REJECTED, + errMsg: + action.error.message || + 'An error occurred while fetching feegrants by me', + }; + }); + builder + .addCase(txCreateFeegrant.pending, (state, action) => { + const { chainID } = action.meta.arg.basicChainInfo; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + state.chains[chainID].tx.status = TxStatus.PENDING; + state.chains[chainID].tx.errMsg = ''; + }) + .addCase(txCreateFeegrant.fulfilled, (state, action) => { + const { chainID } = action.meta.arg.basicChainInfo; + const { txHash } = action.payload; + state.chains[chainID].tx.status = TxStatus.IDLE; + state.chains[chainID].tx.errMsg = ''; + action.meta.arg.onTxComplete?.({ + isTxSuccess: true, + txHash: txHash, + }); + }) + .addCase(txCreateFeegrant.rejected, (state, action) => { + const { chainID } = action.meta.arg.basicChainInfo; + state.chains[chainID].tx.status = TxStatus.REJECTED; + state.chains[chainID].tx.errMsg = + typeof action.payload === 'string' ? action.payload : ''; + action.meta.arg.onTxComplete?.({ + isTxSuccess: false, + error: + typeof action.payload === 'string' ? action.payload : ERR_UNKNOWN, + }); + }); + builder + .addCase(txRevoke.pending, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(defaultState); + state.chains[chainID].tx.status = TxStatus.PENDING; + state.chains[chainID].tx.errMsg = ''; + }) + .addCase(txRevoke.fulfilled, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + state.chains[chainID].tx.status = TxStatus.IDLE; + }) + .addCase(txRevoke.rejected, (state, action) => { + const chainID = action.meta.arg.basicChainInfo.chainID; + state.chains[chainID].tx.status = TxStatus.REJECTED; + state.chains[chainID].tx.errMsg = action.error.message || 'rejected'; + }); + }, +}); + +export const { + enableFeegrantMode, + exitFeegrantMode, + resetState, + resetTxStatus, +} = feegrantSlice.actions; + +export default feegrantSlice.reducer; diff --git a/frontend/src/store/features/gov/govService.ts b/frontend/src/store/features/gov/govService.ts index 240bf7f23..3114b2b27 100644 --- a/frontend/src/store/features/gov/govService.ts +++ b/frontend/src/store/features/gov/govService.ts @@ -1,75 +1,111 @@ -import Axios, { AxiosResponse } from 'axios'; -import { convertPaginationToParams, cleanURL } from '../../../utils/util'; +import { AxiosResponse } from 'axios'; +import { + addChainIDParam, + convertPaginationToParams, +} from '../../../utils/util'; import { GetProposalsInVotingResponse, GovProposal, ProposalVote, } from '@/types/gov'; +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; -const proposalsURL = '/cosmos/gov/v1beta1/proposals'; -const proposalTallyURL = (id: number): string => - `/cosmos/gov/v1beta1/proposals/${id}/tally`; +const proposalsURL = (govV1: boolean) => + govV1 ? '/cosmos/gov/v1/proposals' : '/cosmos/gov/v1beta1/proposals'; +const proposalTallyURL = (id: number, govV1: boolean): string => + govV1 + ? `/cosmos/gov/v1/proposals/${id}/tally` + : `/cosmos/gov/v1beta1/proposals/${id}/tally`; -const voterVoteURL = (id: number, voter: string): string => - `/cosmos/gov/v1beta1/proposals/${id}/votes/${voter}`; +const voterVoteURL = (id: number, voter: string, govV1: boolean): string => + govV1 + ? `/cosmos/gov/v1/proposals/${id}/votes/${voter}` + : `/cosmos/gov/v1beta1/proposals/${id}/votes/${voter}`; const depositParamsURL = `/cosmos/gov/v1beta1/params/deposit`; const govTallyParamsURL = `/cosmos/gov/v1beta1/params/tallying`; const fetchProposals = ( - baseURL: string, + baseURLs: string[], key: string | undefined, limit: number | undefined, - status: number + status: number, + govV1: boolean, + chainID: string ): Promise> => { - let uri = `${cleanURL(baseURL)}${proposalsURL}`; - uri += `?proposal_status=${status}`; + let endPoint = `${proposalsURL(govV1)}`; + + endPoint += `?proposal_status=${status}`; const params = convertPaginationToParams({ key: key, limit: limit, }); - if (params !== '') uri += `&${params}`; - return Axios.get(uri); + if (params !== '') endPoint += `&${params}`; + endPoint = addChainIDParam(endPoint, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); }; const fetchProposalTally = ( - baseURL: string, - proposalId: number + baseURLs: string[], + proposalId: number, + govV1: boolean, + chainID: string ): Promise => { - const uri = `${cleanURL(baseURL)}${proposalTallyURL(proposalId)}`; - return Axios.get(uri); + let endPoint = `${proposalTallyURL(proposalId, govV1)}`; + endPoint = addChainIDParam(endPoint, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); }; const fetchVoterVote = ( - baseURL: string, + baseURLs: string[], proposalId: number, voter: string, key: string | undefined, - limit: number | undefined + limit: number | undefined, + govV1: boolean, + chainID: string ): Promise> => { - let uri = `${cleanURL(baseURL)}${voterVoteURL(proposalId, voter)}`; + let endPoint = `${voterVoteURL(proposalId, voter, govV1)}`; const params = convertPaginationToParams({ key: key, limit: limit, }); - if (params !== '') uri += `?${params}`; - return Axios.get(uri); + if (params !== '') endPoint += `?${params}`; + endPoint = addChainIDParam(endPoint, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); }; const fetchProposal = ( - baseURL: string, - proposalId: number -): Promise> => - Axios.get(`${cleanURL(baseURL)}${proposalsURL}/${proposalId}`); + baseURLs: string[], + proposalId: number, + govV1: boolean, + chainID: string +): Promise> => { + let endPoint = `${proposalsURL(govV1)}/${proposalId}`; + endPoint = addChainIDParam(endPoint, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); +}; -const fetchDepositParams = (baseURL: string): Promise => - Axios.get(`${cleanURL(baseURL)}${depositParamsURL}`); +const fetchDepositParams = ( + baseURLs: string[], + chainID: string +): Promise => { + let endPoint = `${depositParamsURL}`; + endPoint = addChainIDParam(endPoint, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); +}; -const fetchGovTallyParams = (baseURL: string): Promise => - Axios.get(`${cleanURL(baseURL)}${govTallyParamsURL}`); +const fetchGovTallyParams = ( + baseURLs: string[], + chainID: string +): Promise => { + let endPoint = `${govTallyParamsURL}`; + endPoint = addChainIDParam(endPoint, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); +}; const result = { proposals: fetchProposals, diff --git a/frontend/src/store/features/gov/govSlice.ts b/frontend/src/store/features/gov/govSlice.ts index b81abad5d..88132847e 100644 --- a/frontend/src/store/features/gov/govSlice.ts +++ b/frontend/src/store/features/gov/govSlice.ts @@ -2,7 +2,7 @@ import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import govService from './govService'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, get } from 'lodash'; import { AxiosError } from 'axios'; import { ERR_UNKNOWN } from '@/utils/errors'; import { TxStatus } from '@/types/enums'; @@ -21,11 +21,12 @@ import { TxDepositInputs, GovParamsResponse, } from '@/types/gov'; -import { GAS_FEE, PROPOSAL_STATUS_VOTING_PERIOD } from '@/utils/constants'; +import { FAILED, GAS_FEE, PROPOSAL_STATUS_VOTING_PERIOD, SUCCESS } from '@/utils/constants'; import { signAndBroadcast } from '@/utils/signing'; import { setError, setTxAndHash } from '../common/commonSlice'; import { GovDepositMsg, GovVoteMsg } from '@/txns/gov'; import { NewTransaction } from '@/utils/transaction'; +import { trackEvent } from '@/utils/util'; const PROPSAL_STATUS_DEPOSIT = 1; const PROPOSAL_STATUS_ACTIVE = 2; @@ -168,7 +169,12 @@ export const getProposal = createAsyncThunk( 'gov/proposal-info', async (data: GetProposalInputs, { rejectWithValue }) => { try { - const response = await govService.proposal(data.baseURL, data.proposalId); + const response = await govService.proposal( + data.baseURLs, + data.proposalId, + data.govV1, + data.chainID + ); return { chainID: data.chainID, data: response.data, @@ -185,7 +191,10 @@ export const getGovTallyParams = createAsyncThunk( 'gov/tally-params', async (data: GetDepositParamsInputs, { rejectWithValue }) => { try { - const response = await govService.govTallyParams(data.baseURL); + const response = await govService.govTallyParams( + data.baseURLs, + data.chainID + ); return { chainID: data.chainID, data: response.data, @@ -203,16 +212,19 @@ export const getProposalsInDeposit = createAsyncThunk( async (data: GetProposalsInDepositInputs, { rejectWithValue, dispatch }) => { try { const response = await govService.proposals( - data.baseURL, + data.baseURLs, data?.key, data?.limit, - PROPSAL_STATUS_DEPOSIT + PROPSAL_STATUS_DEPOSIT, + data.govV1, + data.chainID ); if (response?.data?.proposals?.length && data?.chainID?.length) { dispatch( getDepositParams({ baseURL: data.baseURL, + baseURLs: data.baseURLs, chainID: data.chainID, }) ); @@ -234,7 +246,10 @@ export const getDepositParams = createAsyncThunk( 'gov/deposit-params', async (data: GetDepositParamsInputs, { rejectWithValue }) => { try { - const response = await govService.depositParams(data.baseURL); + const response = await govService.depositParams( + data.baseURLs, + data.chainID + ); return { chainID: data.chainID, @@ -253,31 +268,43 @@ export const getProposalsInVoting = createAsyncThunk( async (data: GetProposalsInVotingInputs, { rejectWithValue, dispatch }) => { try { const response = await govService.proposals( - data.baseURL, + data.baseURLs, data.key, data.limit, - PROPOSAL_STATUS_ACTIVE + PROPOSAL_STATUS_ACTIVE, + data.govV1, + data.chainID ); const { data: responseData } = response || {}; const proposals = responseData?.proposals || []; proposals.forEach((proposal) => { - const proposalId = Number(proposal.proposal_id); - dispatch( - getProposalTally({ - baseURL: data?.baseURL, - proposalId, - chainID: data?.chainID, - }) - ); - dispatch( - getVotes({ - baseURL: data?.baseURL, - proposalId, - voter: data?.voter, - chainID: data?.chainID, - }) + const proposalId = Number( + get(proposal, 'proposal_id', get(proposal, 'id', '')) ); + if (!isNaN(proposalId) && proposalId) { + dispatch( + getProposalTally({ + baseURL: data?.baseURL, + baseURLs: data?.baseURLs, + proposalId, + chainID: data?.chainID, + govV1: data.govV1, + }) + ); + } + // if (data?.voter?.length) { + // dispatch( + // getVotes({ + // baseURL: data?.baseURL, + // baseURLs: data?.baseURLs, + // proposalId, + // voter: data?.voter, + // chainID: data?.chainID, + // govV1: data.govV1, + // }) + // ); + // } }); return { @@ -297,11 +324,13 @@ export const getVotes = createAsyncThunk( async (data: GetVotesInputs, { rejectWithValue }) => { try { const response = await govService.votes( - data.baseURL, + data.baseURLs, data.proposalId, data.voter, data.key, - data.limit + data.limit, + data.govV1, + data.chainID ); response.data.vote.proposal_id = data.proposalId; @@ -309,11 +338,15 @@ export const getVotes = createAsyncThunk( return { chainID: data.chainID, data: response.data, + proposalId: data.proposalId, }; } catch (error) { if (error instanceof AxiosError) return rejectWithValue({ message: error.message }); - return rejectWithValue({ message: ERR_UNKNOWN }); + return rejectWithValue({ + message: ERR_UNKNOWN, + proposalId: data.proposalId, + }); } } ); @@ -322,7 +355,12 @@ export const getProposalTally = createAsyncThunk( 'gov/proposal-tally', async (data: GetProposalTallyInputs, { rejectWithValue }) => { try { - const response = await govService.tally(data.baseURL, data.proposalId); + const response = await govService.tally( + data.baseURLs, + data.proposalId, + data.govV1, + data.chainID + ); response.data.tally.proposal_id = data.proposalId; @@ -341,24 +379,36 @@ export const getProposalTally = createAsyncThunk( export const txVote = createAsyncThunk( 'gov/tx-vote', async ( - data: TxVoteInputs, + data: TxVoteInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { try { - const msg = GovVoteMsg(data.proposalId, data.voter, data.option); + let msgs: Msg[]; + if (data.isAuthzMode) msgs = data.msgs; + else msgs = [GovVoteMsg(data.proposalId, data.voter, data.option)]; + const result = await signAndBroadcast( - data.chainID, - data.aminoConfig, - data.prefix, - [msg], + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + msgs, GAS_FEE, - data?.justification || '', - `${data.feeAmount}${data.denom}`, - data.rest - // data.feegranter?.length > 0 ? data.feegranter : undefined + data.isAuthzMode ? data.memo : data?.justification || '', + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs ); - const tx = NewTransaction(result, [msg], data.chainID, data.voter); + const tx = NewTransaction( + result, + msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.address + ); if (result?.code === 0) { dispatch( @@ -368,8 +418,10 @@ export const txVote = createAsyncThunk( }) ); + trackEvent('GOV', 'VOTE', SUCCESS); return fulfillWithValue({ txHash: result?.transactionHash }); } else { + trackEvent('GOV', 'VOTE', FAILED); dispatch( setError({ type: 'error', @@ -378,23 +430,17 @@ export const txVote = createAsyncThunk( ); return rejectWithValue(result?.rawLog); } - } catch (error) { - if (error instanceof AxiosError) { - dispatch( - setError({ - type: 'error', - message: error.message, - }) - ); - return rejectWithValue(error.response); - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + trackEvent('GOV', 'VOTE', FAILED); + const errMessage = error?.response?.data?.error || error?.message; dispatch( setError({ type: 'error', - message: ERR_UNKNOWN, + message: errMessage || ERR_UNKNOWN, }) ); - return rejectWithValue(ERR_UNKNOWN); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -402,55 +448,68 @@ export const txVote = createAsyncThunk( export const txDeposit = createAsyncThunk( 'gov/tx-deposit', async ( - data: TxDepositInputs, + data: TxDepositInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { try { - const msg = GovDepositMsg( - data.proposalId, - data.depositer, - data.amount, - data.denom - ); + let msgs: Msg[]; + if (data.isAuthzMode) { + msgs = data.msgs; + } else { + msgs = [ + GovDepositMsg( + data.proposalId, + data.depositer, + data.amount, + data.denom + ), + ]; + } + const result = await signAndBroadcast( - data.chainID, - data.aminoConfig, - data.prefix, - [msg], - 860000, - data?.justification || '', - `${data.feeAmount}${data.denom}`, - data.rest, - data.feegranter?.length > 0 ? data.feegranter : undefined + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + msgs, + GAS_FEE, + data.isAuthzMode ? data.memo : data.justification || '', + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data.feegranter, + '', + data?.basicChainInfo?.restURLs ); const { code, transactionHash, rawLog } = result || {}; - const tx = NewTransaction(result, [msg], data.chainID, data.depositer); + const tx = NewTransaction( + result, + msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.address + ); if (code === 0) { + trackEvent('GOV', 'DEPOSIT', SUCCESS); dispatch(setTxAndHash({ tx: tx, hash: transactionHash })); return fulfillWithValue({ txHash: transactionHash }); } else { + trackEvent('GOV', 'DEPOSIT', FAILED); dispatch(setError({ type: 'error', message: rawLog || '' })); return rejectWithValue(rawLog); } - } catch (error) { - if (error instanceof AxiosError) { - dispatch( - setError({ - type: 'error', - message: error.message, - }) - ); - return rejectWithValue(error.response); - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + trackEvent('GOV', 'DEPOSIT', FAILED); + const errMessage = error?.response?.data?.error || error?.message; dispatch( setError({ type: 'error', - message: ERR_UNKNOWN, + message: errMessage || ERR_UNKNOWN, }) ); - return rejectWithValue(ERR_UNKNOWN); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -618,9 +677,31 @@ export const govSlice = createSlice({ }) .addCase(getVotes.rejected, (state, action) => { const chainID = action.meta?.arg?.chainID; - const payload = action.payload as { message: string }; + const payload = action.payload as { + message: string; + proposalId: number; + }; state.chains[chainID].votes.status = TxStatus.REJECTED; state.chains[chainID].votes.errMsg = payload.message || ''; + const result: VotesData = { + status: TxStatus.IDLE, + errMsg: '', + proposals: state.chains[chainID].votes?.proposals || {}, + }; + + const proposalId = Number(payload.proposalId).toString(); + + // Initialize the proposal with a valid ProposalVote object + result.proposals[proposalId] = { + vote: { + proposal_id: Number(proposalId), + voter: '', + option: '', + options: [], + }, + }; + + state.chains[chainID].votes = result; }); // tally @@ -723,34 +804,36 @@ export const govSlice = createSlice({ // tx-vote builder .addCase(txVote.pending, (state, action) => { - const chainID = action.meta?.arg?.chainID; + const chainID = action.meta?.arg?.basicChainInfo.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(initialState.defaultState); state.chains[chainID].tx.status = TxStatus.PENDING; state.chains[chainID].tx.txHash = ''; }) .addCase(txVote.fulfilled, (state, action) => { - const chainID = action.meta?.arg?.chainID; + const chainID = action.meta?.arg?.basicChainInfo.chainID; state.chains[chainID].tx.status = TxStatus.IDLE; state.chains[chainID].tx.txHash = action.payload.txHash; }) .addCase(txVote.rejected, (state, action) => { - const chainID = action.meta?.arg?.chainID; + const chainID = action.meta?.arg?.basicChainInfo.chainID; state.chains[chainID].tx.status = TxStatus.REJECTED; state.chains[chainID].tx.txHash = ''; }); builder .addCase(txDeposit.pending, (state, action) => { - const chainID = action.meta?.arg?.chainID; + const chainID = action.meta?.arg?.basicChainInfo.chainID; state.chains[chainID].tx.status = TxStatus.PENDING; state.chains[chainID].tx.txHash = ''; }) .addCase(txDeposit.fulfilled, (state, action) => { - const chainID = action.meta?.arg?.chainID; + const chainID = action.meta?.arg?.basicChainInfo.chainID; state.chains[chainID].tx.status = TxStatus.IDLE; state.chains[chainID].tx.txHash = action.payload.txHash; }) .addCase(txDeposit.rejected, (state, action) => { - const chainID = action.meta?.arg?.chainID; + const chainID = action.meta?.arg?.basicChainInfo.chainID; state.chains[chainID].tx.status = TxStatus.REJECTED; state.chains[chainID].tx.txHash = ''; }); diff --git a/frontend/src/store/features/ibc/ibcSlice.ts b/frontend/src/store/features/ibc/ibcSlice.ts index 6703700df..bb3c819fa 100644 --- a/frontend/src/store/features/ibc/ibcSlice.ts +++ b/frontend/src/store/features/ibc/ibcSlice.ts @@ -6,14 +6,13 @@ import { trackIBCTx, txIBCTransfer } from './ibcService'; import { TxStatus } from '@/types/enums'; import axios from 'axios'; import { parseTxResult } from '@/utils/signing'; -import { - addTransactions, - updateIBCTransaction, -} from '../transactionHistory/transactionHistorySlice'; -import { NewTransaction } from '@/utils/transaction'; +import { NewIBCTransaction, NewTransaction } from '@/utils/transaction'; import { setError, setTxAndHash } from '../common/commonSlice'; import { capitalize } from 'lodash'; import { getBalances } from '../bank/bankSlice'; +import { addIBCTransaction, updateIBCTransactionStatus } from '../recent-transactions/recentTransactionsSlice'; +import { FAILED, SUCCESS } from '@/utils/constants'; +import { trackEvent } from '@/utils/util'; export interface IBCState { txStatus: TxStatus; @@ -25,13 +24,13 @@ const initialState: IBCState = { txStatus: TxStatus.INIT, chains: {} }; export const trackTx = createAsyncThunk( 'ibc/trackTx', async ( - data: { chainID: string; txHash: string; cosmosAddress: string }, + data: { chainID: string; txHash: string }, { rejectWithValue, dispatch } ) => { const onDestChainTxSuccess = (chainID: string, txHash: string) => { dispatch(removeFromPending({ chainID, txHash })); dispatch( - updateIBCTransaction({ chainID, address: data.cosmosAddress, txHash }) + updateIBCTransactionStatus({ txHash }) ); }; try { @@ -57,7 +56,7 @@ export const txTransfer = createAsyncThunk( const onSourceChainTxSuccess = async (chainID: string, txHash: string) => { dispatch(resetTxStatus()); const response = await axios.get( - data.rest + '/cosmos/tx/v1beta1/txs/' + txHash + data.rest + '/cosmos/tx/v1beta1/txs/' + txHash+ `?chain=${data.sourceChainID}` ); const msgs = response?.data?.tx?.body?.messages || []; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -76,23 +75,35 @@ export const txTransfer = createAsyncThunk( true, result.code === 0 ); + const ibcTx = NewIBCTransaction( + result, + formattedMsgs, + chainID, + data.from, + true, + result.code === 0 + ); + if(result.code === 0) { + trackEvent('TRANSFER', 'IBC_TRANSFER', SUCCESS); + } else { + trackEvent('TRANSFER', 'IBC_TRANSFER', FAILED); + } dispatch( setTxAndHash({ hash: txHash, tx, }) ); - dispatch( - addTransactions({ - chainID: chainID, - address: data.cosmosAddress, - transactions: [tx], - }) - ); + dispatch(addIBCTransaction(ibcTx)); dispatch(addToPending({ chainID, txHash })); dispatch( - getBalances({ baseURL: data.rest, address: data.from, chainID }) + getBalances({ + baseURL: data.rest, + address: data.from, + chainID, + baseURLs: data.restURLs, + }) ); return result; }; @@ -104,8 +115,8 @@ export const txTransfer = createAsyncThunk( ) => { dispatch(removeFromPending({ chainID, txHash })); dispatch( - updateIBCTransaction({ chainID, address: data.cosmosAddress, txHash }) - ); + updateIBCTransactionStatus({ txHash }) + ); dispatch( setError({ type: 'success', @@ -144,6 +155,7 @@ export const txTransfer = createAsyncThunk( return response; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ } catch (err: any) { + trackEvent('TRANSFER', 'IBC_TRANSFER', FAILED); dispatch( setError({ type: 'error', diff --git a/frontend/src/store/features/multiops/multiopsSlice.ts b/frontend/src/store/features/multiops/multiopsSlice.ts new file mode 100644 index 000000000..17c4f149f --- /dev/null +++ b/frontend/src/store/features/multiops/multiopsSlice.ts @@ -0,0 +1,129 @@ +'use client'; + +import { TxStatus } from '@/types/enums'; +import { signAndBroadcast } from '@/utils/signing'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { setError, setTxAndHash } from '../common/commonSlice'; +import { NewTransaction } from '@/utils/transaction'; +import { ERR_UNKNOWN } from '@/utils/errors'; +import { getBalances } from '../bank/bankSlice'; +import { trackEvent } from '@/utils/util'; +import { FAILED, SUCCESS } from '@/utils/constants'; + +interface MultiopsState { + tx: { + status: TxStatus; + error: string; + }; +} + +const initialState: MultiopsState = { + tx: { + status: TxStatus.INIT, + error: '', + }, +}; + +export const txExecuteMultiMsg = createAsyncThunk( + 'multiops/tx-execute', + async ( + data: TxExecuteMultiMsgInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + data.msgs, + data.gas, + data.memo, + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + data.basicChainInfo.rpc, + data.basicChainInfo.restURLs + ); + + const tx = NewTransaction( + result, + data.msgs, + data.basicChainInfo.chainID, + data.basicChainInfo.address + ); + + if (result?.code === 0) { + dispatch( + setTxAndHash({ + tx: tx, + hash: result?.transactionHash, + }) + ); + + trackEvent('MULTIOPS', 'TXN_BUILDER', SUCCESS); + + dispatch( + getBalances({ + address: data.address, + baseURL: data.basicChainInfo.rest, + baseURLs: data.basicChainInfo.restURLs, + chainID: data.basicChainInfo.chainID, + }) + ); + + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + trackEvent('MULTIOPS', 'TXN_BUILDER', FAILED); + dispatch( + setError({ + type: 'error', + message: result?.rawLog || '', + }) + ); + return rejectWithValue(result?.rawLog); + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('MULTIOPS', 'TXN_BUILDER', FAILED); + const errMsg = error?.message || ERR_UNKNOWN; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + +export const multiopsSlice = createSlice({ + name: 'multiops', + initialState, + reducers: { + resetTx: (state) => { + state.tx.status = TxStatus.INIT; + state.tx.error = ''; + }, + }, + extraReducers: (builder) => { + builder + .addCase(txExecuteMultiMsg.pending, (state) => { + state.tx.status = TxStatus.PENDING; + state.tx.error = ''; + }) + .addCase(txExecuteMultiMsg.fulfilled, (state) => { + state.tx.status = TxStatus.IDLE; + state.tx.error = ''; + }) + .addCase(txExecuteMultiMsg.rejected, (state, action) => { + state.tx.status = TxStatus.REJECTED; + state.tx.error = action.error.message || ERR_UNKNOWN; + }); + }, +}); + +export const { resetTx } = multiopsSlice.actions; +export default multiopsSlice.reducer; diff --git a/frontend/src/store/features/multisig/multisigService.ts b/frontend/src/store/features/multisig/multisigService.ts index f10e4ce46..34092f03d 100644 --- a/frontend/src/store/features/multisig/multisigService.ts +++ b/frontend/src/store/features/multisig/multisigService.ts @@ -11,6 +11,8 @@ import { VerifyUserPayload, } from '@/types/multisig'; import { API_URL } from '@/utils/constants'; +import { getAddressByPrefix } from '@/utils/address'; +import { SigningStargateClient } from '@cosmjs/stargate'; const BASE_URL: string = cleanURL(API_URL); @@ -19,6 +21,8 @@ const GET_ACCOUNTS_URL = '/multisig/accounts'; const SIGNATURE_PARAMS_STRING = (queryParams: QueryParams): string => `?address=${encodeURIComponent( queryParams.address + )}&cosmos_address=${encodeURIComponent( + getAddressByPrefix(queryParams.address, 'cosmos') )}&signature=${encodeURIComponent(queryParams.signature)}`; const CREATE_ACCOUNT = (queryParams: QueryParams): string => @@ -78,7 +82,7 @@ const updateTx = ( ): Promise => Axios.post( `${BASE_URL}/multisig/${address}/tx/${txId}` + - SIGNATURE_PARAMS_STRING(queryParams), + SIGNATURE_PARAMS_STRING(queryParams), payload ); @@ -105,7 +109,7 @@ export const deleteTx = ( ): Promise => Axios.delete( `${BASE_URL}/multisig/${address}/tx/${txId}` + - SIGNATURE_PARAMS_STRING(queryParams) + SIGNATURE_PARAMS_STRING(queryParams) ); export const deleteMultisig = ( @@ -113,10 +117,23 @@ export const deleteMultisig = ( address: string ): Promise => Axios.delete( - `${BASE_URL}/multisig/${address}` + - SIGNATURE_PARAMS_STRING(queryParams) + `${BASE_URL}/multisig/${address}` + SIGNATURE_PARAMS_STRING(queryParams) ); +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const getStargateClient = async (urls: string[]) => { + for (const url of urls) { + try { + const client = await SigningStargateClient.connect(url); + return client; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + console.error(`Error connecting to ${url}: ${error.message}`); + } + } + throw new Error('Unable to connect to any RPC URLs'); +}; + export default { createAccount, getAccounts, @@ -129,4 +146,5 @@ export default { deleteMultisig, verifyUser, getAccountAllMultisigTxns, + getStargateClient, }; diff --git a/frontend/src/store/features/multisig/multisigSlice.ts b/frontend/src/store/features/multisig/multisigSlice.ts index b610281d7..bcd9279f3 100644 --- a/frontend/src/store/features/multisig/multisigSlice.ts +++ b/frontend/src/store/features/multisig/multisigSlice.ts @@ -1,29 +1,53 @@ 'use client'; -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import multisigService from './multisigService'; import { AxiosError } from 'axios'; -import { ERR_UNKNOWN, WALLET_REQUEST_ERROR } from '../../../utils/errors'; import { + ERR_UNKNOWN, + FAILED_TO_BROADCAST_ERROR, + NETWORK_ERROR, + NOT_MULTISIG_ACCOUNT_ERROR, + NOT_MULTISIG_MEMBER_ERROR, + WALLET_REQUEST_ERROR, +} from '../../../utils/errors'; +import { + COSMOS_CHAIN_ID, + FAILED, MAX_SALT_VALUE, MIN_SALT_VALUE, + MULTISIG_LEGACY_AMINO_PUBKEY_TYPE, OFFCHAIN_VERIFICATION_MESSAGE, + SUCCESS, } from '@/utils/constants'; -import { TxStatus } from '@/types/enums'; +import { MultisigTxStatus, TxStatus } from '@/types/enums'; import bankService from '@/store/features/bank/bankService'; import { CreateAccountPayload, CreateTxnInputs, DeleteMultisigInputs, DeleteTxnInputs, - GetMultisigBalanceInputs, + GetMultisigBalancesInputs, GetTxnsInputs, + ImportMultisigAccountRes, + MultisigAddressPubkey, MultisigState, QueryParams, SignTxInputs, + Txn, UpdateTxnInputs, } from '@/types/multisig'; -import { getRandomNumber } from '@/utils/util'; +import { + getRandomNumber, + isMultisigAccountMember, + isNetworkError, + trackEvent, +} from '@/utils/util'; +import authService from './../auth/authService'; +import { get } from 'lodash'; +import { getAuthToken } from '@/utils/localStorage'; +import multisigSigning from '@/app/(routes)/multisig/utils/multisigSigning'; +import { setError } from '../common/commonSlice'; const initialState: MultisigState = { createMultisigAccountRes: { @@ -60,10 +84,7 @@ const initialState: MultisigState = { error: '', }, balance: { - balance: { - amount: '', - denom: '', - }, + balance: [], status: TxStatus.INIT, error: '', }, @@ -79,7 +100,24 @@ const initialState: MultisigState = { status: TxStatus.INIT, error: '', }, + signTransactionRes: { + status: TxStatus.INIT, + error: '', + }, + broadcastTxnRes: { + status: TxStatus.INIT, + error: '', + txHash: '', + txResponse: { + code: 0, + fee: [], + transactionHash: '', + rawLog: '', + memo: '', + }, + }, txns: { + Count: [], list: [], status: TxStatus.INIT, error: '', @@ -88,6 +126,22 @@ const initialState: MultisigState = { status: TxStatus.INIT, error: '', }, + // multisigAccountData is used to store the multisigAccount details when user imports an + // on chain multisig account + multisigAccountData: { + account: { + account: { + '@type': '', + account_number: '', + address: '', + pub_key: {}, + sequence: '', + }, + }, + status: TxStatus.INIT, + error: '', + }, + verifyDialogOpen: false, }; declare let window: WalletWindow; @@ -103,8 +157,10 @@ export const createAccount = createAsyncThunk( data.queryParams, data.data ); + trackEvent('MULTISIG', 'CREATE_MULTISIG', SUCCESS); return response.data; } catch (error) { + trackEvent('MULTISIG', 'CREATE_MULTISIG', FAILED); if (error instanceof AxiosError) return rejectWithValue({ message: error?.response?.data?.message || ERR_UNKNOWN, @@ -168,8 +224,10 @@ export const deleteMultisig = createAsyncThunk( data.queryParams, data.data.address ); + trackEvent('MULTISIG', 'DELETE_MULTISIG', SUCCESS); return response.data; } catch (error) { + trackEvent('MULTISIG', 'DELETE_MULTISIG', FAILED); if (error instanceof AxiosError) return rejectWithValue({ message: error.message }); return rejectWithValue({ message: ERR_UNKNOWN }); @@ -186,8 +244,10 @@ export const deleteTxn = createAsyncThunk( data.data.address, data.data.id ); + trackEvent('MULTISIG', 'DELETE_TXN', SUCCESS); return response.data; } catch (error) { + trackEvent('MULTISIG', 'DELETE_TXN', FAILED); if (error instanceof AxiosError) return rejectWithValue({ message: error.message }); return rejectWithValue({ message: ERR_UNKNOWN }); @@ -209,14 +269,14 @@ export const multisigByAddress = createAsyncThunk( } ); -export const getMultisigBalance = createAsyncThunk( +export const getMultisigBalances = createAsyncThunk( 'multisig/multisigBalance', - async (data: GetMultisigBalanceInputs, { rejectWithValue }) => { + async (data: GetMultisigBalancesInputs, { rejectWithValue }) => { try { - const response = await bankService.balance( - data.baseURL, + const response = await bankService.balances( + data.baseURLs, data.address, - data.denom + data.chainID ); return response.data; } catch (error) { @@ -236,11 +296,13 @@ export const createTxn = createAsyncThunk( data.data.address, data.data ); + trackEvent('MULTISIG', 'CREATE_TXN', SUCCESS); return response.data; - } catch (error) { - if (error instanceof AxiosError) - return rejectWithValue({ message: error.message }); - return rejectWithValue({ message: ERR_UNKNOWN }); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('MULTISIG', 'CREATE_TXN', FAILED); + const errMsg = error?.response?.data?.message || ERR_UNKNOWN; + return rejectWithValue({ message: errMsg }); } } ); @@ -276,6 +338,104 @@ export const getAccountAllMultisigTxns = createAsyncThunk( } ); +export const broadcastTransaction = createAsyncThunk( + 'multisig/broadcastTransaction', + async ( + data: { + chainID: string; + multisigAddress: string; + signedTxn: Txn; + walletAddress: string; + pubKeys: MultisigAddressPubkey[]; + threshold: number; + baseURLs: string[]; + rpcURLs: string[]; + }, + { rejectWithValue, dispatch } + ) => { + const authToken = getAuthToken(COSMOS_CHAIN_ID); + const queryParams = { + address: data.walletAddress, + signature: authToken?.signature || '', + }; + try { + const { result, code, transactionHash, fee, memo, rawLog, queryParams } = + await multisigSigning.broadcastTransaction(data); + + if (result.code === 0) { + dispatch( + updateTxn({ + queryParams: queryParams, + data: { + txId: data.signedTxn?.id, + address: data.multisigAddress, + body: { + status: MultisigTxStatus.SUCCESS, + hash: result?.transactionHash || '', + error_message: '', + }, + }, + }) + ); + trackEvent('MULTISIG', 'BROADCAST_TXN', SUCCESS); + } else { + trackEvent('MULTISIG', 'BROADCAST_TXN', FAILED); + dispatch( + setError({ + type: 'error', + message: result?.rawLog || FAILED_TO_BROADCAST_ERROR, + }) + ); + dispatch( + updateTxn({ + queryParams: queryParams, + data: { + txId: data.signedTxn?.id, + address: data.multisigAddress, + body: { + status: MultisigTxStatus.FAILED, + hash: result?.transactionHash || '', + error_message: result?.rawLog || FAILED_TO_BROADCAST_ERROR, + }, + }, + }) + ); + } + return { + data: { code, transactionHash, fee, memo, rawLog }, + chainID: data.chainID, + }; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('MULTISIG', 'BROADCAST_TXN', FAILED); + const errMsg = error?.message; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + + dispatch( + updateTxn({ + queryParams, + data: { + txId: data.signedTxn?.id, + address: data.multisigAddress, + body: { + status: MultisigTxStatus.FAILED, + hash: '', + error_message: error?.message || FAILED_TO_BROADCAST_ERROR, + }, + }, + }) + ); + + return rejectWithValue(errMsg); + } + } +); + export const updateTxn = createAsyncThunk( 'multisig/updateTxn', async (data: UpdateTxnInputs, { rejectWithValue }) => { @@ -295,6 +455,61 @@ export const updateTxn = createAsyncThunk( } ); +export const signTransaction = createAsyncThunk( + 'multisig/signTransaction', + async ( + data: { + chainID: string; + multisigAddress: string; + unSignedTxn: Txn; + walletAddress: string; + rpcURLs: string[]; + }, + { rejectWithValue, dispatch } + ) => { + try { + const payload = await multisigSigning.signTransaction( + data.chainID, + data.multisigAddress, + data.unSignedTxn, + data.walletAddress, + data.rpcURLs + ); + const authToken = getAuthToken(COSMOS_CHAIN_ID); + + const response = await multisigService.signTx( + { + address: data.walletAddress, + signature: authToken?.signature || '', + }, + data.multisigAddress, + data.unSignedTxn.id, + { + signer: payload.signer, + signature: payload.signature, + } + ); + trackEvent('MULTISIG', 'SIGN_TXN', SUCCESS); + return response.data; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('MULTISIG', 'SIGN_TXN', FAILED); + let errMsg = + error?.message || 'Error while signing the transaction, Try again.'; + if (isNetworkError(errMsg)) { + errMsg = `${NETWORK_ERROR}: ${errMsg}`; + } + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + export const signTx = createAsyncThunk( 'multisig/signTx', async (data: SignTxInputs, { rejectWithValue }) => { @@ -317,6 +532,52 @@ export const signTx = createAsyncThunk( } ); +export const importMultisigAccount = createAsyncThunk( + 'multisig/importMultisigAccount', + async ( + data: { + baseURLs: string[]; + accountAddress: string; + multisigAddress: string; + addressPrefix: string; + chainID: string; + }, + { rejectWithValue } + ) => { + try { + const response = await authService.accountInfo( + data.baseURLs, + data.multisigAddress, + data.chainID + ); + if (response?.status === 200) { + if ( + get(response, 'data.account.pub_key.@type') === + MULTISIG_LEGACY_AMINO_PUBKEY_TYPE + ) { + if ( + !isMultisigAccountMember( + data.accountAddress, + get(response, 'data.account.pub_key.public_keys', []), + data.addressPrefix + ) + ) { + return rejectWithValue(NOT_MULTISIG_MEMBER_ERROR); + } + } else { + return rejectWithValue(NOT_MULTISIG_ACCOUNT_ERROR); + } + } + return response.data; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + const errorMsg = + error?.response?.data?.message || error?.message || ERR_UNKNOWN; + return rejectWithValue(errorMsg); + } + } +); + export const multisigSlice = createSlice({ name: 'multisig', initialState, @@ -333,15 +594,27 @@ export const multisigSlice = createSlice({ resetUpdateTxnState: (state) => { state.updateTxnRes = initialState.updateTxnRes; }, + resetBroadcastTxnRes: (state) => { + state.broadcastTxnRes = initialState.broadcastTxnRes; + }, resetSignTxnState: (state) => { state.signTxRes = initialState.signTxRes; }, + resetsignTransactionRes: (state) => { + state.signTransactionRes = initialState.signTransactionRes; + }, resetVerifyAccountRes: (state) => { state.verifyAccountRes = initialState.verifyAccountRes; }, resetDeleteMultisigRes: (state) => { state.deleteMultisigRes = initialState.deleteMultisigRes; }, + resetMultisigAccountData: (state) => { + state.multisigAccountData = initialState.multisigAccountData; + }, + setVerifyDialogOpen: (state, action: PayloadAction) => { + state.verifyDialogOpen = action.payload; + }, }, extraReducers: (builder) => { builder @@ -442,16 +715,16 @@ export const multisigSlice = createSlice({ state.multisigAccount.error = payload.message || ''; }); builder - .addCase(getMultisigBalance.pending, (state) => { + .addCase(getMultisigBalances.pending, (state) => { state.balance.status = TxStatus.PENDING; state.balance.error = ''; }) - .addCase(getMultisigBalance.fulfilled, (state, action) => { + .addCase(getMultisigBalances.fulfilled, (state, action) => { state.balance.status = TxStatus.IDLE; state.balance.error = ''; - state.balance.balance = action.payload.balance; + state.balance.balance = action.payload.balances; }) - .addCase(getMultisigBalance.rejected, (state, action) => { + .addCase(getMultisigBalances.rejected, (state, action) => { state.balance.status = TxStatus.REJECTED; const payload = action.payload as { message: string }; state.balance.error = payload.message || ''; @@ -492,6 +765,7 @@ export const multisigSlice = createSlice({ state.txns.status = TxStatus.IDLE; state.txns.error = ''; state.txns.list = action.payload?.data || []; + state.txns.Count = action.payload?.count || []; }) .addCase(getTxns.rejected, (state, action) => { state.txns.status = TxStatus.REJECTED; @@ -526,6 +800,49 @@ export const multisigSlice = createSlice({ const payload = action.payload as { message: string }; state.signTxRes.error = payload.message || ''; }); + builder + .addCase(signTransaction.pending, (state) => { + state.signTransactionRes.status = TxStatus.PENDING; + }) + .addCase(signTransaction.fulfilled, (state) => { + state.signTransactionRes.status = TxStatus.IDLE; + }) + .addCase(signTransaction.rejected, (state, action) => { + state.signTransactionRes.status = TxStatus.REJECTED; + const payload = action.payload as { message: string }; + state.signTransactionRes.error = payload.message || ''; + }); + builder + .addCase(broadcastTransaction.pending, (state) => { + state.broadcastTxnRes.status = TxStatus.PENDING; + state.broadcastTxnRes.error = ''; + }) + .addCase(broadcastTransaction.fulfilled, (state, action) => { + state.broadcastTxnRes.status = TxStatus.IDLE; + state.broadcastTxnRes.error = ''; + state.broadcastTxnRes.txResponse = action.payload.data; + state.broadcastTxnRes.txHash = action.payload.data.transactionHash; + }) + .addCase(broadcastTransaction.rejected, (state, action) => { + state.broadcastTxnRes.status = TxStatus.REJECTED; + state.broadcastTxnRes.error = action.error.message || ERR_UNKNOWN; + }); + builder + .addCase(importMultisigAccount.pending, (state) => { + state.multisigAccountData.status = TxStatus.PENDING; + state.multisigAccountData.error = ''; + }) + .addCase(importMultisigAccount.fulfilled, (state, action) => { + state.multisigAccountData.status = TxStatus.IDLE; + state.multisigAccountData.error = ''; + state.multisigAccountData.account = + action.payload as ImportMultisigAccountRes; + }) + .addCase(importMultisigAccount.rejected, (state, action) => { + state.multisigAccountData.status = TxStatus.REJECTED; + state.multisigAccountData.error = + typeof action.payload === 'string' ? action.payload : ''; + }); }, }); @@ -537,6 +854,10 @@ export const { resetSignTxnState, resetVerifyAccountRes, resetDeleteMultisigRes, + resetMultisigAccountData, + resetBroadcastTxnRes, + resetsignTransactionRes, + setVerifyDialogOpen, } = multisigSlice.actions; export default multisigSlice.reducer; diff --git a/frontend/src/store/features/recent-transactions/recentTransactionsService.tsx b/frontend/src/store/features/recent-transactions/recentTransactionsService.tsx new file mode 100644 index 000000000..6c905af65 --- /dev/null +++ b/frontend/src/store/features/recent-transactions/recentTransactionsService.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { API_URL } from '@/utils/constants'; +import { cleanURL } from '@/utils/util'; +import Axios, { AxiosResponse } from 'axios'; + +const BASE_URL = cleanURL(API_URL); + +const RECENT_TXNS_URL = (module: string) => `/transactions?module=${module}`; +const ALL_TXNS_URL = ( + address: string, + chainID: string, + limit: number, + offset: number +) => `/txns/${chainID}/${address}?limit=${limit}&offset=${offset}`; + +const TXN_URL = (address: string, chainID: string, txhash: string) => + `/txns/${chainID}/${address}/${txhash}`; + +const ANY_CHAIN_TX_URL = (txHash: string) => `/search/txns/${txHash}`; + +export const fetchRecentTransactions = ({ + payload, + module, +}: { + payload: { + chain_id: string; + address: string; + }[]; + module: string; +}): Promise => + Axios.post(`${BASE_URL}${RECENT_TXNS_URL(module)}`, { addresses: payload }); + +export const fetchAllTransactions = ({ + address, + chainID, + limit, + offset, +}: { + address: string; + chainID: string; + limit: number; + offset: number; +}): Promise => + Axios.get(`${BASE_URL}${ALL_TXNS_URL(address, chainID, limit, offset)}`); + +export const fetchTx = ({ + address, + chainID, + txhash, +}: { + address: string; + chainID: string; + txhash: string; +}): Promise => + Axios.get(`${BASE_URL}${TXN_URL(address, chainID, txhash)}`); + +export const fetchAnyChainTx = (txHash: string): Promise => + Axios.get(`${BASE_URL}${ANY_CHAIN_TX_URL(txHash)}`); + +export default { + recentTransactions: fetchRecentTransactions, + allTransactions: fetchAllTransactions, + fetchTx: fetchTx, + fetchAnyChainTx, +}; diff --git a/frontend/src/store/features/recent-transactions/recentTransactionsSlice.tsx b/frontend/src/store/features/recent-transactions/recentTransactionsSlice.tsx new file mode 100644 index 000000000..627ac45f2 --- /dev/null +++ b/frontend/src/store/features/recent-transactions/recentTransactionsSlice.tsx @@ -0,0 +1,435 @@ +'use client'; + +import { TxStatus } from '@/types/enums'; +import { ERR_TXN_NOT_FOUND, ERR_UNKNOWN } from '@/utils/errors'; +import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { AxiosError } from 'axios'; +import recentTransactionsService from './recentTransactionsService'; +import { + addIBCTxn, + getIBCTxn, + updateIBCTransactionStatusInLocal, +} from '@/utils/localStorage'; +import { FAILED, GAS_FEE, IBC_SEND_TYPE_URL, SUCCESS } from '@/utils/constants'; +import { trackTx } from '../ibc/ibcSlice'; +import { NewTransaction } from '@/utils/transaction'; +import { setError, setTxAndHash } from '../common/commonSlice'; +import { signAndBroadcast } from '@/utils/signing'; +import { trackEvent } from '@/utils/util'; + +interface RecentTransactionsState { + txns: { + data: ParsedTransaction[]; + status: TxStatus; + error: string; + total: number; + }; + txnRepeat: { + status: TxStatus; + error: string; + }; + txn: { + data?: ParsedTransaction[]; + status: TxStatus; + error: string; + }; +} + +const initialState: RecentTransactionsState = { + txns: { + data: [], + total: 0, + error: '', + status: TxStatus.INIT, + }, + txnRepeat: { + status: TxStatus.INIT, + error: '', + }, + txn: { + data: undefined, + status: TxStatus.INIT, + error: '', + }, +}; + +export const getRecentTransactions = createAsyncThunk( + 'recent-txns/get-recent-txns', + async ( + data: { + addresses: { + address: string; + chain_id: string; + }[]; + module: string; + }, + { rejectWithValue, dispatch } + ) => { + try { + const response = await recentTransactionsService.recentTransactions({ + payload: data.addresses, + module: data.module, + }); + let txns: ParsedTransaction[] = []; + const txnsData = response?.data?.data; + txnsData.forEach((txn: ParsedTransaction) => { + const { txhash, messages } = txn; + if (messages[0]?.['@type'] === IBC_SEND_TYPE_URL) { + const ibcTx = getIBCTxn(txhash); + if (ibcTx) { + txns = [...txns, ibcTx]; + if (ibcTx?.isIBCPending) { + dispatch( + trackTx({ + chainID: ibcTx.chain_id, + txHash: ibcTx.txhash, + }) + ); + } + } else { + let formattedTxn = txn; + formattedTxn = { ...formattedTxn, isIBCPending: false }; + formattedTxn = { ...formattedTxn, isIBCTxn: true }; + txns = [...txns, formattedTxn]; + } + } else { + let formattedTxn = txn; + formattedTxn = { ...formattedTxn, isIBCPending: false }; + formattedTxn = { ...formattedTxn, isIBCTxn: false }; + txns = [...txns, formattedTxn]; + } + }); + return { + data: txns, + }; + } catch (error) { + if (error instanceof AxiosError) + return rejectWithValue({ + message: error?.response?.data?.message || ERR_UNKNOWN, + }); + return rejectWithValue({ message: ERR_UNKNOWN }); + } + } +); + +export const getAllTransactions = createAsyncThunk( + 'recent-txns/get-all-txns', + async ( + data: { + address: string; + chainID: string; + limit: number; + offset: number; + }, + { rejectWithValue, dispatch } + ) => { + try { + const response = await recentTransactionsService.allTransactions(data); + let txns: ParsedTransaction[] = []; + const txnsData = response?.data?.data?.data; + txnsData.forEach((txn: ParsedTransaction) => { + const { txhash, messages } = txn; + if (messages[0]?.['@type'] === IBC_SEND_TYPE_URL) { + const ibcTx = getIBCTxn(txhash); + if (ibcTx) { + txns = [...txns, ibcTx]; + if (ibcTx?.isIBCPending) { + dispatch( + trackTx({ + chainID: ibcTx.chain_id, + txHash: ibcTx.txhash, + }) + ); + } + } else { + let formattedTxn = txn; + formattedTxn = { ...formattedTxn, isIBCPending: false }; + formattedTxn = { ...formattedTxn, isIBCTxn: true }; + txns = [...txns, formattedTxn]; + } + } else { + let formattedTxn = txn; + formattedTxn = { ...formattedTxn, isIBCPending: false }; + formattedTxn = { ...formattedTxn, isIBCTxn: false }; + txns = [...txns, formattedTxn]; + } + }); + return { + data: { + data: txns, + total: response?.data?.data?.total, + }, + }; + } catch (error) { + if (error instanceof AxiosError) + return rejectWithValue({ + message: error?.response?.data?.message || ERR_UNKNOWN, + }); + return rejectWithValue({ message: ERR_UNKNOWN }); + } + } +); + +export const getTransaction = createAsyncThunk( + 'recent-txns/get-txn', + async ( + data: { + address: string; + chainID: string; + txhash: string; + }, + { rejectWithValue, dispatch } + ) => { + try { + const response = await recentTransactionsService.fetchTx(data); + let txns: ParsedTransaction[] = []; + const txnsData = response?.data?.data?.data; + const { txhash, messages } = txnsData; + if (txnsData) { + if (messages[0]?.['@type'] === IBC_SEND_TYPE_URL) { + const ibcTx = getIBCTxn(txhash); + if (ibcTx) { + txns = [...txns, ibcTx]; + if (ibcTx?.isIBCPending) { + dispatch( + trackTx({ + chainID: ibcTx.chain_id, + txHash: ibcTx.txhash, + }) + ); + } + } else { + let formattedTxn = txnsData; + formattedTxn = { ...formattedTxn, isIBCPending: false }; + formattedTxn = { ...formattedTxn, isIBCTxn: true }; + txns = [...txns, formattedTxn]; + } + } else { + let formattedTxn = txnsData; + formattedTxn = { ...formattedTxn, isIBCPending: false }; + formattedTxn = { ...formattedTxn, isIBCTxn: false }; + + txns = [...txns, formattedTxn]; + } + } else { + throw new Error(ERR_TXN_NOT_FOUND); + } + + return { data: txns }; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + const errMsg = error?.message || ERR_TXN_NOT_FOUND; + return rejectWithValue(errMsg); + } + } +); + +export const getAnyChainTransaction = createAsyncThunk( + 'recent-txns/get-any-chain-txn', + async ( + data: { + txhash: string; + }, + { rejectWithValue } + ) => { + try { + const response = await recentTransactionsService.fetchAnyChainTx( + data.txhash + ); + let txns: ParsedTransaction[] = []; + const txnsData = response?.data?.data?.data; + let formattedTxn = txnsData; + if (txnsData) { + formattedTxn = { ...formattedTxn, isIBCPending: false }; + formattedTxn = { ...formattedTxn, isIBCTxn: false }; + } else { + throw new Error(ERR_TXN_NOT_FOUND); + } + + txns = [...txns, formattedTxn]; + + return { data: txns }; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + const errMsg = error?.message || ERR_TXN_NOT_FOUND; + return rejectWithValue(errMsg); + } + } +); + +export const txRepeatTransaction = createAsyncThunk( + 'recent-txns/repeat-txn', + async ( + data: RepeatTransactionInputs, + { rejectWithValue, fulfillWithValue, dispatch } + ) => { + try { + const result = await signAndBroadcast( + data.basicChainInfo.chainID, + data.basicChainInfo.aminoConfig, + data.basicChainInfo.prefix, + data.messages, + GAS_FEE, + '', + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.basicChainInfo.feeCurrencies[0].coinDenom + }`, + data.basicChainInfo.rest, + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs + ); + const tx = NewTransaction( + result, + data.messages, + data.basicChainInfo.chainID, + data.basicChainInfo.address + ); + + dispatch( + setTxAndHash({ + tx, + hash: tx.transactionHash, + }) + ); + + if (result?.code === 0) { + trackEvent('TRANSACTIONS', 'REPEAT_TRANSACTION', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); + } else { + trackEvent('TRANSACTIONS', 'REPEAT_TRANSACTION', FAILED); + return rejectWithValue(result?.rawLog); + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('TRANSACTIONS', 'REPEAT_TRANSACTION', FAILED); + const errMsg = error?.message || 'Failed to execute contract'; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(errMsg); + } + } +); + +export const recentTransactionsSlice = createSlice({ + name: 'recentTransactions', + initialState, + reducers: { + resetRecentTxns: (state) => { + state.txns.data = []; + state.txns.error = ''; + state.txns.status = TxStatus.INIT; + state.txns.total = 0; + }, + addIBCTransaction: (state, action: PayloadAction) => { + const transaction = action.payload; + state.txns.data = [transaction, ...state.txns.data]; + addIBCTxn(transaction); + }, + updateIBCTransactionStatus: ( + state, + action: PayloadAction<{ txHash: string }> + ) => { + const { txHash } = action.payload; + const allTransactions = state.txns.data; + const updatedAllTransactions = allTransactions.map((tx) => { + if (tx.txhash === txHash) { + return { ...tx, isIBCPending: false }; + } + return tx; + }); + state.txns.data = updatedAllTransactions; + updateIBCTransactionStatusInLocal(txHash); + }, + }, + extraReducers: (builder) => { + builder + .addCase(getRecentTransactions.pending, (state) => { + state.txns.status = TxStatus.PENDING; + state.txns.error = ''; + }) + .addCase(getRecentTransactions.fulfilled, (state, action) => { + state.txns.status = TxStatus.IDLE; + state.txns.data = action.payload.data; + state.txns.error = ''; + }) + .addCase(getRecentTransactions.rejected, (state, action) => { + state.txns.status = TxStatus.REJECTED; + state.txns.data = []; + const payload = action.payload as { message: string }; + state.txns.error = payload.message || ''; + }); + builder + .addCase(getAllTransactions.pending, (state) => { + state.txns.status = TxStatus.PENDING; + state.txns.error = ''; + }) + .addCase(getAllTransactions.fulfilled, (state, action) => { + state.txns.status = TxStatus.IDLE; + state.txns.data = action.payload.data.data; + state.txns.total = action.payload.data.total; + state.txns.error = ''; + }) + .addCase(getAllTransactions.rejected, (state, action) => { + state.txns.status = TxStatus.REJECTED; + state.txns.data = []; + const payload = action.payload as { message: string }; + state.txns.error = payload.message || ''; + }); + builder + .addCase(getTransaction.pending, (state) => { + state.txn.status = TxStatus.PENDING; + state.txn.error = ''; + }) + .addCase(getTransaction.fulfilled, (state, action) => { + state.txn.status = TxStatus.IDLE; + state.txn.data = action?.payload?.data || []; + state.txns.error = ''; + }) + .addCase(getTransaction.rejected, (state, action) => { + state.txn.status = TxStatus.REJECTED; + state.txn.data = []; + state.txn.error = action.error.message || 'Failed to fetch transaction'; + }); + builder + .addCase(getAnyChainTransaction.pending, (state) => { + state.txn.status = TxStatus.PENDING; + state.txn.error = ''; + }) + .addCase(getAnyChainTransaction.fulfilled, (state, action) => { + state.txn.status = TxStatus.IDLE; + state.txn.data = action?.payload?.data || []; + state.txns.error = ''; + }) + .addCase(getAnyChainTransaction.rejected, (state, action) => { + state.txn.status = TxStatus.REJECTED; + state.txn.data = []; + state.txn.error = action.error.message || 'Failed to fetch transaction'; + }); + builder + .addCase(txRepeatTransaction.pending, (state) => { + state.txnRepeat.status = TxStatus.PENDING; + state.txnRepeat.error = ''; + }) + .addCase(txRepeatTransaction.fulfilled, (state) => { + state.txnRepeat.status = TxStatus.IDLE; + state.txnRepeat.error = ''; + }) + .addCase(txRepeatTransaction.rejected, (state, action) => { + state.txnRepeat.status = TxStatus.REJECTED; + state.txnRepeat.error = action.error.message || 'Transaction failed'; + }); + }, +}); + +export const { + resetRecentTxns, + addIBCTransaction, + updateIBCTransactionStatus, +} = recentTransactionsSlice.actions; + +export default recentTransactionsSlice.reducer; diff --git a/frontend/src/store/features/staking/stakeSlice.ts b/frontend/src/store/features/staking/stakeSlice.ts index 757370893..14a03abae 100644 --- a/frontend/src/store/features/staking/stakeSlice.ts +++ b/frontend/src/store/features/staking/stakeSlice.ts @@ -6,7 +6,7 @@ import stakingService from './stakingService'; import { ERR_UNKNOWN } from '../../../utils/errors'; import { signAndBroadcast } from '../../../utils/signing'; import cloneDeep from 'lodash/cloneDeep'; -import { GAS_FEE } from '../../../utils/constants'; +import { FAILED, GAS_FEE, SUCCESS } from '../../../utils/constants'; import { GetDelegationsResponse, GetUnbondingResponse, @@ -23,11 +23,14 @@ import { import { AxiosError } from 'axios'; import { TxStatus } from '../../../types/enums'; import { NewTransaction } from '@/utils/transaction'; -import { addTransactions } from '../transactionHistory/transactionHistorySlice'; import { setError, setTxAndHash } from '../common/commonSlice'; import { Unbonding } from '@/txns/staking/unbonding'; -import { getDelegatorTotalRewards } from '../distribution/distributionSlice'; -import { getBalances } from '../bank/bankSlice'; +import { + getAuthzDelegatorTotalRewards, + getDelegatorTotalRewards, +} from '../distribution/distributionSlice'; +import { getAuthzBalances, getBalances } from '../bank/bankSlice'; +import { trackEvent } from '@/utils/util'; interface Chain { validators: Validators; @@ -48,7 +51,12 @@ interface Chain { pagination: Pagination | undefined; totalUnbonded: number; }; - + validator: { + [key: string]: Validator | undefined | TxStatus | string; + validatorInfo: Validator | undefined; + status: TxStatus; + errMsg: string; + }; params: Params | undefined; paramsStatus: TxStatus; tx: { @@ -63,27 +71,60 @@ interface Chain { reStakeTxStatus: TxStatus; cancelUnbondingTxStatus: TxStatus; isTxAll: boolean; + validatorProfiles: Record; } -interface Chains { +export interface Chains { [key: string]: Chain; } interface StakingState { validatorsLoading: number; delegationsLoading: number; + undelegationsLoading: number; + totalUndelegationsAmount: number; chains: Chains; hasDelegations: boolean; hasUnbonding: boolean; defaultState: Chain; + authz: { + delegationsLoading: number; + chains: Chains; + hasDelegations: boolean; + hasUnbonding: boolean; + undelegationsLoading: number; + totalUndelegationsAmount: number; + }; + /* eslint-disable @typescript-eslint/no-explicit-any */ + witvalNonCosmosValidators: { + chains: Record; + delegators: Record; + }; + allValidators: Record; + filteredValidators: Record; + searchQuery: string; } const initialState: StakingState = { chains: {}, validatorsLoading: 0, delegationsLoading: 0, + undelegationsLoading: 0, + totalUndelegationsAmount: 0, hasUnbonding: false, hasDelegations: false, + authz: { + chains: {}, + delegationsLoading: 0, + hasUnbonding: false, + hasDelegations: false, + undelegationsLoading: 0, + totalUndelegationsAmount: 0, + }, + witvalNonCosmosValidators: { + chains: {}, + delegators: {}, + }, defaultState: { paramsStatus: TxStatus.INIT, validators: { @@ -99,6 +140,7 @@ const initialState: StakingState = { totalActive: 0, totalInactive: 0, }, + validatorProfiles: {}, delegations: { status: TxStatus.INIT, delegations: { @@ -128,6 +170,11 @@ const initialState: StakingState = { pagination: undefined, totalUnbonded: 0.0, }, + validator: { + validatorInfo: undefined, + errMsg: '', + status: TxStatus.INIT, + }, pool: { not_bonded_tokens: '0', bonded_tokens: '0', @@ -142,16 +189,18 @@ const initialState: StakingState = { cancelUnbondingTxStatus: TxStatus.INIT, isTxAll: false, }, + allValidators: {}, + filteredValidators: {}, + searchQuery: '', }; export const txRestake = createAsyncThunk( 'staking/restake', async ( - data: TxReStakeInputs, + data: TxReStakeInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { - const { chainID, address, rest, aminoConfig, prefix, cosmosAddress } = - data.basicChainInfo; + const { chainID, address, rest, aminoConfig, prefix } = data.basicChainInfo; try { const result = await signAndBroadcast( chainID, @@ -160,18 +209,15 @@ export const txRestake = createAsyncThunk( data.msgs, 399999 + Math.ceil(399999 * 0.1 * (data.msgs?.length || 1)), data.memo, - `${data.feeAmount}${data.denom}`, + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, rest, - data.feegranter?.length > 0 ? data.feegranter : undefined + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs ); const tx = NewTransaction(result, data.msgs, chainID, address); - dispatch( - addTransactions({ - chainID, - address: cosmosAddress, - transactions: [tx], - }) - ); dispatch( setTxAndHash({ tx, @@ -179,27 +225,58 @@ export const txRestake = createAsyncThunk( }) ); if (result?.code === 0) { - dispatch( - getDelegatorTotalRewards({ - baseURL: rest, - address: address, - chainID: chainID, - denom: data.denom, - }) - ); - dispatch( - getDelegations({ - baseURL: rest, - address: address, - chainID: chainID, - }) - ); + trackEvent('STAKING', 'RESTAKE', SUCCESS); + if (data.isAuthzMode) { + dispatch( + getAuthzDelegatorTotalRewards({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: rest, + address: data.authzChainGranter, + chainID: chainID, + denom: data.denom, + }) + ); + dispatch( + getAuthzDelegations({ + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: chainID, + }) + ); + } else { + dispatch( + getDelegatorTotalRewards({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: rest, + address: address, + chainID: chainID, + denom: data.denom, + }) + ); + dispatch( + getDelegations({ + baseURLs: data.basicChainInfo.restURLs, + address: address, + chainID: chainID, + }) + ); + } return fulfillWithValue({ txHash: result?.transactionHash }); } else { + trackEvent('STAKING', 'RESTAKE', FAILED); return rejectWithValue(result?.rawLog); } - } catch (error) { - if (error instanceof AxiosError) return rejectWithValue(error.response); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + trackEvent('STAKING', 'RESTAKE', FAILED); + const errMessage = error?.response?.data?.error || error?.message; + dispatch( + setError({ + type: 'error', + message: errMessage || ERR_UNKNOWN, + }) + ); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -207,43 +284,41 @@ export const txRestake = createAsyncThunk( export const txDelegate = createAsyncThunk( 'staking/delegate', async ( - data: TxDelegateInputs, + data: TxDelegateInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { try { - const msg = Delegate( - data.delegator, - data.validator, - data.amount, - data.denom - ); + let msgs: Msg[]; + if (data.isAuthzMode) { + msgs = data.msgs; + } else { + msgs = [ + Delegate(data.delegator, data.validator, data.amount, data.denom), + ]; + } const result = await signAndBroadcast( data.basicChainInfo.chainID, data.basicChainInfo.aminoConfig, data.basicChainInfo.prefix, - [msg], + msgs, GAS_FEE, '', - `${data.feeAmount}${data.denom}`, + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, data.basicChainInfo.rest, - data.feegranter?.length > 0 ? data.feegranter : undefined + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs ); const tx = NewTransaction( result, - [msg], + msgs, data.basicChainInfo.chainID, data.basicChainInfo.address ); - dispatch( - addTransactions({ - chainID: data.basicChainInfo.chainID, - address: data.basicChainInfo.cosmosAddress, - transactions: [tx], - }) - ); - dispatch( setTxAndHash({ tx, @@ -252,27 +327,62 @@ export const txDelegate = createAsyncThunk( ); if (result?.code === 0) { - dispatch(resetDelegations({ chainID: data.basicChainInfo.chainID })); - dispatch( - getDelegations({ - baseURL: data.basicChainInfo.baseURL, - address: data.delegator, - chainID: data.basicChainInfo.chainID, - }) - ); - dispatch( - getBalances({ - baseURL: data.basicChainInfo.baseURL, - chainID: data.basicChainInfo.chainID, - address: data.delegator, - }) - ); + if (data.isAuthzMode) { + dispatch( + resetAuthzDelegations({ chainID: data.basicChainInfo.chainID }) + ); + dispatch( + getAuthzDelegations({ + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: data.basicChainInfo.chainID, + }) + ); + dispatch( + getAuthzBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.baseURL, + chainID: data.basicChainInfo.chainID, + address: data.authzChainGranter, + }) + ); + } else { + dispatch(resetDelegations({ chainID: data.basicChainInfo.chainID })); + dispatch( + getDelegations({ + baseURLs: data.basicChainInfo.restURLs, + address: data.delegator, + chainID: data.basicChainInfo.chainID, + }) + ); + dispatch( + getBalances({ + baseURLs: data.basicChainInfo.restURLs, + baseURL: data.basicChainInfo.baseURL, + chainID: data.basicChainInfo.chainID, + address: data.delegator, + }) + ); + } + + trackEvent('STAKING', 'DELEGATE', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); } else { + trackEvent('STAKING', 'DELEGATE', FAILED); return rejectWithValue(result?.rawLog); } - } catch (error) { - if (error instanceof AxiosError) return rejectWithValue(error.response); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + trackEvent('STAKING', 'DELEGATE', FAILED); + const errMessage = error?.response?.data?.error || error?.message; + dispatch( + setError({ + type: 'error', + message: errMessage || ERR_UNKNOWN, + }) + ); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -280,44 +390,48 @@ export const txDelegate = createAsyncThunk( export const txReDelegate = createAsyncThunk( 'staking/redelegate', async ( - data: TxRedelegateInputs, + data: TxRedelegateInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { try { - const msg = Redelegate( - data.delegator, - data.srcVal, - data.destVal, - data.amount, - data.denom - ); + let msgs: Msg[]; + if (data.isAuthzMode) { + msgs = data.msgs; + } else { + msgs = [ + Redelegate( + data.delegator, + data.srcVal, + data.destVal, + data.amount, + data.denom + ), + ]; + } + const result = await signAndBroadcast( data.basicChainInfo.chainID, data.basicChainInfo.aminoConfig, data.basicChainInfo.prefix, - [msg], + msgs, GAS_FEE, '', - `${data.feeAmount}${data.denom}`, + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, data.basicChainInfo.rest, - data.feegranter?.length > 0 ? data.feegranter : undefined + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs ); const tx = NewTransaction( result, - [msg], + msgs, data.basicChainInfo.chainID, data.basicChainInfo.address ); - dispatch( - addTransactions({ - chainID: data.basicChainInfo.chainID, - address: data.basicChainInfo.cosmosAddress, - transactions: [tx], - }) - ); - dispatch( setTxAndHash({ tx, @@ -326,20 +440,47 @@ export const txReDelegate = createAsyncThunk( ); if (result?.code === 0) { - dispatch(resetDelegations({ chainID: data.basicChainInfo.chainID })); - dispatch( - getDelegations({ - baseURL: data.basicChainInfo.baseURL, - address: data.delegator, - chainID: data.basicChainInfo.chainID, - }) - ); + if (data.isAuthzMode) { + dispatch( + resetAuthzDelegations({ chainID: data.basicChainInfo.chainID }) + ); + dispatch( + getAuthzDelegations({ + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: data.basicChainInfo.chainID, + }) + ); + } else { + dispatch(resetDelegations({ chainID: data.basicChainInfo.chainID })); + dispatch( + getDelegations({ + baseURLs: data.basicChainInfo.restURLs, + address: data.delegator, + chainID: data.basicChainInfo.chainID, + }) + ); + } + + trackEvent('STAKING', 'REDELEGATE', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); } else { + trackEvent('STAKING', 'REDELEGATE', FAILED); + return rejectWithValue(result?.rawLog); } - } catch (error) { - if (error instanceof AxiosError) return rejectWithValue(error.response); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + trackEvent('STAKING', 'REDELEGATE', FAILED); + const errMessage = error?.response?.data?.error || error?.message; + dispatch( + setError({ + type: 'error', + message: errMessage || ERR_UNKNOWN, + }) + ); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -347,43 +488,42 @@ export const txReDelegate = createAsyncThunk( export const txUnDelegate = createAsyncThunk( 'staking/undelegate', async ( - data: TxUndelegateInputs, + data: TxUndelegateInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { try { - const msg = UnDelegate( - data.delegator, - data.validator, - data.amount, - data.denom - ); + let msgs: Msg[]; + if (data.isAuthzMode) { + msgs = data.msgs; + } else { + msgs = [ + UnDelegate(data.delegator, data.validator, data.amount, data.denom), + ]; + } + const result = await signAndBroadcast( data.basicChainInfo.chainID, data.basicChainInfo.aminoConfig, data.basicChainInfo.prefix, - [msg], - 860000, + msgs, + GAS_FEE, '', - `${data.feeAmount}${data.denom}`, + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, data.basicChainInfo.rest, - data.feegranter?.length > 0 ? data.feegranter : undefined + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs ); const tx = NewTransaction( result, - [msg], + msgs, data.basicChainInfo.chainID, data.basicChainInfo.address ); - dispatch( - addTransactions({ - chainID: data.basicChainInfo.chainID, - address: data.basicChainInfo.cosmosAddress, - transactions: [tx], - }) - ); - dispatch( setTxAndHash({ tx, @@ -392,26 +532,57 @@ export const txUnDelegate = createAsyncThunk( ); if (result?.code === 0) { - dispatch( - getDelegations({ - baseURL: data.basicChainInfo.rest, - address: data.basicChainInfo.address, - chainID: data.basicChainInfo.chainID, - }) - ); - dispatch( - getUnbonding({ - baseURL: data.basicChainInfo.rest, - address: data.basicChainInfo.address, - chainID: data.basicChainInfo.chainID, - }) - ); + if (data.isAuthzMode) { + dispatch( + getAuthzDelegations({ + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: data.basicChainInfo.chainID, + }) + ); + dispatch( + getAuthzUnbonding({ + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: data.basicChainInfo.chainID, + }) + ); + } else { + dispatch( + getDelegations({ + baseURLs: data.basicChainInfo.restURLs, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + }) + ); + dispatch( + getUnbonding({ + baseURLs: data.basicChainInfo.restURLs, + address: data.basicChainInfo.address, + chainID: data.basicChainInfo.chainID, + }) + ); + } + + trackEvent('STAKING', 'UNDELEGATE', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); } else { + trackEvent('STAKING', 'UNDELEGATE', FAILED); + return rejectWithValue(result?.rawLog); } - } catch (error) { - if (error instanceof AxiosError) return rejectWithValue(error.response); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + trackEvent('STAKING', 'UNDELEGATE', FAILED); + const errMessage = error?.response?.data?.error || error?.message; + dispatch( + setError({ + type: 'error', + message: errMessage || ERR_UNKNOWN, + }) + ); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -419,43 +590,47 @@ export const txUnDelegate = createAsyncThunk( export const txCancelUnbonding = createAsyncThunk( 'staking/cancel-unbonding', async ( - data: TxCancelUnbondingInputs, + data: TxCancelUnbondingInputs | TxAuthzExecInputs, { rejectWithValue, fulfillWithValue, dispatch } ) => { try { - const msg = Unbonding( - data.delegator, - data.validator, - data.amount, - data.denom, - data.creationHeight - ); - msg.value.creationHeight = msg.value.creationHeight.toString(); + let msgs: Msg[]; + if (data.isAuthzMode) { + msgs = data.msgs; + } else { + const msg = Unbonding( + data.delegator, + data.validator, + data.amount, + data.denom, + data.creationHeight + ); + msg.value.creationHeight = msg.value.creationHeight.toString(); + msgs = [msg]; + } + const result = await signAndBroadcast( data.basicChainInfo.chainID, data.basicChainInfo.aminoConfig, data.basicChainInfo.prefix, - [msg], + msgs, GAS_FEE, '', - `${data.feeAmount}${data.denom}`, + `${data.basicChainInfo.feeAmount * 10 ** data.basicChainInfo.decimals}${ + data.denom + }`, data.basicChainInfo.rest, - data.feegranter?.length > 0 ? data.feegranter : undefined + data?.feegranter?.length ? data.feegranter : undefined, + data?.basicChainInfo?.rpc, + data?.basicChainInfo?.restURLs ); const tx = NewTransaction( result, - [msg], + msgs, data.basicChainInfo.chainID, data.basicChainInfo.address ); - dispatch( - addTransactions({ - chainID: data.basicChainInfo.chainID, - address: data.basicChainInfo.cosmosAddress, - transactions: [tx], - }) - ); dispatch( setTxAndHash({ tx, @@ -464,36 +639,47 @@ export const txCancelUnbonding = createAsyncThunk( ); if (result?.code === 0) { - const inputData = { - baseURL: data.basicChainInfo.baseURL, - address: data.delegator, - chainID: data.basicChainInfo.chainID, - }; - dispatch(resetDelegations({ chainID: data.basicChainInfo.chainID })); - dispatch(getDelegations(inputData)); - dispatch(getUnbonding(inputData)); + if (data.isAuthzMode) { + const inputData = { + baseURLs: data.basicChainInfo.restURLs, + address: data.authzChainGranter, + chainID: data.basicChainInfo.chainID, + }; + dispatch( + resetAuthzDelegations({ chainID: data.basicChainInfo.chainID }) + ); + dispatch(getAuthzDelegations(inputData)); + dispatch(getAuthzUnbonding(inputData)); + } else { + const inputData = { + baseURLs: data.basicChainInfo.restURLs, + address: data.delegator, + chainID: data.basicChainInfo.chainID, + }; + dispatch(resetDelegations({ chainID: data.basicChainInfo.chainID })); + dispatch(getDelegations(inputData)); + dispatch(getUnbonding(inputData)); + } + + trackEvent('STAKING', 'CANCEL_UNBONDING', SUCCESS); + return fulfillWithValue({ txHash: result?.transactionHash }); } else { + trackEvent('STAKING', 'CANCEL_UNBONDING', FAILED); + return rejectWithValue(result?.rawLog); } - } catch (error) { - console.log('Error while cancel unbonding the transaction ', error); - if (error instanceof AxiosError) { - dispatch( - setError({ - type: 'error', - message: error.message, - }) - ); - return rejectWithValue(error.response); - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + trackEvent('STAKING', 'CANCEL_UNBONDING', FAILED); + const errMessage = error?.response?.data?.error || error?.message; dispatch( setError({ type: 'error', - message: ERR_UNKNOWN, + message: errMessage || ERR_UNKNOWN, }) ); - return rejectWithValue(ERR_UNKNOWN); + return rejectWithValue(errMessage || ERR_UNKNOWN); } } ); @@ -502,7 +688,7 @@ export const getValidators = createAsyncThunk( 'staking/validators', async ( data: { - baseURL: string; + baseURLs: string[]; status?: string; pagination?: KeyLimitPagination; chainID: string; @@ -511,7 +697,8 @@ export const getValidators = createAsyncThunk( ) => { try { const response = await stakingService.validators( - data.baseURL, + data.baseURLs, + data.chainID, data?.status, data?.pagination ); @@ -531,7 +718,7 @@ export const getAllValidators = createAsyncThunk( 'staking/all-validators', async ( data: { - baseURL: string; + baseURLs: string[]; status?: string; chainID: string; }, @@ -543,7 +730,8 @@ export const getAllValidators = createAsyncThunk( const limit = 100; while (true) { const response = await stakingService.validators( - data.baseURL, + data.baseURLs, + data.chainID, data?.status, nextKey ? { @@ -572,8 +760,8 @@ export const getAllValidators = createAsyncThunk( export const getPoolInfo = createAsyncThunk( 'staking/poolInfo', - async (data: { baseURL: string; chainID: string }) => { - const response = await stakingService.poolInfo(data.baseURL); + async (data: { baseURLs: string[]; chainID: string }) => { + const response = await stakingService.poolInfo(data.baseURLs, data.chainID); return { chainID: data.chainID, data: response.data, @@ -583,8 +771,27 @@ export const getPoolInfo = createAsyncThunk( export const getParams = createAsyncThunk( 'staking/params', - async (data: { baseURL: string; chainID: string }) => { - const response = await stakingService.params(data.baseURL); + async (data: { baseURLs: string[]; chainID: string }) => { + const response = await stakingService.params(data.baseURLs, data.chainID); + return { + data: response.data, + chainID: data.chainID, + }; + } +); + +export const getTotalDelegationsCount = createAsyncThunk( + 'staking/total-delegations-count', + async (data: { + baseURLs: string[]; + chainID: string; + operatorAddress: string; + }) => { + const response = await stakingService.validatorDelegations( + data.baseURLs, + data.operatorAddress, + data.chainID + ); return { data: response.data, chainID: data.chainID, @@ -596,7 +803,51 @@ export const getDelegations = createAsyncThunk( 'staking/delegations', async ( data: { - baseURL: string; + baseURLs: string[]; + address: string; + chainID: string; + }, + { rejectWithValue } + ) => { + try { + const delegations = []; + let nextKey = null; + const limit = 100; + while (true) { + const response = await stakingService.delegations( + data.baseURLs, + data.address, + data.chainID, + nextKey + ? { + key: nextKey, + limit: limit, + } + : {} + ); + delegations.push(...(response.data?.delegation_responses || [])); + if (!response.data.pagination?.next_key) { + break; + } + nextKey = response.data.pagination.next_key; + } + + return { + delegations: delegations, + chainID: data.chainID, + }; + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.message); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const getAuthzDelegations = createAsyncThunk( + 'staking/authz-delegations', + async ( + data: { + baseURLs: string[]; address: string; chainID: string; }, @@ -608,8 +859,9 @@ export const getDelegations = createAsyncThunk( const limit = 100; while (true) { const response = await stakingService.delegations( - data.baseURL, + data.baseURLs, data.address, + data.chainID, nextKey ? { key: nextKey, @@ -638,13 +890,14 @@ export const getDelegations = createAsyncThunk( export const getUnbonding = createAsyncThunk( 'staking/unbonding', async ( - data: { baseURL: string; address: string; chainID: string }, + data: { baseURLs: string[]; address: string; chainID: string }, { rejectWithValue } ) => { try { const response = await stakingService.unbonding( - data.baseURL, - data.address + data.baseURLs, + data.address, + data.chainID ); return { data: response.data, @@ -657,10 +910,176 @@ export const getUnbonding = createAsyncThunk( } ); +export const getAuthzUnbonding = createAsyncThunk( + 'staking/authz-unbonding', + async ( + data: { baseURLs: string[]; address: string; chainID: string }, + { rejectWithValue } + ) => { + try { + const response = await stakingService.unbonding( + data.baseURLs, + data.address, + data.chainID + ); + return { + data: response.data, + chainID: data.chainID, + }; + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.message); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const getValidator = createAsyncThunk( + 'staking/get-validator', + async ( + data: { + baseURLs: string[]; + chainID: string; + valoperAddress: string; + }, + { rejectWithValue } + ) => { + try { + const response = await stakingService.validatorInfo( + data.baseURLs, + data.valoperAddress, + data.chainID + ); + return { + data: response.data, + chainID: data.chainID, + }; + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.message); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const getAuthzValidator = createAsyncThunk( + 'staking/get-authz-validator', + async ( + data: { + baseURLs: string[]; + chainID: string; + valoperAddress: string; + }, + { rejectWithValue } + ) => { + try { + const response = await stakingService.validatorInfo( + data.baseURLs, + data.valoperAddress, + data.chainID + ); + return { + data: response.data, + chainID: data.chainID, + }; + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.message); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const getWitvalPolygonValidator = createAsyncThunk( + 'staking/get-polygon-witval', + async ( + data: { + baseURL: string; + id: number; + }, + { rejectWithValue } + ) => { + try { + const response = await stakingService.polygonValidator( + data.baseURL, + data.id + ); + return { + data: response.data, + }; + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.message); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const getWitvalPolygonDelegatorsCount = createAsyncThunk( + 'staking/get-polygon-witval-delegators', + async ( + data: { + baseURL: string; + id: number; + }, + { rejectWithValue } + ) => { + try { + const response = await stakingService.polygonDelegators( + data.baseURL, + data.id + ); + return { + data: response.data, + }; + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.message); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + +export const getWitvalOasisDelegations = createAsyncThunk( + 'staking/get-oasis-witval-delegations', + async ( + data: { + baseURL: string; + operatorAddress: string; + }, + { rejectWithValue } + ) => { + try { + const response = await stakingService.oasisDelegations( + data.baseURL, + data.operatorAddress + ); + return { + data: response.data, + }; + } catch (error) { + if (error instanceof AxiosError) return rejectWithValue(error.message); + return rejectWithValue(ERR_UNKNOWN); + } + } +); + export const stakeSlice = createSlice({ name: 'staking', initialState, reducers: { + setValidators(state, action: PayloadAction>) { + state.allValidators = action.payload; + state.filteredValidators = action.payload; + }, + setSearchQuery(state, action: PayloadAction) { + state.searchQuery = action.payload; + }, + filterValidators(state) { + const query = state.searchQuery.toLowerCase(); + state.filteredValidators = Object.fromEntries( + Object.entries(state.allValidators).filter( + ([, validator]) => + validator.operator_address.toLowerCase().includes(query) || + validator.description?.moniker.toLowerCase().includes(query) + ) + ); + }, resetRestakeTx: (state, action: PayloadAction<{ chainID: string }>) => { const chainID = action.payload.chainID; if (chainID?.length && state.chains[chainID]) { @@ -675,6 +1094,20 @@ export const stakeSlice = createSlice({ const { chainID } = action.payload; state.chains[chainID] = cloneDeep(initialState.defaultState); }, + resetCompleteState: (state) => { + /* eslint-disable-next-line */ + state = cloneDeep(initialState); + }, + resetAuthz: (state) => { + state.authz = { + chains: {}, + delegationsLoading: 0, + hasUnbonding: false, + hasDelegations: false, + undelegationsLoading: 0, + totalUndelegationsAmount: 0, + }; + }, resetCancelUnbondingTx: ( state, action: PayloadAction<{ chainID: string }> @@ -696,6 +1129,14 @@ export const stakeSlice = createSlice({ const { chainID } = action.payload; state.chains[chainID].delegations = initialState.defaultState.delegations; }, + resetAuthzDelegations: ( + state, + action: PayloadAction<{ chainID: string }> + ) => { + const { chainID } = action.payload; + state.authz.chains[chainID].delegations = + initialState.defaultState.delegations; + }, sortValidatorsByVotingPower: ( state, action: PayloadAction<{ chainID: string }> @@ -894,6 +1335,51 @@ export const stakeSlice = createSlice({ } }); + builder + .addCase(getAuthzDelegations.pending, (state, action) => { + state.authz.delegationsLoading++; + const chainID = action.meta?.arg?.chainID; + if (!state.authz.chains[chainID]) + state.authz.chains[chainID] = cloneDeep(initialState.defaultState); + state.authz.chains[chainID].delegations.status = TxStatus.PENDING; + state.authz.chains[chainID].delegations.errMsg = ''; + }) + .addCase(getAuthzDelegations.fulfilled, (state, action) => { + state.authz.delegationsLoading--; + const chainID = action.meta?.arg?.chainID; + if (state.authz.chains[chainID]) { + const delegation_responses = action.payload.delegations; + if (delegation_responses?.length) { + state.authz.chains[chainID].delegations.hasDelegations = true; + state.authz.hasDelegations = true; + } + state.authz.chains[chainID].delegations.status = TxStatus.IDLE; + state.authz.chains[ + chainID + ].delegations.delegations.delegation_responses = delegation_responses; + state.authz.chains[chainID].delegations.errMsg = ''; + + let total = 0.0; + for (let i = 0; i < delegation_responses.length; i++) { + const delegation = delegation_responses[i]; + state.authz.chains[chainID].delegations.delegatedTo[ + delegation?.delegation?.validator_address + ] = true; + total += parseFloat(delegation?.delegation?.shares); + } + state.authz.chains[chainID].delegations.totalStaked = total; + } + }) + .addCase(getAuthzDelegations.rejected, (state, action) => { + state.authz.delegationsLoading--; + const chainID = action.meta?.arg?.chainID; + if (state.authz.chains[chainID]) { + state.authz.chains[chainID].delegations.status = TxStatus.REJECTED; + state.authz.chains[chainID].delegations.errMsg = + action.error.message || ''; + } + }); + builder .addCase(getPoolInfo.pending, (state, action) => { const { chainID } = action.meta.arg; @@ -928,11 +1414,13 @@ export const stakeSlice = createSlice({ builder .addCase(getUnbonding.pending, (state, action) => { + state.undelegationsLoading++; const { chainID } = action.meta.arg; state.chains[chainID].unbonding.status = TxStatus.PENDING; state.chains[chainID].unbonding.errMsg = ''; }) .addCase(getUnbonding.fulfilled, (state, action) => { + state.undelegationsLoading--; const { chainID } = action.meta.arg; const unbonding_responses = action.payload.data.unbonding_responses; let totalUnbonded = 0.0; @@ -943,6 +1431,7 @@ export const stakeSlice = createSlice({ }); }); state.chains[chainID].unbonding.totalUnbonded = totalUnbonded; + state.totalUndelegationsAmount += totalUnbonded; if (unbonding_responses[0].entries.length) { state.chains[chainID].unbonding.hasUnbonding = true; state.hasUnbonding = true; @@ -956,11 +1445,151 @@ export const stakeSlice = createSlice({ state.chains[chainID].unbonding.errMsg = ''; }) .addCase(getUnbonding.rejected, (state, action) => { + state.undelegationsLoading--; const { chainID } = action.meta.arg; state.chains[chainID].unbonding.status = TxStatus.REJECTED; state.chains[chainID].unbonding.errMsg = action.error.message || ''; }); + builder + .addCase(getAuthzUnbonding.pending, (state, action) => { + state.authz.undelegationsLoading++; + const { chainID } = action.meta.arg; + if (!state.authz.chains[chainID]) + state.authz.chains[chainID] = cloneDeep(state.defaultState); + state.authz.chains[chainID].unbonding.status = TxStatus.PENDING; + state.authz.chains[chainID].unbonding.errMsg = ''; + }) + .addCase(getAuthzUnbonding.fulfilled, (state, action) => { + state.authz.undelegationsLoading--; + const { chainID } = action.meta.arg; + const unbonding_responses = action.payload.data.unbonding_responses; + let totalUnbonded = 0.0; + if (unbonding_responses?.length) { + unbonding_responses.forEach((unbondingEntries) => { + unbondingEntries.entries.forEach((unbondingEntry) => { + totalUnbonded += +unbondingEntry.balance; + }); + }); + state.authz.chains[chainID].unbonding.totalUnbonded = totalUnbonded; + state.authz.totalUndelegationsAmount += totalUnbonded; + if (unbonding_responses[0].entries.length) { + state.authz.chains[chainID].unbonding.hasUnbonding = true; + state.authz.hasUnbonding = true; + } + } + state.authz.chains[chainID].unbonding.status = TxStatus.IDLE; + state.authz.chains[chainID].unbonding.unbonding.unbonding_responses = + unbonding_responses; + state.authz.chains[chainID].unbonding.pagination = + action.payload.data.pagination; + state.authz.chains[chainID].unbonding.errMsg = ''; + }) + .addCase(getAuthzUnbonding.rejected, (state, action) => { + state.authz.undelegationsLoading--; + const { chainID } = action.meta.arg; + state.authz.chains[chainID].unbonding.status = TxStatus.REJECTED; + state.authz.chains[chainID].unbonding.errMsg = + action.error.message || ''; + }); + + builder + .addCase(getValidator.pending, (state, action) => { + const chainID = action.meta?.arg?.chainID; + if (!state.chains[chainID]) + state.chains[chainID] = cloneDeep(initialState.defaultState); + state.chains[chainID].validator.status = TxStatus.PENDING; + const valoperAddress = action.meta?.arg?.valoperAddress; + state.chains[chainID].validator[valoperAddress] = TxStatus.PENDING; + state.chains[chainID].validator.errMsg = ''; + }) + .addCase(getValidator.fulfilled, (state, action) => { + const chainID = action.meta?.arg?.chainID; + state.chains[chainID].validator.status = TxStatus.IDLE; + state.chains[chainID].validator.validatorInfo = + action.payload.data.validator; + + const valoperAddress = action.meta?.arg?.valoperAddress; + state.chains[chainID].validator[valoperAddress] = + action?.payload?.data?.validator; + }) + .addCase(getValidator.rejected, (state, action) => { + const chainID = action.meta?.arg?.chainID; + state.chains[chainID].validator.status = TxStatus.REJECTED; + state.chains[chainID].validator.errMsg = ''; + const valoperAddress = action.meta?.arg?.valoperAddress; + state.chains[chainID].validator[valoperAddress] = TxStatus.REJECTED; + }); + + builder + .addCase(getAuthzValidator.pending, (state, action) => { + const chainID = action.meta?.arg?.chainID; + if (!state.authz.chains[chainID]) + state.authz.chains[chainID] = cloneDeep(initialState.defaultState); + state.authz.chains[chainID].validator.status = TxStatus.PENDING; + state.authz.chains[chainID].validator.errMsg = ''; + }) + .addCase(getAuthzValidator.fulfilled, (state, action) => { + const chainID = action.meta?.arg?.chainID; + state.authz.chains[chainID].validator.status = TxStatus.IDLE; + state.authz.chains[chainID].validator.validatorInfo = + action.payload.data.validator; + }) + .addCase(getAuthzValidator.rejected, (state, action) => { + const chainID = action.meta?.arg?.chainID; + state.authz.chains[chainID].validator.status = TxStatus.REJECTED; + state.authz.chains[chainID].validator.errMsg = ''; + }); + + builder + .addCase(getTotalDelegationsCount.pending, (state, action) => { + const { chainID, operatorAddress } = action.meta.arg; + state.chains[chainID].validatorProfiles = { + ...state.chains[chainID].validatorProfiles, + [operatorAddress]: { totalDelegators: 0 }, + }; + }) + .addCase(getTotalDelegationsCount.fulfilled, (state, action) => { + const { chainID, operatorAddress } = action.meta.arg; + state.chains[chainID].validatorProfiles = { + ...state.chains[chainID].validatorProfiles, + [operatorAddress]: { + totalDelegators: action.payload?.data?.pagination?.total || 0, + }, + }; + }) + .addCase(getTotalDelegationsCount.rejected, () => {}); + + builder + .addCase(getWitvalPolygonValidator.pending, () => {}) + .addCase(getWitvalPolygonValidator.fulfilled, (state, action) => { + state.witvalNonCosmosValidators.chains = { + ...state.witvalNonCosmosValidators.chains, + polygon: action.payload.data, + }; + }) + .addCase(getWitvalPolygonValidator.rejected, () => {}); + + builder + .addCase(getWitvalPolygonDelegatorsCount.pending, () => {}) + .addCase(getWitvalPolygonDelegatorsCount.fulfilled, (state, action) => { + state.witvalNonCosmosValidators.delegators = { + ...state.witvalNonCosmosValidators.delegators, + polygon: action.payload?.data?.summary?.total, + }; + }) + .addCase(getWitvalPolygonDelegatorsCount.rejected, () => {}); + + builder + .addCase(getWitvalOasisDelegations.pending, () => {}) + .addCase(getWitvalOasisDelegations.fulfilled, (state, action) => { + state.witvalNonCosmosValidators.delegators = { + ...state.witvalNonCosmosValidators.delegators, + oasis: action.payload?.data, + }; + }) + .addCase(getWitvalOasisDelegations.rejected, () => {}); + builder .addCase(txDelegate.pending, (state, action) => { const { chainID } = action.meta.arg.basicChainInfo; @@ -1053,6 +1682,12 @@ export const { resetDefaultState, resetRestakeTx, resetCancelUnbondingTx, + resetCompleteState, + resetAuthz, + resetAuthzDelegations, + setValidators, + setSearchQuery, + filterValidators, } = stakeSlice.actions; export default stakeSlice.reducer; diff --git a/frontend/src/store/features/staking/stakingService.ts b/frontend/src/store/features/staking/stakingService.ts index f7adccb3e..7eea8f99b 100644 --- a/frontend/src/store/features/staking/stakingService.ts +++ b/frontend/src/store/features/staking/stakingService.ts @@ -1,67 +1,148 @@ 'use client'; -import Axios, { AxiosResponse } from 'axios'; -import { convertPaginationToParams, cleanURL } from '../../../utils/util'; +import { AxiosResponse } from 'axios'; +import { + addChainIDParam, + convertPaginationToParams, +} from '../../../utils/util'; import { GetDelegationsResponse, GetParamsResponse, GetUnbondingResponse, GetValidatorsResponse, + Validator, } from '../../../types/staking'; -/* disable eslint*/ +import { axiosGetRequestWrapper } from '@/utils/RequestWrapper'; const validatorsURL = '/cosmos/staking/v1beta1/validators'; const delegationsURL = '/cosmos/staking/v1beta1/delegations/'; const unbondingDelegationsURL = (address: string) => `/cosmos/staking/v1beta1/delegators/${address}/unbonding_delegations`; +const validatorDelegationsURL = (operatorAddress: string) => + `/cosmos/staking/v1beta1/validators/${operatorAddress}/delegations?pagination.count_total=1`; +const validatorURL = (address: string) => + `/cosmos/staking/v1beta1/validators/${address}`; const paramsURL = '/cosmos/staking/v1beta1/params'; const poolURL = '/cosmos/staking/v1beta1/pool'; +const polygonValidatorURL = (id: number) => `/validators/${id}`; +const polygonDelegatorsURL = (id: number) => `/validators/${id}/delegators`; +const oasisDelegationsURL = (operatorAddress: string) => + `/mainnet/validator/delegators?address=${operatorAddress}&page=1&size=20`; + const fetchValidators = ( - baseURL: string, + baseURLs: string[], + chainID: string, status?: string, pagination?: KeyLimitPagination ): Promise> => { - let uri = `${cleanURL(baseURL)}${validatorsURL}`; + let endPoint = `${validatorsURL}`; const pageParams = convertPaginationToParams(pagination); if (status) { - uri += `?status=${status}`; - if (pageParams) uri += `&${pageParams}`; + endPoint += `?status=${status}`; + if (pageParams) endPoint += `&${pageParams}`; } else { - if (pageParams) uri += `?${pageParams}`; + if (pageParams) endPoint += `?${pageParams}`; } + endPoint = addChainIDParam(endPoint, chainID); - return Axios.get(uri); + return axiosGetRequestWrapper(baseURLs, endPoint); }; const fetchdelegations = ( - baseURL: string, + baseURLs: string[], address: string, + chainID: string, pagination: KeyLimitPagination ): Promise> => { - let uri = `${cleanURL(baseURL)}${delegationsURL}${address}`; + let endPoint = `${delegationsURL}${address}`; const pageParams = convertPaginationToParams(pagination); - if (pageParams !== '') uri += `?${pageParams}`; + if (pageParams !== '') { + endPoint += `?${pageParams}`; + } + endPoint = addChainIDParam(endPoint, chainID); - return Axios.get(uri); + return axiosGetRequestWrapper(baseURLs, endPoint); }; const fetchUnbonding = async ( - baseURL: string, - address: string + baseURLs: string[], + address: string, + chainID: string ): Promise> => { - const uri = `${baseURL}${unbondingDelegationsURL(address)}`; + let endPoint = `${unbondingDelegationsURL(address)}`; + endPoint = addChainIDParam(endPoint, chainID); - return Axios.get(uri); + return axiosGetRequestWrapper(baseURLs, endPoint); }; const fetchParams = ( - baseURL: string -): Promise> => - Axios.get(`${cleanURL(baseURL)}${paramsURL}`); + baseURLs: string[], + chainID: string +): Promise> => { + const endPoint = addChainIDParam(paramsURL, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); +}; + +const fetchPoolInfo = ( + baseURLs: string[], + chainID: string +): Promise => { + const endPoint = addChainIDParam(poolURL, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); +}; + +const fetchValidator = ( + baseURLs: string[], + address: string, + chainID: string +): Promise> => { + let endPoint = validatorURL(address); + endPoint = addChainIDParam(endPoint, chainID); + return axiosGetRequestWrapper(baseURLs, endPoint); +}; + +const fetchValidatorDelegations = async ( + baseURLs: string[], + operatorAddress: string, + chainID: string + /* eslint-disable @typescript-eslint/no-explicit-any */ +): Promise> => { + let endPoint = `${validatorDelegationsURL(operatorAddress)}`; + endPoint = addChainIDParam(endPoint, chainID); -const fetchPoolInfo = (baseURL: string): Promise => - Axios.get(`${cleanURL(baseURL)}${poolURL}`); + return axiosGetRequestWrapper(baseURLs, endPoint); +}; + +const fetchPolygonValidator = async ( + baseURL: string, + id: number + /* eslint-disable @typescript-eslint/no-explicit-any */ +): Promise> => { + const endPoint = `${polygonValidatorURL(id)}`; + + return axiosGetRequestWrapper([baseURL], endPoint); +}; + +const fetchPolygonDelegators = async ( + baseURL: string, + id: number + /* eslint-disable @typescript-eslint/no-explicit-any */ +): Promise> => { + const endPoint = `${polygonDelegatorsURL(id)}`; + + return axiosGetRequestWrapper([baseURL], endPoint); +}; + +const fetchOasisDelegations = async ( + baseURL: string, + operatorAddress: string + /* eslint-disable @typescript-eslint/no-explicit-any */ +): Promise> => { + const endPoint = `${oasisDelegationsURL(operatorAddress)}`; + + return axiosGetRequestWrapper([baseURL], endPoint); +}; const result = { validators: fetchValidators, @@ -69,6 +150,11 @@ const result = { unbonding: fetchUnbonding, params: fetchParams, poolInfo: fetchPoolInfo, + validatorInfo: fetchValidator, + validatorDelegations: fetchValidatorDelegations, + polygonValidator: fetchPolygonValidator, + polygonDelegators: fetchPolygonDelegators, + oasisDelegations: fetchOasisDelegations, }; export default result; diff --git a/frontend/src/store/features/swaps/swapsService.ts b/frontend/src/store/features/swaps/swapsService.ts new file mode 100644 index 000000000..555bf6d5c --- /dev/null +++ b/frontend/src/store/features/swaps/swapsService.ts @@ -0,0 +1,106 @@ +import { TxSwapServiceInputs } from '@/types/swaps'; +import { SQUID_CLIENT_API, SQUID_ID } from '@/utils/constants'; +import { GetRoute, Squid } from '@0xsquid/sdk'; +import { OfflineDirectSigner } from '@cosmjs/proto-signing'; +import { SigningStargateClient } from '@cosmjs/stargate'; +import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; + +const squidClient = new Squid(); +squidClient.setConfig({ + baseUrl: SQUID_CLIENT_API, + integratorId: SQUID_ID, +}); +squidClient.init(); + +export const txExecuteSwap = async ({ + route, + signer, + signerAddress, +}: TxSwapServiceInputs): Promise => { + try { + const tx = (await squidClient.executeRoute({ + signer: signer, + route: route, + signerAddress: signerAddress, + })) as TxRaw; + return tx; + } catch (error) { + throw error; + } +}; + +export const connectWithSigner = async ( + urls: string[], + offlineSigner: OfflineDirectSigner +) => { + for (const url of urls) { + try { + const signer = await SigningStargateClient.connectWithSigner( + url, + offlineSigner + ); + return signer; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + console.error(`Error connecting to ${url}: ${error.message}`); + } + } + throw new Error('Unable to connect to any RPC URLs'); +}; + +export const trackTransactionStatus = async ({ + transactionId, + fromChainId, + toChainId, +}: { + transactionId: string; + fromChainId: string; + toChainId: string; +}): Promise => { + const squid = new Squid(); + squid.setConfig({ + baseUrl: SQUID_CLIENT_API, + integratorId: SQUID_ID, + }); + await squid.init(); + + let status: string; + do { + const response = await squid.getStatus({ + transactionId, + integratorId: SQUID_ID, + fromChainId, + toChainId, + }); + status = response.squidTransactionStatus || ''; + if (status === 'ongoing') { + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + } while (status === 'ongoing'); + + return status; +}; + +export const connectStargateClient = async (urls: string[]) => { + for (const url of urls) { + try { + const client = await SigningStargateClient.connect(url); + return client; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + console.error(`Error connecting to ${url}: ${error.message}`); + } + } + throw new Error('Unable to connect to any RPC URLs'); +}; + +export const fetchSwapRoute = async (params: GetRoute) => { + try { + const res = await squidClient.getRoute(params); + return res; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (err: any) { + console.error(err.message); + throw new Error(err.message); + } +}; diff --git a/frontend/src/store/features/swaps/swapsSlice.ts b/frontend/src/store/features/swaps/swapsSlice.ts new file mode 100644 index 000000000..535d15ffe --- /dev/null +++ b/frontend/src/store/features/swaps/swapsSlice.ts @@ -0,0 +1,238 @@ +'use client'; + +import { TxStatus } from '@/types/enums'; +import { + AssetConfig, + ChainConfig, + SwapState, + TxSwapInputs, +} from '@/types/swaps'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { + connectStargateClient, + connectWithSigner, + trackTransactionStatus, + txExecuteSwap, +} from './swapsService'; +import { setError } from '../common/commonSlice'; +import { ERR_UNKNOWN } from '@/utils/errors'; +import { OfflineDirectSigner } from '@cosmjs/proto-signing'; +import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import { getBalances } from '../bank/bankSlice'; +import { trackEvent } from '@/utils/util'; +import { FAILED, SUCCESS } from '@/utils/constants'; + +declare let window: WalletWindow; + +const initialState: SwapState = { + destAsset: null, + destChain: null, + sourceAsset: null, + sourceChain: null, + amountIn: '', + amountOut: '', + fromAddress: '', + toAddress: '', + slippage: '1', + txStatus: { + status: TxStatus.INIT, + error: '', + }, + txSuccess: { + txHash: '', + }, + txDestSuccess: { + status: '', + msg: '', + }, + explorerEndpoint: '', +}; + +export const txIBCSwap = createAsyncThunk( + 'ibc-swap/txSwap', + async (data: TxSwapInputs, { rejectWithValue, dispatch }) => { + try { + dispatch(setExplorerEndpoint(data.explorerEndpoint)); + dispatch(resetTx()); + dispatch(resetTxDestSuccess()); + const offlineSigner: OfflineDirectSigner = + await window.wallet.getOfflineSigner(data.sourceChainID); + + const signerAddress = (await offlineSigner.getAccounts())[0].address; + + const signer = await connectWithSigner(data.rpcURLs, offlineSigner); + + const executionResponse = await txExecuteSwap({ + route: data.swapRoute, + signer, + signerAddress, + }); + + const client = await connectStargateClient(data.rpcURLs); + + const txResponse = await client.broadcastTx( + Uint8Array.from(TxRaw.encode(executionResponse).finish()) + ); + + dispatch(setTx(txResponse.transactionHash)); + + if (txResponse?.code === 0) { + trackEvent('TRANSFER', 'IBC_SWAP', SUCCESS); + dispatch( + getBalances({ + address: data.signerAddress, + baseURL: data.baseURLs[0], + baseURLs: data.baseURLs, + chainID: data.sourceChainID, + }) + ); + } else { + trackEvent('TRANSFER', 'IBC_SWAP', FAILED); + } + + const txStatus = await trackTransactionStatus({ + transactionId: txResponse.transactionHash, + toChainId: data.destChainID, + fromChainId: data.sourceChainID, + }); + + if (txStatus === 'success') { + dispatch( + setTxDestSuccess({ + msg: 'Transaction Successful', + status: 'success', + }) + ); + } else if (txStatus === 'needs_gas') { + dispatch( + setError({ + message: 'Transaction could not be completed, needs gas', + type: 'error', + }) + ); + } else if (txStatus === 'partial_success') { + dispatch( + setTxDestSuccess({ + msg: 'Transaction Partially Successful', + status: 'partial_success', + }) + ); + } else if (txStatus === 'not_found') { + dispatch( + setError({ + message: 'Transaction not found', + type: 'error', + }) + ); + } + + return txStatus; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + trackEvent('TRANSFER', 'IBC_SWAP', FAILED); + const errMsg = error?.message || ERR_UNKNOWN; + dispatch( + setError({ + message: errMsg, + type: 'error', + }) + ); + return rejectWithValue(error?.message || ERR_UNKNOWN); + } + } +); + +export const swapsSlice = createSlice({ + name: 'ibc-swap', + initialState, + reducers: { + setSourceChain: (state, action: PayloadAction) => { + state.sourceChain = action.payload; + }, + setSourceAsset: (state, action: PayloadAction) => { + state.sourceAsset = action.payload; + }, + setDestChain: (state, action: PayloadAction) => { + state.destChain = action.payload; + }, + setDestAsset: (state, action: PayloadAction) => { + state.destAsset = action.payload; + }, + setAmountIn: (state, action: PayloadAction) => { + state.amountIn = action.payload; + }, + setAmountOut: (state, action: PayloadAction) => { + state.amountOut = action.payload; + }, + setToAddress: (state, action: PayloadAction) => { + state.toAddress = action.payload; + }, + setFromAddress: (state, action: PayloadAction) => { + state.fromAddress = action.payload; + }, + setSlippage: (state, action: PayloadAction) => { + state.slippage = action.payload; + }, + setExplorerEndpoint: (state, action: PayloadAction) => { + state.explorerEndpoint = action.payload; + }, + resetTxStatus: (state) => { + state.txStatus = { + status: TxStatus.INIT, + error: '', + }; + }, + setTx: (state, action: PayloadAction) => { + state.txSuccess.txHash = action.payload; + }, + resetTx: (state) => { + state.txSuccess.txHash = ''; + }, + setTxDestSuccess: ( + state, + action: PayloadAction<{ status: string; msg: string }> + ) => { + state.txDestSuccess.status = action.payload.status; + state.txDestSuccess.msg = action.payload.msg; + }, + resetTxDestSuccess: (state) => { + state.txDestSuccess.status = ''; + state.txDestSuccess.msg = ''; + }, + }, + extraReducers: (builder) => { + builder + .addCase(txIBCSwap.pending, (state) => { + state.txStatus.status = TxStatus.PENDING; + state.txStatus.error = ''; + }) + .addCase(txIBCSwap.fulfilled, (state) => { + state.txStatus.status = TxStatus.IDLE; + state.txStatus.error = ''; + }) + .addCase(txIBCSwap.rejected, (state, action) => { + state.txStatus.status = TxStatus.REJECTED; + state.txStatus.error = action.error.message || ERR_UNKNOWN; + }); + }, +}); + +export const { + setDestAsset, + setDestChain, + setSourceAsset, + setSourceChain, + setAmountIn, + setAmountOut, + resetTxStatus, + resetTx, + setTx, + setTxDestSuccess, + resetTxDestSuccess, + setFromAddress, + setToAddress, + setSlippage, + setExplorerEndpoint, +} = swapsSlice.actions; + +export default swapsSlice.reducer; diff --git a/frontend/src/store/features/transactionHistory/transactionHistorySlice.ts b/frontend/src/store/features/transactionHistory/transactionHistorySlice.ts deleted file mode 100644 index 29353ed74..000000000 --- a/frontend/src/store/features/transactionHistory/transactionHistorySlice.ts +++ /dev/null @@ -1,105 +0,0 @@ -'use client'; - -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { - addTransactions as addTxsInLocalStorage, - getTransactions, - updateIBCStatus, -} from '@/utils/localStorage'; -import { trackTx } from '../ibc/ibcSlice'; - -type TransactionHistoryState = { - chains: { [chainID: string]: Transaction[] }; - allTransactions: Transaction[]; -}; - -const initialState: TransactionHistoryState = { - chains: {}, - allTransactions: [], -}; - -export const loadTransactions = createAsyncThunk( - 'transactions/load', - async (data: LoadTransactionsInputs, { dispatch }) => { - const transactions = getTransactions(data.address); - transactions.forEach((tx) => { - if (tx.isIBCPending) { - dispatch( - trackTx({ - chainID: tx.chainID, - txHash: tx.transactionHash, - cosmosAddress: data.address, - }) - ); - } - }); - return transactions; - } -); - -export const transactionHistorySlice = createSlice({ - name: 'transactionHistory', - initialState, - reducers: { - addTransactions: (state, action: PayloadAction) => { - const { transactions, chainID, address } = action.payload; - state.allTransactions = [ - ...transactions, - ...(state.allTransactions || []), - ]; - state.chains[chainID] = [ - ...transactions, - ...(state.chains[chainID] || []), - ]; - addTxsInLocalStorage(transactions, address); - }, - - updateIBCTransaction: ( - state, - action: PayloadAction - ) => { - const { txHash, chainID, address } = action.payload; - const allTransactions = state.allTransactions; - const updatedAllTransactions = allTransactions.map((tx) => { - if (tx.transactionHash === txHash) { - return { ...tx, isIBCPending: false }; - } - return tx; - }); - state.allTransactions = updatedAllTransactions; - - const chainTransactions = state.chains[chainID] || []; - const updatedChainTransactions = chainTransactions.map((tx) => { - if (tx.transactionHash === txHash) { - return { ...tx, isIBCPending: false }; - } - return tx; - }); - state.chains[chainID] = updatedChainTransactions; - - updateIBCStatus(address, txHash); - }, - }, - - extraReducers: (builder) => { - builder - .addCase(loadTransactions.pending, () => {}) - .addCase(loadTransactions.fulfilled, (state, action) => { - const transactions = action.payload; - state.allTransactions = transactions; - const chains: { [key: string]: Transaction[] } = {}; - transactions.forEach((tx) => { - const { chainID } = tx; - if (!chains[chainID]) chains[chainID] = []; - chains[chainID] = [...chains[chainID], tx]; - }); - state.chains = chains; - }) - .addCase(loadTransactions.rejected, () => {}); - }, -}); - -export const { addTransactions, updateIBCTransaction } = - transactionHistorySlice.actions; - -export default transactionHistorySlice.reducer; diff --git a/frontend/src/store/features/wallet/walletService.ts b/frontend/src/store/features/wallet/walletService.ts index f4d70d4cf..05fc7b035 100644 --- a/frontend/src/store/features/wallet/walletService.ts +++ b/frontend/src/store/features/wallet/walletService.ts @@ -14,6 +14,10 @@ export const isWalletInstalled = (walletName: string): boolean => { if (!window.cosmostation?.providers?.keplr) return false; window.wallet = window?.cosmostation?.providers?.keplr; return true; + case 'metamask': + if (!window.ethereum) return false; + window.wallet = window.ethereum; + return true; default: return false; } diff --git a/frontend/src/store/features/wallet/walletSlice.ts b/frontend/src/store/features/wallet/walletSlice.ts index a95387e19..205d8a6ad 100644 --- a/frontend/src/store/features/wallet/walletSlice.ts +++ b/frontend/src/store/features/wallet/walletSlice.ts @@ -1,11 +1,20 @@ 'use client'; -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { getWalletAmino } from '../../../txns/execute'; import { isWalletInstalled } from './walletService'; -import { setConnected, setWalletName } from '../../../utils/localStorage'; +import { + getFeegrantMode, + setConnected, + setWalletName, +} from '../../../utils/localStorage'; import { TxStatus } from '@/types/enums'; -import { loadTransactions } from '../transactionHistory/transactionHistorySlice'; import { setError } from '../common/commonSlice'; +import { getKey } from '@leapwallet/cosmos-snap-provider'; +import { getAddressByPrefix } from '@/utils/address'; +import { getAuthzMode } from '@/utils/localStorage'; +import { enableAuthzMode } from '../authz/authzSlice'; +import { enableFeegrantMode } from '../feegrant/feegrantSlice'; +import { NotSupportedMetamaskChainIds } from '@/utils/constants'; declare let window: WalletWindow; @@ -23,6 +32,7 @@ interface ChainInfo { interface WalletState { name: string; + connectWalletOpen: boolean; connected: boolean; isLoading: boolean; isNanoLedger: boolean; @@ -34,6 +44,7 @@ interface WalletState { const initialState: WalletState = { name: '', + connectWalletOpen: false, connected: false, isLoading: true, isNanoLedger: false, @@ -53,10 +64,8 @@ export const establishWalletConnection = createAsyncThunk( { rejectWithValue, fulfillWithValue, dispatch } ) => { const networks = data.networks; - if (!isWalletInstalled(data.walletName)) { dispatch(setError({ type: 'error', message: 'Wallet is not installed' })); - return rejectWithValue('wallet is not installed'); } else { window.wallet.defaultOptions = { @@ -78,7 +87,10 @@ export const establishWalletConnection = createAsyncThunk( let isNanoLedger = false; const chainInfos: Record = {}; const nameToChainIDs: Record = {}; + let anyNetworkAddress = ''; + for (let i = 0; i < networks.length; i++) { + const chainId = networks[i].config.chainId; try { if ( (data.walletName === 'keplr' || @@ -90,27 +102,33 @@ export const establishWalletConnection = createAsyncThunk( if (data.walletName === 'leap' && networks[i].leapExperimental) { await window.wallet.experimentalSuggestChain(networks[i].config); } - const chainId: string = networks[i].config.chainId; - const chainName: string = networks[i].config.chainName; await getWalletAmino(chainId); const walletInfo = await window.wallet.getKey(chainId); walletInfo.pubKey = Buffer.from(walletInfo?.pubKey).toString( 'base64' ); - delete walletInfo?.address; walletName = walletInfo?.name; isNanoLedger = walletInfo?.isNanoLedger || false; chainInfos[chainId] = { walletInfo: walletInfo, network: networks[i], }; - nameToChainIDs[chainName?.toLowerCase().split(' ').join('')] = - chainId; + if (anyNetworkAddress === '') + anyNetworkAddress = walletInfo?.bech32Address || ''; + nameToChainIDs[ + networks[i].config.chainName.toLowerCase().split(' ').join('') + ] = chainId; } catch (error) { console.log( `unable to connect to network ${networks[i].config.chainName}: `, error ); + dispatch( + setError({ + type: 'error', + message: `Unable to connect to network ${networks[i].config.chainName}`, + }) + ); } } @@ -121,19 +139,21 @@ export const establishWalletConnection = createAsyncThunk( message: 'Permission denied for all the networks', }) ); - return rejectWithValue('Permission denied for all the networks'); } else { setConnected(); setWalletName(data.walletName); - // todo: use Hex Address instead to avoid certain cases - dispatch( - loadTransactions({ - address: - chainInfos['cosmoshub-4']?.walletInfo?.bech32Address || 'Todo', - }) - ); + const cosmosAddress = getAddressByPrefix(anyNetworkAddress, 'cosmos'); + const authzMode = getAuthzMode(cosmosAddress); + if (authzMode.isAuthzModeOn) + dispatch(enableAuthzMode({ address: authzMode.authzAddress })); + const feegrantMode = getFeegrantMode(cosmosAddress); + if (feegrantMode.isFeegrantModeOn) + dispatch( + enableFeegrantMode({ address: feegrantMode.feegrantAddress }) + ); + return fulfillWithValue({ chainInfos, nameToChainIDs, @@ -145,6 +165,70 @@ export const establishWalletConnection = createAsyncThunk( } ); +export const establishMetamaskConnection = createAsyncThunk( + 'wallet/metamask-connection', + async ( + data: { + network: Network; + walletName: string; + }, + { rejectWithValue, dispatch } + ) => { + if (!isWalletInstalled(data.walletName)) { + dispatch(setError({ type: 'error', message: 'Wallet is not installed' })); + return rejectWithValue('wallet is not installed'); + } else { + window.wallet.defaultOptions = { + sign: { + preferNoSetMemo: true, + disableBalanceCheck: true, + }, + }; + const chainId = data.network.config.chainId; + try { + await window.wallet.enable(chainId); + } catch (error) { + console.log('caught', error); + } + + try { + if (NotSupportedMetamaskChainIds.indexOf(chainId) === -1) { + const walletInfo = await getKey(chainId); + const chainInfo: ChainInfo = { + walletInfo: { + algo: walletInfo?.algo, + bech32Address: walletInfo?.address, + pubKey: Buffer.from(walletInfo?.pubkey).toString('base64'), + isKeystone: '', + isNanoLedger: false, + name: walletInfo?.address || '', + }, + network: data.network, + }; + + setConnected(); + setWalletName(data.walletName); + dispatch(addChainInfo({ chainId, chainInfo })); + dispatch( + addNameToChainIDs({ + chainName: data.network.config.chainName + .toLowerCase() + .split(' ') + .join(''), + chainId, + }) + ); + } + } catch (error) { + console.log( + `unable to connect to network ${data.network.config.chainName}: `, + error + ); + } + } + } +); + const walletSlice = createSlice({ name: 'wallet', initialState, @@ -166,16 +250,53 @@ const walletSlice = createSlice({ resetConnectWalletStatus: (state) => { state.status = TxStatus.INIT; }, + setIsLoading: (state) => { + state.isLoading = true; + }, unsetIsLoading: (state) => { state.isLoading = false; }, + setConnectWalletOpen: (state, action: PayloadAction) => { + state.connectWalletOpen = action.payload; + }, + addChainInfo: ( + state, + action: PayloadAction<{ chainId: string; chainInfo: ChainInfo }> + ) => { + const { chainId, chainInfo } = action.payload; + state.networks = { ...state.networks, [chainId]: chainInfo }; + state.connected = true; + state.isLoading = false; + state.status = TxStatus.IDLE; + if (!state.name?.length) { + const cosmosAddress = getAddressByPrefix( + chainInfo.walletInfo.name || '', + 'cosmos' + ); + state.name = cosmosAddress; + } + }, + addNameToChainIDs: ( + state, + action: PayloadAction<{ chainName: string; chainId: string }> + ) => { + const { chainName, chainId } = action.payload; + state.nameToChainIDs = { ...state.nameToChainIDs, [chainName]: chainId }; + }, }, extraReducers: (builder) => { builder .addCase(establishWalletConnection.pending, (state) => { state.status = TxStatus.PENDING; + state.isLoading = true; }) .addCase(establishWalletConnection.fulfilled, (state, action) => { + if (!action.payload) { + state.connected = true; + state.status = TxStatus.IDLE; + state.isLoading = false; + return; + } const networks = action.payload.chainInfos; const nameToChainIDs = action.payload.nameToChainIDs; state.networks = networks; @@ -198,6 +319,10 @@ export const { resetWallet, resetConnectWalletStatus, unsetIsLoading, + setConnectWalletOpen, + setIsLoading, + addChainInfo, + addNameToChainIDs, } = walletSlice.actions; export default walletSlice.reducer; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 5550a6c9d..c3f5bd4c2 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -7,10 +7,15 @@ import commonSlice from './features/common/commonSlice'; import stakeSlice from './features/staking/stakeSlice'; import bankSlice from './features/bank/bankSlice'; import distributionSlice from './features/distribution/distributionSlice'; -import transactionHistorySlice from './features/transactionHistory/transactionHistorySlice'; import authSlice from './features/auth/authSlice'; import govSlice from './features/gov/govSlice'; import ibcSlice from './features/ibc/ibcSlice'; +import authzSlice from './features/authz/authzSlice'; +import feegrantSlice from './features/feegrant/feegrantSlice'; +import recentTransactionsSlice from './features/recent-transactions/recentTransactionsSlice'; +import multiopsSlice from './features/multiops/multiopsSlice'; +import swapsSlice from './features/swaps/swapsSlice'; +import cosmwasmSlice from './features/cosmwasm/cosmwasmSlice'; export const store = configureStore({ reducer: { @@ -22,8 +27,13 @@ export const store = configureStore({ auth: authSlice, distribution: distributionSlice, gov: govSlice, - transactionHistory: transactionHistorySlice, ibc: ibcSlice, + authz: authzSlice, + feegrant: feegrantSlice, + recentTransactions: recentTransactionsSlice, + multiops: multiopsSlice, + swaps: swapsSlice, + cosmwasm: cosmwasmSlice, }, }); diff --git a/frontend/src/txns/authz/exec.ts b/frontend/src/txns/authz/exec.ts new file mode 100644 index 000000000..37dfc27cc --- /dev/null +++ b/frontend/src/txns/authz/exec.ts @@ -0,0 +1,282 @@ +import { DelegationsPairs } from '@/types/distribution'; +import { MsgExec } from 'cosmjs-types/cosmos/authz/v1beta1/tx'; +import { MsgSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx'; +import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; +import { MsgWithdrawDelegatorReward } from 'cosmjs-types/cosmos/distribution/v1beta1/tx'; +import { VoteOption } from 'cosmjs-types/cosmos/gov/v1beta1/gov'; +import { MsgDeposit, MsgVote } from 'cosmjs-types/cosmos/gov/v1beta1/tx'; +import { + MsgBeginRedelegate, + MsgDelegate, + MsgUndelegate, +} from 'cosmjs-types/cosmos/staking/v1beta1/tx'; + +const msgSendTypeUrl = '/cosmos.bank.v1beta1.MsgSend'; +export const msgAuthzExecTypeUrl = '/cosmos.authz.v1beta1.MsgExec'; +const msgVote = '/cosmos.gov.v1beta1.MsgVote'; +const msgDeposit = '/cosmos.gov.v1beta1.MsgDeposit'; +const msgWithdrawRewards = + '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward'; +const msgDelegate = '/cosmos.staking.v1beta1.MsgDelegate'; +const msgUnDelegate = '/cosmos.staking.v1beta1.MsgUndelegate'; +const msgReDelegate = '/cosmos.staking.v1beta1.MsgBeginRedelegate'; + +export const serialize = () => { + return 'Executed an authz permission'; +}; + +export function AuthzExecSendMsg( + grantee: string, + from: string, + to: string, + amount: number, + denom: string +): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [ + { + typeUrl: msgSendTypeUrl, + value: MsgSend.encode({ + fromAddress: from, + toAddress: to, + amount: [ + { + denom: denom, + amount: String(amount), + }, + ], + }).finish(), + }, + ], + }), + }; +} + +export function AuthzExecVoteMsg( + grantee: string, + proposalId: number, + option: VoteOption, + granter: string +): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [ + { + typeUrl: msgVote, + value: MsgVote.encode({ + option: option, + proposalId: BigInt(proposalId), + voter: granter, + }).finish(), + }, + ], + }), + }; +} + +export function AuthzExecDepositMsg( + grantee: string, + proposalId: number, + granter: string, + amount: number, + denom: string +): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [ + { + typeUrl: msgDeposit, + value: MsgDeposit.encode({ + proposalId: BigInt(proposalId), + depositor: granter, + amount: [{ amount: '' + amount, denom }], + }).finish(), + }, + ], + }), + }; +} + +export function AuthzExecWithdrawRewardsMsg( + grantee: string, + + payload: DelegationsPairs[] +): Msg { + const msgs = []; + for (let i = 0; i < payload.length; i++) { + msgs.push({ + typeUrl: msgWithdrawRewards, + value: MsgWithdrawDelegatorReward.encode({ + delegatorAddress: payload[i].delegator, + validatorAddress: payload[i].validator, + }).finish(), + }); + } + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: msgs, + }), + }; +} + +export function AuthzExecDelegateMsg( + grantee: string, + granter: string, + validator: string, + amount: number, + denom: string +): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [ + { + typeUrl: msgDelegate, + value: MsgDelegate.encode({ + delegatorAddress: granter, + validatorAddress: validator, + amount: Coin.fromPartial({ + amount: String(amount), + denom: denom, + }), + }).finish(), + }, + ], + }), + }; +} + +export function AuthzExecReDelegateMsg( + grantee: string, + granter: string, + src: string, + dest: string, + amount: number, + denom: string +): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [ + { + typeUrl: msgReDelegate, + value: MsgBeginRedelegate.encode({ + validatorDstAddress: dest, + delegatorAddress: granter, + validatorSrcAddress: src, + amount: Coin.fromPartial({ + amount: String(amount), + denom: denom, + }), + }).finish(), + }, + ], + }), + }; +} + +export function AuthzExecUnDelegateMsg( + grantee: string, + granter: string, + validator: string, + amount: number, + denom: string +): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [ + { + typeUrl: msgUnDelegate, + value: MsgUndelegate.encode({ + validatorAddress: validator, + delegatorAddress: granter, + amount: Coin.fromPartial({ + amount: String(amount), + denom: denom, + }), + }).finish(), + }, + ], + }), + }; +} + +export function AuthzExecMsgRevoke(feegrant: Msg, grantee: string): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [feegrant], + }), + }; +} + +export function AuthzExecMsgFeegrant(feegrant: Msg, grantee: string): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [feegrant], + }), + }; +} + +export function AuthzExecMsgCancelUnbond(unbond: Msg, grantee: string): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: [unbond], + }), + }; +} + +// delegate written again in a different way +export function AuthzExecMsgRestake(delegations: Msg[], grantee: string): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: delegations, + }), + }; +} + +export function AuthzExecWithdrawRewardsAndCommissionMsg( + grantee: string, + msgs: Msg[] +): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: msgs, + }), + }; +} + +export function AuthzExecSetWithdrawAddressMsg( + grantee: string, + msgs: Msg[] +): Msg { + return { + typeUrl: msgAuthzExecTypeUrl, + value: MsgExec.fromPartial({ + grantee: grantee, + msgs: msgs, + }), + }; +} diff --git a/frontend/src/txns/authz/grant.ts b/frontend/src/txns/authz/grant.ts new file mode 100644 index 000000000..502fdf518 --- /dev/null +++ b/frontend/src/txns/authz/grant.ts @@ -0,0 +1,171 @@ +import { Timestamp } from 'cosmjs-types/google/protobuf/timestamp'; +import { SendAuthorization } from 'cosmjs-types/cosmos/bank/v1beta1/authz'; +import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; +import { MsgGrant } from 'cosmjs-types/cosmos/authz/v1beta1/tx'; +import { fromRfc3339WithNanoseconds } from '@cosmjs/tendermint-rpc'; +import { GenericAuthorization } from 'cosmjs-types/cosmos/authz/v1beta1/authz'; +import { + StakeAuthorization, + AuthorizationType, + StakeAuthorization_Validators, +} from 'cosmjs-types/cosmos/staking/v1beta1/authz'; + +export const msgAuthzGrantTypeUrl = '/cosmos.authz.v1beta1.MsgGrant'; + +export function AuthzSendGrantMsg( + granter: string, + grantee: string, + denom: string, + spendLimit: number, + expiration: string +): Msg { + const expWithNano = fromRfc3339WithNanoseconds(expiration); + const expSec = Math.floor(expWithNano.getTime() / 1000); + const expNano = + (expWithNano.getTime() % 1000) * 1000000 + (expWithNano.nanoseconds ?? 0); + const exp = Timestamp.fromPartial({ + nanos: expNano, + seconds: BigInt(expSec), + }); + + const sendAuthValue = SendAuthorization.encode( + SendAuthorization.fromPartial({ + spendLimit: [ + Coin.fromPartial({ + amount: String(spendLimit), + denom: denom, + }), + ], + }) + ).finish(); + const grantValue = MsgGrant.fromPartial({ + grant: { + authorization: { + typeUrl: '/cosmos.bank.v1beta1.SendAuthorization', + value: sendAuthValue, + }, + expiration: exp, + }, + grantee: grantee, + granter: granter, + }); + + return { + typeUrl: msgAuthzGrantTypeUrl, + value: grantValue, + }; +} + +export function AuthzGenericGrantMsg( + granter: string, + grantee: string, + typeURL: string, + expiration: string +): Msg { + const expWithNano = fromRfc3339WithNanoseconds(expiration); + const expSec = Math.floor(expWithNano.getTime() / 1000); + const expNano = + (expWithNano.getTime() % 1000) * 1000000 + (expWithNano.nanoseconds ?? 0); + const exp = Timestamp.fromPartial({ + nanos: expNano, + seconds: BigInt(expSec), + }); + + return { + typeUrl: msgAuthzGrantTypeUrl, + value: { + grant: { + authorization: { + typeUrl: '/cosmos.authz.v1beta1.GenericAuthorization', + value: GenericAuthorization.encode( + GenericAuthorization.fromPartial({ + msg: typeURL, + }) + ).finish(), + }, + expiration: exp, + }, + grantee: grantee, + granter: granter, + }, + }; +} + +export function AuthzStakeGrantMsg({ + expiration, + grantee, + granter, + allowList, + denyList, + maxTokens, + denom, + stakeAuthzType, +}: { + granter: string; + grantee: string; + expiration: string; + allowList?: string[]; + denyList?: string[]; + maxTokens?: string; + denom?: string; + stakeAuthzType: AuthorizationType; +}): Msg { + const expWithNano = fromRfc3339WithNanoseconds(expiration); + const expSec = Math.floor(expWithNano.getTime() / 1000); + const expNano = + (expWithNano.getTime() % 1000) * 1000000 + (expWithNano.nanoseconds ?? 0); + const exp = Timestamp.fromPartial({ + nanos: expNano, + seconds: BigInt(expSec), + }); + + const allow_list = StakeAuthorization_Validators.encode( + StakeAuthorization_Validators.fromPartial({ + address: allowList, + }) + ).finish(); + const deny_list = StakeAuthorization_Validators.encode( + StakeAuthorization_Validators.fromPartial({ + address: denyList, + }) + ).finish(); + const stakeAuthValue = StakeAuthorization.encode( + StakeAuthorization.fromPartial({ + authorizationType: stakeAuthzType, + allowList: allowList?.length + ? StakeAuthorization_Validators.decode(allow_list) + : undefined, + denyList: denyList?.length + ? StakeAuthorization_Validators.decode(deny_list) + : undefined, + maxTokens: maxTokens + ? Coin.fromPartial({ + amount: maxTokens, + denom: denom, + }) + : undefined, + }) + ).finish(); + const grantValue = MsgGrant.fromPartial({ + grant: { + authorization: { + typeUrl: '/cosmos.staking.v1beta1.StakeAuthorization', + value: stakeAuthValue, + }, + expiration: exp, + }, + grantee: grantee, + granter: granter, + }); + + return { + typeUrl: msgAuthzGrantTypeUrl, + value: grantValue, + }; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function serializeMsgGrantAuthz(msg: any) { + const { grantee } = msg; + return `Granted authz permission to ${grantee}`; +} diff --git a/frontend/src/txns/authz/index.ts b/frontend/src/txns/authz/index.ts new file mode 100644 index 000000000..fb2a78cfc --- /dev/null +++ b/frontend/src/txns/authz/index.ts @@ -0,0 +1,10 @@ +export { AuthzGenericGrantMsg, AuthzSendGrantMsg } from "./grant"; +export { + AuthzExecDelegateMsg, + AuthzExecReDelegateMsg, + AuthzExecSendMsg, + AuthzExecUnDelegateMsg, + AuthzExecVoteMsg, + AuthzExecWithdrawRewardsMsg, +} from './exec'; +export { AuthzRevokeMsg } from './revoke'; diff --git a/frontend/src/txns/authz/revoke.ts b/frontend/src/txns/authz/revoke.ts new file mode 100644 index 000000000..1664f2c0c --- /dev/null +++ b/frontend/src/txns/authz/revoke.ts @@ -0,0 +1,18 @@ +import { MsgRevoke } from 'cosmjs-types/cosmos/authz/v1beta1/tx'; + +export const msgAuthzRevokeTypeUrl = '/cosmos.authz.v1beta1.MsgRevoke'; + +export function AuthzRevokeMsg( + granter: string, + grantee: string, + typeURL: string +): Msg { + return { + typeUrl: msgAuthzRevokeTypeUrl, + value: MsgRevoke.fromPartial({ + msgTypeUrl: typeURL, + grantee: grantee, + granter: granter, + }), + }; +} diff --git a/frontend/src/txns/bank/send.ts b/frontend/src/txns/bank/send.ts index 27189715b..db1f98027 100644 --- a/frontend/src/txns/bank/send.ts +++ b/frontend/src/txns/bank/send.ts @@ -1,5 +1,5 @@ import { parseBalance } from '@/utils/denom'; -import { formatNumber } from '@/utils/util'; +import { formatNumber, shortenAddress } from '@/utils/util'; import { MsgSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx'; export const msgSendTypeUrl: string = '/cosmos.bank.v1beta1.MsgSend'; @@ -29,12 +29,29 @@ export function serialize(msg: Msg): string { return `Sent ${amount[0].amount} ${amount[0].denom} to ${toAddress}`; } +/* eslint-disable @typescript-eslint/no-explicit-any */ export function formattedSerialize( + msg: any, + decimals: number, + originalDenom: string, + pastTense?: boolean +) { + const { to_address, amount } = msg; + const parsedAmount = parseBalance(amount, decimals, amount[0].denom); + return `${pastTense ? 'Sent' : 'Send'} ${formatNumber( + parsedAmount + )} ${originalDenom} to ${to_address}`; +} + +export function formatSendMessage( msg: Msg, decimals: number, - originalDenom: string + originalDenom: string, + pastTense?: boolean ) { const { toAddress, amount } = msg.value; const parsedAmount = parseBalance(amount, decimals, amount[0].denom); - return `Send ${formatNumber(parsedAmount)} ${originalDenom} to ${toAddress}`; + return `${pastTense ? 'Sent' : 'Send'} ${formatNumber( + parsedAmount + )} ${originalDenom} to ${shortenAddress(toAddress, 15)}`; } diff --git a/frontend/src/txns/distribution/setWithdrawAddress.ts b/frontend/src/txns/distribution/setWithdrawAddress.ts new file mode 100644 index 000000000..0bf5c3760 --- /dev/null +++ b/frontend/src/txns/distribution/setWithdrawAddress.ts @@ -0,0 +1,30 @@ +import { MsgSetWithdrawAddress } from 'cosmjs-types/cosmos/distribution/v1beta1/tx'; + +export const msgSetWithdrawAddress = + '/cosmos.distribution.v1beta1.MsgSetWithdrawAddress'; + +export function SetWithdrawAddressMsg( + delegatorAddress: string, + withdrawAddress: string +): Msg { + return { + typeUrl: msgSetWithdrawAddress, + value: MsgSetWithdrawAddress.fromPartial({ + delegatorAddress: delegatorAddress, + withdrawAddress: withdrawAddress, + }), + }; +} + +export function EncodedSetWithdrawAddressMsg( + delegatorAddress: string, + withdrawAddress: string +): Msg { + return { + typeUrl: msgSetWithdrawAddress, + value: MsgSetWithdrawAddress.encode({ + delegatorAddress: delegatorAddress, + withdrawAddress: withdrawAddress, + }).finish(), + }; +} diff --git a/frontend/src/txns/distribution/withDrawRewards.ts b/frontend/src/txns/distribution/withDrawRewards.ts index 3ef591d0c..b84104690 100644 --- a/frontend/src/txns/distribution/withDrawRewards.ts +++ b/frontend/src/txns/distribution/withDrawRewards.ts @@ -17,10 +17,21 @@ export function WithdrawAllRewardsMsg( }; } -export function serialize(msg: Msg): string { - const { delegatorAddress, validatorAddress } = msg.value; - return `${shortenMsg( - delegatorAddress, - 10 - )} withdrew rewards from ${shortenMsg(validatorAddress, 10)}`; +export function EncodedWithdrawAllRewardsMsg( + delegator: string, + validator: string +): Msg { + return { + typeUrl: msgWithdrawRewards, + value: MsgWithdrawDelegatorReward.encode({ + delegatorAddress: delegator, + validatorAddress: validator, + }).finish(), + }; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function serialize(msg: any): string { + const validatorAddress = msg?.validator_address; + return `Withdrew rewards from ${shortenMsg(validatorAddress, 10)}`; } diff --git a/frontend/src/txns/distribution/withDrawValidatorCommission.ts b/frontend/src/txns/distribution/withDrawValidatorCommission.ts new file mode 100644 index 000000000..bca27a2e0 --- /dev/null +++ b/frontend/src/txns/distribution/withDrawValidatorCommission.ts @@ -0,0 +1,22 @@ +import { MsgWithdrawValidatorCommission } from 'cosmjs-types/cosmos/distribution/v1beta1/tx'; + +export const msgWithdrawValidatorCommission = + '/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission'; + +export function WithdrawValidatorCommissionMsg(validator: string): Msg { + return { + typeUrl: msgWithdrawValidatorCommission, + value: MsgWithdrawValidatorCommission.fromPartial({ + validatorAddress: validator, + }), + }; +} + +export function EncodedWithdrawValidatorCommissionMsg(validator: string): Msg { + return { + typeUrl: msgWithdrawValidatorCommission, + value: MsgWithdrawValidatorCommission.encode({ + validatorAddress: validator, + }).finish(), + }; +} diff --git a/frontend/src/txns/execute.ts b/frontend/src/txns/execute.ts index f455ce5fa..5b5db6268 100644 --- a/frontend/src/txns/execute.ts +++ b/frontend/src/txns/execute.ts @@ -9,6 +9,8 @@ import { import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; import { GeneratedType, Registry } from '@cosmjs/proto-signing'; import { GAS_FEE } from '../utils/constants'; +import { CosmjsOfflineSigner } from '@leapwallet/cosmos-snap-provider'; +import { isMetaMaskWallet } from '@/utils/localStorage'; declare let window: WalletWindow; @@ -85,10 +87,18 @@ export async function getWalletAmino( ): Promise< [OfflineAminoSigner, { address: string; algo: string; pubKey: Uint32Array }] > { - await window.wallet.enable(chainID); - const offlineSigner = window.wallet.getOfflineSignerOnlyAmino(chainID); - const accounts = await offlineSigner.getAccounts(); - return [offlineSigner, accounts[0]]; + if (isMetaMaskWallet()) { + await window.ethereum.enable(chainID); + const offlineSigner = new CosmjsOfflineSigner(chainID) + const accounts = await offlineSigner.getAccounts(); + return [offlineSigner, { address: accounts[0].address, algo: accounts[0].algo, pubKey: new Uint32Array(accounts[0].pubkey) }]; + } else { + await window.wallet.enable(chainID); + const offlineSigner = window.wallet.getOfflineSignerOnlyAmino(chainID); + const accounts = await offlineSigner.getAccounts(); + return [offlineSigner, accounts[0]]; + } + } export async function getWalletDirect( diff --git a/frontend/src/txns/feegrant/grant.ts b/frontend/src/txns/feegrant/grant.ts new file mode 100644 index 000000000..bb0870e39 --- /dev/null +++ b/frontend/src/txns/feegrant/grant.ts @@ -0,0 +1,318 @@ +import { Timestamp } from 'cosmjs-types/google/protobuf/timestamp'; +import { Duration } from 'cosmjs-types/google/protobuf/duration'; +import { fromRfc3339WithNanoseconds } from '@cosmjs/tendermint-rpc'; +import { + BasicAllowance, + PeriodicAllowance, + AllowedMsgAllowance, +} from 'cosmjs-types/cosmos/feegrant/v1beta1/feegrant'; +import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; +import { MsgGrantAllowance } from 'cosmjs-types/cosmos/feegrant/v1beta1/tx'; + +export const msgFeegrantGrantTypeUrl = + '/cosmos.feegrant.v1beta1.MsgGrantAllowance'; + +export function FeegrantBasicMsg( + granter: string, + grantee: string, + denom: string, + spendLimit?: string, + expiration?: string, + isAuthzMode?: boolean +): Msg { + let exp: Timestamp | undefined; + if (expiration) { + const expWithNano = fromRfc3339WithNanoseconds(expiration); + const expSec = Math.floor(expWithNano.getTime() / 1000); + const expNano = + (expWithNano.getTime() % 1000) * 1000000 + (expWithNano.nanoseconds ?? 0); + exp = Timestamp.fromPartial({ + nanos: expNano, + seconds: BigInt(expSec), + }); + } + + const basicValue = BasicAllowance.encode( + BasicAllowance.fromPartial({ + spendLimit: + spendLimit === null + ? undefined + : [ + Coin.fromPartial({ + amount: String(spendLimit), + denom: denom, + }), + ], + expiration: exp, + }) + ).finish(); + + if (isAuthzMode) { + return { + typeUrl: msgFeegrantGrantTypeUrl, + value: MsgGrantAllowance.encode({ + allowance: { + typeUrl: '/cosmos.feegrant.v1beta1.BasicAllowance', + value: basicValue, + }, + grantee: grantee, + granter: granter, + }).finish(), + }; + } + return { + typeUrl: msgFeegrantGrantTypeUrl, + value: MsgGrantAllowance.fromPartial({ + allowance: { + typeUrl: '/cosmos.feegrant.v1beta1.BasicAllowance', + value: basicValue, + }, + grantee: grantee, + granter: granter, + }), + }; +} + +export function FeegrantPeriodicMsg( + granter: string, + grantee: string, + denom: string, + spendLimit: number, + period: number, + periodSpendLimit: number, + expiration?: string, + isAuthzMode?: boolean +) { + try { + const now = new Date(); + + let exp: Timestamp | undefined; + try { + if (expiration) { + const expWithNano = fromRfc3339WithNanoseconds(expiration); + const expSec = Math.floor(expWithNano.getTime() / 1000); + const expNano = + (expWithNano.getTime() % 1000) * 1000000 + + (expWithNano.nanoseconds ?? 0); + exp = Timestamp.fromPartial({ + nanos: expNano, + seconds: BigInt(expSec), + }); + } + } catch (error) { + console.log('Error while expiration set', error); + } + + const periodDuration = Duration.fromPartial({ + nanos: period, + seconds: BigInt(period), + }); + + const basicValue = BasicAllowance.fromPartial({ + expiration: exp, + spendLimit: + spendLimit === undefined + ? undefined + : [ + Coin.fromPartial({ + amount: String(spendLimit), + denom: denom, + }), + ], + }); + + const periodicValue = PeriodicAllowance.encode({ + basic: basicValue, + period: periodDuration, + periodReset: Timestamp.fromPartial({ + nanos: (now.getTime() % 1000) * 1000000 + periodDuration.nanos, + seconds: + BigInt(Math.floor(now.getTime() / 1000)) + periodDuration.seconds, + }), + periodCanSpend: [ + Coin.fromPartial({ + amount: String(periodSpendLimit), + denom: denom, + }), + ], + periodSpendLimit: [ + Coin.fromPartial({ + amount: String(periodSpendLimit), + denom: denom, + }), + ], + }).finish(); + + if (isAuthzMode) { + return { + typeUrl: msgFeegrantGrantTypeUrl, + value: MsgGrantAllowance.encode({ + allowance: { + typeUrl: '/cosmos.feegrant.v1beta1.PeriodicAllowance', + value: periodicValue, + }, + grantee: grantee, + granter: granter, + }).finish(), + }; + } + + return { + typeUrl: msgFeegrantGrantTypeUrl, + value: MsgGrantAllowance.fromPartial({ + allowance: { + typeUrl: '/cosmos.feegrant.v1beta1.PeriodicAllowance', + value: periodicValue, + }, + grantee: grantee, + granter: granter, + }), + }; + } catch (error) { + console.log('error while creating periodic allowance', error); + throw error; + } +} + +export function FeegrantFilterMsg( + granter: string, + grantee: string, + denom: string, + spendLimit: number, + period: number, + periodSpendLimit: number, + expiration?: string, + txMsg?: Array, + allowanceType?: string, + isAuthzMode?: boolean +) { + console.log(granter); + console.log(grantee); + console.log(denom); + console.log(spendLimit); + console.log(period); + console.log(periodSpendLimit); + console.log(expiration); + console.log(txMsg); + console.log(allowanceType); + try { + const now = new Date(); + + let exp: Timestamp | undefined; + try { + if (expiration) { + const expWithNano = fromRfc3339WithNanoseconds(expiration); + const expSec = Math.floor(expWithNano.getTime() / 1000); + const expNano = + (expWithNano.getTime() % 1000) * 1000000 + + (expWithNano.nanoseconds ?? 0); + exp = Timestamp.fromPartial({ + nanos: expNano, + seconds: BigInt(expSec), + }); + } + } catch (error) { + console.log('Error while expiration set', error); + } + + const periodDuration = Duration.fromPartial({ + nanos: period, + seconds: BigInt(period), + }); + + const basicValue = BasicAllowance.fromPartial({ + expiration: exp, + spendLimit: + spendLimit === undefined + ? undefined + : [ + Coin.fromPartial({ + amount: String(spendLimit), + denom: denom, + }), + ], + }); + + const periodicValue = PeriodicAllowance.encode({ + basic: basicValue, + period: periodDuration, + periodReset: Timestamp.fromPartial({ + nanos: (now.getTime() % 1000) * 1000000 + periodDuration.nanos, + seconds: + BigInt(Math.floor(now.getTime() / 1000)) + periodDuration.seconds, + }), + periodCanSpend: [ + Coin.fromPartial({ + amount: String(periodSpendLimit), + denom: denom, + }), + ], + periodSpendLimit: [ + Coin.fromPartial({ + amount: String(periodSpendLimit), + denom: denom, + }), + ], + }).finish(); + + let value, typeURL; + let obj; + + if (allowanceType === 'Basic') { + value = basicValue; + typeURL = '/cosmos.feegrant.v1beta1.BasicAllowance'; + + obj = AllowedMsgAllowance.encode({ + allowance: { + typeUrl: '/cosmos.feegrant.v1beta1.BasicAllowance', + value: BasicAllowance.encode(basicValue).finish(), + }, + allowedMessages: txMsg || [], + }).finish(); + } else { + value = periodicValue; + typeURL = '/cosmos.feegrant.v1beta1.PeriodicAllowance'; + obj = AllowedMsgAllowance.encode({ + allowance: { + typeUrl: typeURL, + value: value, + }, + allowedMessages: txMsg || [], + }).finish(); + } + + if (isAuthzMode) { + return { + typeUrl: msgFeegrantGrantTypeUrl, + value: MsgGrantAllowance.encode({ + allowance: { + typeUrl: '/cosmos.feegrant.v1beta1.AllowedMsgAllowance', + value: obj, + }, + grantee: grantee, + granter: granter, + }).finish(), + }; + } + return { + typeUrl: msgFeegrantGrantTypeUrl, + value: MsgGrantAllowance.fromPartial({ + allowance: { + typeUrl: '/cosmos.feegrant.v1beta1.AllowedMsgAllowance', + value: obj, + }, + grantee: grantee, + granter: granter, + }), + }; + } catch (error) { + console.log('error while creating periodic allowance', error); + throw error; + } +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function serializeMsgGrantAllowance(msg: any) { + const { grantee } = msg; + return `Granted allowance to ${grantee}`; +} diff --git a/frontend/src/txns/feegrant/index.ts b/frontend/src/txns/feegrant/index.ts new file mode 100644 index 000000000..f1002f2bb --- /dev/null +++ b/frontend/src/txns/feegrant/index.ts @@ -0,0 +1,2 @@ +export { FeegrantBasicMsg, FeegrantPeriodicMsg } from './grant'; +export { FeegrantRevokeMsg } from './revoke'; diff --git a/frontend/src/txns/feegrant/revoke.ts b/frontend/src/txns/feegrant/revoke.ts new file mode 100644 index 000000000..89ca4b9cd --- /dev/null +++ b/frontend/src/txns/feegrant/revoke.ts @@ -0,0 +1,13 @@ +import { MsgRevokeAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx"; + +export const revokeTypeUrl = "/cosmos.feegrant.v1beta1.MsgRevokeAllowance"; + +export function FeegrantRevokeMsg(granter: string, grantee: string): Msg { + return { + typeUrl: revokeTypeUrl, + value: MsgRevokeAllowance.fromPartial({ + grantee: grantee, + granter: granter, + }), + }; +} diff --git a/frontend/src/txns/gov/deposit.ts b/frontend/src/txns/gov/deposit.ts index b10b10d34..db4861456 100644 --- a/frontend/src/txns/gov/deposit.ts +++ b/frontend/src/txns/gov/deposit.ts @@ -1,6 +1,8 @@ +import { parseBalance } from '@/utils/denom'; +import { formatNumber } from '@/utils/util'; import { MsgDeposit } from 'cosmjs-types/cosmos/gov/v1beta1/tx'; -const msgDeposit = '/cosmos.gov.v1beta1.MsgDeposit'; +export const msgDepositTypeUrl = '/cosmos.gov.v1beta1.MsgDeposit'; export function GovDepositMsg( proposalId: number, @@ -9,7 +11,7 @@ export function GovDepositMsg( denom: string ): Msg { return { - typeUrl: msgDeposit, + typeUrl: msgDepositTypeUrl, value: MsgDeposit.fromPartial({ depositor: depositer, proposalId: BigInt(proposalId), @@ -22,3 +24,14 @@ export function GovDepositMsg( }), }; } + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function serializeMsgDeposit( + msg: any, + decimals: number, + originalDenom: string +) { + const { proposal_id, amount } = msg; + const parsedAmount = parseBalance(amount, decimals, amount[0].denom); + return `Deposited ${formatNumber(parsedAmount)} ${originalDenom} on proposal #${proposal_id}`; +} diff --git a/frontend/src/txns/gov/vote.ts b/frontend/src/txns/gov/vote.ts index ef35a50a7..cbb81ac33 100644 --- a/frontend/src/txns/gov/vote.ts +++ b/frontend/src/txns/gov/vote.ts @@ -1,7 +1,7 @@ import { VoteOption } from 'cosmjs-types/cosmos/gov/v1beta1/gov'; import { MsgVote } from 'cosmjs-types/cosmos/gov/v1beta1/tx'; -const msgVote = '/cosmos.gov.v1beta1.MsgVote'; +export const msgVoteTypeUrl = '/cosmos.gov.v1beta1.MsgVote'; export function GovVoteMsg( proposalId: number, @@ -9,7 +9,7 @@ export function GovVoteMsg( option: VoteOption ): Msg { return { - typeUrl: msgVote, + typeUrl: msgVoteTypeUrl, value: MsgVote.fromPartial({ voter: voter, option: option, @@ -17,3 +17,17 @@ export function GovVoteMsg( }), }; } + +const voteOptions: Record = { + VOTE_OPTION_YES: 'Yes', + VOTE_OPTION_ABSTAIN: 'Abstain', + VOTE_OPTION_NO: 'No', + VOTE_OPTION_NO_WITH_VETO: 'NoWithVeto', + VOTE_OPTION_UNSPECIFIED: '', +}; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function serializeMsgVote(msg: any) { + const { option, proposal_id } = msg; + return `Voted ${voteOptions?.[option] || '-'} on proposal #${proposal_id}`; +} diff --git a/frontend/src/txns/ibc/transfer.ts b/frontend/src/txns/ibc/transfer.ts index b94f1a476..eccc678c2 100644 --- a/frontend/src/txns/ibc/transfer.ts +++ b/frontend/src/txns/ibc/transfer.ts @@ -1,8 +1,16 @@ -import { formatNumber } from "@/utils/util"; +import { formatNumber, parseDenomAmount } from '@/utils/util'; export const msgTransfer = '/ibc.applications.transfer.v1.MsgTransfer'; -export function serialize(msg: Msg): string { - const { receiver, token } = msg.value; - return `Transfer ${formatNumber(+token?.amount || 0)} ${token?.denom} to ${receiver}`; +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function serialize( + msg: any, + decimals: number, + originalDenom: string +): string { + const receiver = msg?.receiver; + const token = msg?.token; + return `Transfer ${formatNumber( + parseDenomAmount(token?.amount || '0', decimals) + )} ${originalDenom} to ${receiver}`; } diff --git a/frontend/src/txns/staking/delegate.ts b/frontend/src/txns/staking/delegate.ts index aa80c7167..b596b2ec5 100644 --- a/frontend/src/txns/staking/delegate.ts +++ b/frontend/src/txns/staking/delegate.ts @@ -1,4 +1,4 @@ -import { formatAmount, shortenMsg } from '@/utils/util'; +import { formatNumber, parseDenomAmount, shortenMsg } from '@/utils/util'; import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; import { MsgDelegate } from 'cosmjs-types/cosmos/staking/v1beta1/tx'; @@ -23,11 +23,33 @@ export function Delegate( }; } -export function serialize(msg: Msg): string { - const delegatorAddress = msg.value.delegatorAddress; - const validatorAddress = msg.value.validatorAddress; - const amount = msg.value.amount; - return `${shortenMsg(delegatorAddress, 10)} delegated ${formatAmount( - +amount?.amount || 0 - )} ${amount.denom} to ${shortenMsg(validatorAddress, 10)}`; +export function EncodeDelegate( + delegator: string, + validator: string, + amount: number, + denom: string +): Msg { + return { + typeUrl: msgDelegate, + value: MsgDelegate.encode({ + delegatorAddress: delegator, + validatorAddress: validator, + amount: Coin.fromPartial({ + amount: String(amount), + denom: denom, + }), + }).finish(), + }; +} + +export function serialize( + /* eslint-disable @typescript-eslint/no-explicit-any */ + msg: any, + decimals: number, + originalDenom: string +): string { + const amount = msg?.amount; + const validatorAddress = msg?.validator_address; + return `Delegated ${formatNumber(parseDenomAmount(amount?.amount || '0', decimals))} + ${originalDenom} to ${shortenMsg(validatorAddress, 10)}`; } diff --git a/frontend/src/txns/staking/redelegate.ts b/frontend/src/txns/staking/redelegate.ts index 66983397f..ce1291a08 100644 --- a/frontend/src/txns/staking/redelegate.ts +++ b/frontend/src/txns/staking/redelegate.ts @@ -1,4 +1,4 @@ -import { shortenMsg } from '@/utils/util'; +import { formatNumber, parseDenomAmount, shortenMsg } from '@/utils/util'; import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; import { MsgBeginRedelegate } from 'cosmjs-types/cosmos/staking/v1beta1/tx'; @@ -25,13 +25,21 @@ export function Redelegate( }; } -export function serialize(msg: Msg): string { - const { delegatorAddress, validatorSrcAddress, validatorDstAddress, amount } = - msg.value; - return `${shortenMsg(delegatorAddress, 10)} re-delegated ${ - amount.amount - } ${amount.denom} to ${shortenMsg( - validatorDstAddress, +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function serialize( + msg: any, + decimals: number, + originalDenom: string +): string { + const { + validator_src_address = '', + validator_dst_address = '', + amount, + } = msg; + return `Re-delegated ${formatNumber( + parseDenomAmount(amount.amount, decimals) + )} ${originalDenom} to ${shortenMsg(validator_dst_address, 10)} from ${shortenMsg( + validator_src_address, 10 - )} from ${shortenMsg(validatorSrcAddress, 10)}`; + )}`; } diff --git a/frontend/src/txns/staking/unbonding.ts b/frontend/src/txns/staking/unbonding.ts index 15bde0857..35348132a 100644 --- a/frontend/src/txns/staking/unbonding.ts +++ b/frontend/src/txns/staking/unbonding.ts @@ -1,14 +1,15 @@ import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; import { MsgCancelUnbondingDelegation } from 'cosmjs-types/cosmos/staking/v1beta1/tx'; -export const msgUnbonding = '/cosmos.staking.v1beta1.MsgCancelUnbondingDelegation'; +export const msgUnbonding = + '/cosmos.staking.v1beta1.MsgCancelUnbondingDelegation'; export function Unbonding( delegator: string, validator: string, amount: number, denom: string, - creationHeight: string, + creationHeight: string ): Msg { return { typeUrl: msgUnbonding, @@ -23,3 +24,24 @@ export function Unbonding( }), }; } + +export function UnbondingEncode( + delegator: string, + validator: string, + amount: number, + denom: string, + creationHeight: string +): Msg { + return { + typeUrl: msgUnbonding, + value: MsgCancelUnbondingDelegation.encode({ + delegatorAddress: delegator, + validatorAddress: validator, + amount: Coin.fromPartial({ + amount: String(amount), + denom: denom, + }), + creationHeight: BigInt(creationHeight), + }).finish(), + }; +} diff --git a/frontend/src/txns/staking/undelegate.ts b/frontend/src/txns/staking/undelegate.ts index 4cf0bcea2..59c802d1e 100644 --- a/frontend/src/txns/staking/undelegate.ts +++ b/frontend/src/txns/staking/undelegate.ts @@ -1,4 +1,4 @@ -import { shortenMsg } from '@/utils/util'; +import { formatNumber, parseDenomAmount, shortenMsg } from '@/utils/util'; import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; import { MsgUndelegate } from 'cosmjs-types/cosmos/staking/v1beta1/tx'; @@ -23,13 +23,18 @@ export function UnDelegate( }; } -export function serialize(msg: Msg): string { - const { delegatorAddress, validatorAddress, amount } = msg.value; +/* eslint-disable @typescript-eslint/no-explicit-any */ +export function serialize( + msg: any, + decimals: number, + originalDenom: string +): string { + const amount = msg?.amount; + const delegatorAddress = msg?.delegator_address; + const validatorAddress = msg?.validator_address; return `${shortenMsg( delegatorAddress, 10 - )} un-delegated ${amount?.amount} ${amount?.denom} from ${shortenMsg( - validatorAddress, - 10 - )}`; + )} Un-delegated ${formatNumber(parseDenomAmount(amount?.amount || '0', decimals))} + ${originalDenom} to ${shortenMsg(validatorAddress, 10)}`; } diff --git a/frontend/src/types/authz.d.ts b/frontend/src/types/authz.d.ts new file mode 100644 index 000000000..a4ba7fdd4 --- /dev/null +++ b/frontend/src/types/authz.d.ts @@ -0,0 +1,111 @@ +interface Authorization { + granter: string; + grantee: string; + expiration: string | null; + authorization: GenericAuthorization | SendAuthorization | StakeAuthorization; +} + +interface GenericAuthorization { + spend_limit: Coin[]; + '@type': '/cosmos.authz.v1beta1.GenericAuthorization'; + msg: string; +} + +interface SendAuthorization { + msg: ReactNode; + '@type': '/cosmos.bank.v1beta1.SendAuthorization'; + spend_limit: Coin[]; + allow_list?: string[]; +} + +interface StakeAuthorization { + msg: ReactNode; + spend_limit: Coin[]; + + '@type': '/cosmos.staking.v1beta1.StakeAuthorization'; + + max_tokens: null | Coin; + allow_list: undefined | AddressList; + deny_list: undefined | AddressList; + authorization_type: AuthzDelegateType | AuthzReDelegateType | AuthzUnBondType; +} + +interface AddressList { + address: string[]; +} + +type AuthzUnBondType = 'AUTHORIZATION_TYPE_UNDELEGATE'; +type AuthzDelegateType = 'AUTHORIZATION_TYPE_DELEGATE'; +type AuthzReDelegateType = 'AUTHORIZATION_TYPE_REDELEGATE'; + +interface GetGrantsInputs { + baseURL: string; + baseURLs: string[]; + address: string; + pagination?: KeyLimitPagination; + chainID: string; +} + +interface GetGrantsResponse { + grants: Authorization[]; + pagination: Pagination; +} + +interface AddressGrants { + address: string; + chainID: string; + grants: Authorization[]; +} + +interface Grant { + msg: string; + expiration: Date; + spend_limit?: string; + max_tokens?: string; + isDenyList?: boolean; + validators_list?: string[]; +} + +interface TxGrantAuthzInputs { + basicChainInfo: BasicChainInfo; + msgs: Msg[]; + denom: string; + feeAmount: number; + feegranter: string; + onTxComplete?: ({ isTxSuccess, error, txHash }: OnTxnCompleteInputs) => void; +} + +interface TxGrantMultiChainAuthzInputs { + data: TxGrantAuthzInputs[]; +} + +interface MultiChainTx { + ChainID: string; + txInputs: TxGrantAuthzInputs; +} + +interface OnTxnCompleteInputs { + isTxSuccess: boolean; + error?: string; + txHash?: string; +} + +interface ChainStatus { + isTxSuccess?: boolean; + txStatus: string; + error: string; + txHash: string; +} + +interface TxAuthzExecInputs { + onTxSuccessCallBack?: () => void; + isAuthzMode: true; + basicChainInfo: BasicChainInfo; + denom: string; + memo: string; + msgs: Msg[]; + type?: string; + feegranter?: string; + authzChainGranter: string; + isTxAll?: boolean; +} diff --git a/frontend/src/types/bank.d.ts b/frontend/src/types/bank.d.ts index b82d4901e..3518b4744 100644 --- a/frontend/src/types/bank.d.ts +++ b/frontend/src/types/bank.d.ts @@ -7,6 +7,7 @@ interface MultiTxnsInputs { } interface TxSendInputs { + isAuthzMode: false basicChainInfo: BasicChainInfo; from: string; to: string; @@ -17,4 +18,6 @@ interface TxSendInputs { feegranter: string; memo: string; prefix: string; + rpc?:string; + onTxSuccessCallBack?: () => void; } diff --git a/frontend/src/types/common.d.ts b/frontend/src/types/common.d.ts index 0608b4bd8..faffbed21 100644 --- a/frontend/src/types/common.d.ts +++ b/frontend/src/types/common.d.ts @@ -48,4 +48,13 @@ interface CommonState { tokensInfoState: TokensInfoState; selectedNetwork: SelectedNetwork; allTokensInfoState: AllTokensInfoState; + allNetworksInfo: Record; + changeNetworkDialog: { + open: boolean; + showSearch: boolean; + }; + nameToChainIDs: Record; + addNetworkOpen: boolean; } + +type HandleChangeEvent = (e: React.ChangeEvent) => void; diff --git a/frontend/src/types/cosmwasm.d.ts b/frontend/src/types/cosmwasm.d.ts new file mode 100644 index 000000000..f12251678 --- /dev/null +++ b/frontend/src/types/cosmwasm.d.ts @@ -0,0 +1,215 @@ +interface ContractInfo { + code_id: string; + creator: string; + admin: string; + label: string; + created: { + block_height: string; + tx_index: string; + }; + ibc_port_id: string; + extension: string | null; +} + +interface ContractInfoResponse { + address: string; + contract_info: ContractInfo; +} + +interface AssetInfo { + coinMinimalDenom: string; + decimals: number; + symbol: string; +} + +interface FundInfo { + amount: string; + denom: string; + decimals: number; +} + +interface ParsedExecuteTxnResponse { + code: number; + fee: Coin[]; + transactionHash: string; + rawLog: string; + memo: string; +} + +interface ParsedUploadTxnResponse extends ParsedExecuteTxnResponse { + codeId: string; +} + +interface ParsedInstatiateTxnResponse extends ParsedUploadTxnResponse { + contractAddress: string; +} + +interface GetQueryContractFunctionInputs { + address: string; + baseURLs: string[]; + queryData: string; + chainID: string; +} + +interface QueryContractInfoInputs { + address: string; + baseURLs: string[]; + queryData: string; + chainID: string; + getQueryContract: ({ + address, + baseURLs, + queryData, + }: GetQueryContractFunctionInputs) => Promise<{ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + data: any; + }>; +} + +interface GetExecutionOutputFunctionInputs { + rpcURLs: string[]; + chainID: string; + contractAddress: string; + walletAddress: string; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + msgs: any; + funds: + | { + amount: string; + denom: string; + }[] + | undefined; +} + +interface ExecuteContractInputs { + rpcURLs: string[]; + chainID: string; + contractAddress: string; + walletAddress: string; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + msgs: any; + baseURLs: string[]; + funds: { amount: string; denom: string }[] | undefined; + getExecutionOutput: ({ + rpcURLs, + chainID, + contractAddress, + walletAddress, + msgs, + funds, + }: GetExecutionOutputFunctionInputs) => Promise<{ + txHash: string; + }>; +} + +interface UploadContractFunctionInputs { + chainID: string; + address: string; + messages: Msg[]; +} + +interface UploadCodeInputs { + chainID: string; + address: string; + messages: Msg[]; + baseURLs: string[]; + uploadContract: ({ + chainID, + address, + messages, + }: UploadContractFunctionInputs) => Promise<{ + codeId: string; + txHash: string; + }>; +} + +interface InstantiateContractFunctionInputs { + chainID: string; + codeId: number; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + msg: any; + label: string; + admin?: string; + funds?: Coin[]; +} + +interface InstantiateContractInputs { + chainID: string; + codeId: number; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + msg: any; + label: string; + admin?: string; + funds?: Coin[]; + baseURLs: string[]; + instantiateContract: ({ + chainID, + codeId, + msg, + label, + admin, + funds, + }: InstantiateContractFunctionInputs) => Promise<{ + codeId: string; + contractAddress: string; + txHash: string; + }>; +} + +interface QueryContractInputsI { + messagesLoading: boolean; + contractMessages: string[]; + handleSelectMessage: (msg: string) => Promise; + contractMessageInputs: string[]; + selectedMessage: string; + handleSelectedMessageInputChange: (value: string) => void; + queryText: string; + handleQueryChange: ( + e: React.ChangeEvent + ) => void; + onQuery: (queryInput: string) => void; + queryLoading: TxStatus; + formatJSON: () => boolean; + messageInputsLoading: boolean; + messageInputsError: string; + messagesError: string; + contractAddress: string; +} + +interface MessageInputField { + name: string; + value: string; + open: boolean; +} + +interface ExecuteContractInputsI { + messagesLoading: boolean; + executeMessages: string[]; + handleSelectMessage: (msg: string) => Promise; + selectedMessage: string; + executeMessageInputs: string[]; + handleSelectedMessageInputChange: (value: string) => void; + executeInput: string; + handleExecuteInputChange: ( + e: React.ChangeEvent + ) => void; + onExecute: (input: string) => void; + executionLoading: TxStatus; + formatJSON: () => void; + executeInputsLoading: boolean; + executeInputsError: string; + messagesError: string; + contractAddress: string; +} + +interface CodeInfo { + code_id: string; + creator: string; + data_hash: string; + instantiate_permission: InstantiatePermission; +} + +interface InstantiatePermission { + permission: string; + addresses: string[]; +} diff --git a/frontend/src/types/distribution.d.ts b/frontend/src/types/distribution.d.ts index 8cea6cf32..a44f13961 100644 --- a/frontend/src/types/distribution.d.ts +++ b/frontend/src/types/distribution.d.ts @@ -3,6 +3,7 @@ import { TxStatus } from './enums'; interface DelegatorTotalRewardsRequest { baseURL: string; + baseURLs: string[]; address: string; chainID: string; denom: string; @@ -31,6 +32,15 @@ interface DefaultState { status: TxStatus; txHash: string; }; + txWithdrawCommission: { + status: TxStatus; + errMsg: string; + }; + txSetWithdrawAddress: { + status: TxStatus; + errMsg: string; + }; + withdrawAddress: string; isTxAll: boolean; } @@ -38,6 +48,9 @@ interface DistributionStoreInitialState { chains: { [key: string]: DefaultState; }; + authzChains: { + [key: string]: DefaultState; + }; defaultState: DefaultState; } @@ -51,6 +64,8 @@ interface DelegationsPairs { } interface TxWithdrawAllRewardsInputs { + isAuthzMode: false; + basicChainInfo: BasicChainInfo; msgs: DelegationsPairs[]; denom: string; chainID: string; @@ -62,4 +77,29 @@ interface TxWithdrawAllRewardsInputs { address: string; cosmosAddress: string; isTxAll?: boolean; + rpc?: string; } + +interface BaseTxWithdraw { + isAuthzMode: false; + basicChainInfo: BasicChainInfo; + msgs: Msg[]; + denom: string; + chainID: string; + aminoConfig: AminoConfig; + prefix: string; + rest: string; + feeAmount: number; + feegranter: string; + address: string; + cosmosAddress: string; + isTxAll?: boolean; + rpc?: string; +} + +interface TxWithDrawValidatorCommissionInputs extends BaseTxWithdraw {} + +interface TxWithDrawValidatorCommissionAndRewardsInputs + extends BaseTxWithdraw {} + +interface TxSetWithdrawAddressInputs extends BaseTxWithdraw {} diff --git a/frontend/src/types/feegrant.d.ts b/frontend/src/types/feegrant.d.ts new file mode 100644 index 000000000..8422c9851 --- /dev/null +++ b/frontend/src/types/feegrant.d.ts @@ -0,0 +1,83 @@ +interface Allowance { + granter: string; + grantee: string; + allowance: BasicAllowance | PeriodicAllowance | AllowedMsgAllowance; +} + +interface BasicAllowance { + '@type': '/cosmos.feegrant.v1beta1.BasicAllowance'; + spend_limit: Coin[]; + expiration: string | null; +} + +interface PeriodicAllowance { + '@type': '/cosmos.feegrant.v1beta1.PeriodicAllowance'; + basic: { + spend_limit: Coin[]; + expiration: string | null; + }; + period: string; + period_spend_limit: Coin[]; + period_can_spend: Coin[]; + period_reset: string; +} + +interface AllowedMsgAllowance { + '@type': '/cosmos.feegrant.v1beta1.AllowedMsgAllowance'; + allowance: BasicAllowance | PeriodicAllowance; + allowed_messages: string[]; +} + +interface GetFeegrantsInputs { + baseURLs: string[]; + address: string; + pagination?: KeyLimitPagination; + chainID: string; +} + +interface GetFeegrantsResponse { + allowances: Allowance[]; + pagination: Pagination; +} + +interface AddressFeegrants { + address: string; + chainID: string; + grants: Allowance[]; +} + +interface FeeGrantRevokeInputs { + granter: string; + grantee: string; + basicChainInfo: BasicChainInfo; + baseURLs: string[]; + onTxSuccessCallBack?: () => void; + feegranter?:string; + denom:string, +} +interface FeegrantMsgInputs { + granter: string; + grantee: string; + denom: string; + spendLimit?: number; + period?: number; + periodSpendLimit?: number; + expiration?: Date; + txMsg?: Array; + allowanceType?: string; + isAuthzMode?: boolean; +} + +interface TxCreateFeegrantInputs { + basicChainInfo: BasicChainInfo; + msg: Msg; + denom: string; + feeAmount: number; + feegranter: string; + onTxComplete?: ({ isTxSuccess, error, txHash }: OnTxnCompleteInputs) => void; +} + +interface MultiChainFeegrantTx { + ChainID: string; + txInputs: TxCreateFeegrantInputs; +} diff --git a/frontend/src/types/gov.d.ts b/frontend/src/types/gov.d.ts index f3ab0459c..3e3beaec9 100644 --- a/frontend/src/types/gov.d.ts +++ b/frontend/src/types/gov.d.ts @@ -24,14 +24,12 @@ interface GovProposal { voting_end_time: string; } -interface ProposalInfo{ +interface ProposalInfo { status: TxStatus; errMsg: string; proposal: GovProposal; } - - interface GetProposalsInVotingResponse { proposals: GovProposal[]; pagination: GovPagination; @@ -112,46 +110,59 @@ interface GovParamsResponse { interface GetProposalsInVotingInputs { baseURL: string; + baseURLs: string[]; chainID: string; voter: string; + govV1: boolean; key?: string; limit?: number; } interface GetProposalsInDepositInputs { baseURL: string; + baseURLs: string[]; chainID: string; key?: string; limit?: number; + govV1: boolean; } interface GetVotesInputs { baseURL: string; + baseURLs: string[]; proposalId: number; voter: string; chainID: string; key?: string; limit?: number; + govV1: boolean; } interface GetProposalTallyInputs { baseURL: string; + baseURLs: string[]; proposalId: number; chainID: string; + govV1: boolean; } interface GetDepositParamsInputs { baseURL: string; + baseURLs: string[]; chainID: string; } interface GetProposalInputs { baseURL: string; + baseURLs: string[]; proposalId: number; chainID: string; + govV1: boolean; } interface TxVoteInputs { + isAuthzMode: false; + basicChainInfo: BasicChainInfo; voter: string; proposalId: number; option: number; @@ -167,6 +178,8 @@ interface TxVoteInputs { } interface TxDepositInputs { + isAuthzMode: false; + basicChainInfo: BasicChainInfo; depositer: string; proposalId: number; amount: number; @@ -180,3 +193,37 @@ interface TxDepositInputs { feegranter: string; justification?: string; } + +interface VoteOptionNumber { + [key: string]: number; +} + +interface ProposalsData { + chainID: string; + chainName: string; + chainLogo: string; + isActive: boolean; + proposalInfo: { + proposalTitle: string; + endTime: string; // voting end time or deposit end time + proposalId: string; + }; +} + +interface SelectedProposal { + chainID: string; + proposalId: string; + isActive: boolean; +} + +type HandleInputChangeEvent = (e: React.ChangeEvent) => void; + +type HandleSelectProposalEvent = ({ + chainID, + isActive, + proposalId, +}: { + proposalId: string; + chainID: string; + isActive: boolean; +}) => void; diff --git a/frontend/src/types/ibc.d.ts b/frontend/src/types/ibc.d.ts index c6025ef50..1dab57204 100644 --- a/frontend/src/types/ibc.d.ts +++ b/frontend/src/types/ibc.d.ts @@ -9,4 +9,6 @@ interface TransferRequestInputs { to: string; amount: string; rest: string; + rpc?: string; + restURLs: string[]; } diff --git a/frontend/src/types/multiops.d.ts b/frontend/src/types/multiops.d.ts new file mode 100644 index 000000000..4ff4ab8e8 --- /dev/null +++ b/frontend/src/types/multiops.d.ts @@ -0,0 +1,107 @@ +interface TxExecuteMultiMsgInputs { + msgs: Msg[]; + memo: string; + basicChainInfo: BasicChainInfo; + denom: string; + rpc: string; + rest: string; + aminoConfig: AminoConfig; + prefix: string; + feeAmount: number; + feegranter: string; + gas: number; + address: string; +} + +interface TxnMsgProps { + msg: Msg; + onDelete: (index: number) => void; + currency: Currency; + index: number; +} + +interface ValidatorOption { + address: string; + label: string; // moniker name + identity: string; +} + +type SendMsg = { + type: 'Send'; + address: string; + amount: string; +}; + +type DelegateMsg = { + type: 'Delegate'; + validator: string; + amount: string; +}; + +type UndelegateMsg = { + type: 'Undelegate'; + validator: string; + amount: string; +}; + +type RedelegateMsg = { + type: 'Redelegate'; + sourceValidator: string; + destValidator: string; + amount: string; +}; + +type VoteMsg = { + type: 'Vote'; + proposalId: string; + option: string; +}; + +type CustomMsg = { + type: 'Custom'; + typeUrl: string; + /* eslint-disable @typescript-eslint/no-explicit-any */ + value: any; +}; + +type Message = + | SendMsg + | DelegateMsg + | UndelegateMsg + | RedelegateMsg + | VoteMsg + | CustomMsg; + +type TxnBuilderForm = { + gas: number; + memo: string; + fees: number; + msgs: Msg[]; +}; + +interface ProposalOption { + label: string; + value: string; +} + +interface VoteOption { + label: string; + value: number; +} + +type TxnMsgType = + | 'Send' + | 'Delegate' + | 'Undelegate' + | 'Redelegate' + | 'Vote' + | 'Custom'; + +interface MessagesCount { + Send: number; + Delegate: number; + Redelegate: number; + Undelegate: number; + Vote: number; + Custom: number; +} diff --git a/frontend/src/types/multisig.d.ts b/frontend/src/types/multisig.d.ts index 3d742493e..d27f58f5c 100644 --- a/frontend/src/types/multisig.d.ts +++ b/frontend/src/types/multisig.d.ts @@ -74,10 +74,11 @@ interface MultisigAddressPubkey { pubkey: Pubkey; } -interface GetMultisigBalanceInputs { +interface GetMultisigBalancesInputs { baseURL: string; + baseURLs: string[]; address: string; - denom: string; + chainID: string; } interface Account { @@ -120,6 +121,25 @@ interface MultisigState { updateTxnRes: TxRes; txns: Txns; signTxRes: TxRes; + signTransactionRes: TxRes; + multisigAccountData: { + account: ImportMultisigAccountRes; + status: TxStatus; + error: string; + }; + verifyDialogOpen: boolean; + broadcastTxnRes: { + status: TxStatus; + error: string; + txHash: string; + txResponse: { + code: number; + fee: Coin[]; + transactionHash: string; + rawLog: string; + memo: string; + }; + }; } interface VerifyAccountRes { @@ -134,10 +154,7 @@ interface TxRes { } interface Balance { - balance: { - denom: string; - amount: string; - }; + balance: Coin[]; status: TxStatus; error: string; } @@ -181,10 +198,16 @@ interface Txn { threshold?: number; } +interface TxnCount { + computed_status: string; + count: number; +} + interface Txns { list: Txn[]; status: TxStatus; error: string; + Count: TxnCount[] } interface SignTxInputs { @@ -209,4 +232,46 @@ interface DeleteMultisigInputs { data: { address: string; }; -} \ No newline at end of file +} + +interface ImportMultisigAccountRes { + account: { + '@type': string; + address: string; + /* eslint-disable @typescript-eslint/no-explicit-any */ + pub_key: any; + account_number: string; + sequence: string; + }; +} + +interface DialogCreateMultisigProps { + open: boolean; + onClose: () => void; + chainID: string; +} + +interface PubKeyFields { + name: string; + value: string; + label: string; + placeHolder: string; + required: boolean; + disabled: boolean; + pubKey: string; + address: string; + isPubKey: boolean; + error: string; +} + +interface InputTextComponentProps { + index: number; + field: PubKeyFields; + handleRemoveValue: (index: number) => void; + handleChangeValue: ( + index: number, + e: ChangeEvent + ) => void; + togglePubKey: (index: number) => void; + isImport: boolean; +} diff --git a/frontend/src/types/network.d.ts b/frontend/src/types/network.d.ts index 5de76d8e1..23df9f5ae 100644 --- a/frontend/src/types/network.d.ts +++ b/frontend/src/types/network.d.ts @@ -36,6 +36,8 @@ type Theme = { interface NetworkConfig { rpc: string; rest: string; + restURIs: string[]; + rpcURIs: string[]; chainId: string; chainName: string; stakeCurrency: StakeCurrency; @@ -73,10 +75,13 @@ interface Network { keplrExperimental: boolean; leapExperimental: boolean; isTestnet: boolean; + govV1: boolean; explorerTxHashEndpoint: string; config: NetworkConfig; airdropMessage?: string; airdropActions?: AirdropAction[]; aminoConfig: AminoConfig; enableModules: EnableModule; + isCustomNetwork: boolean; + supportedWallets: string[]; } diff --git a/frontend/src/types/signing.d.ts b/frontend/src/types/signing.d.ts index 94688e302..376e7a3d5 100644 --- a/frontend/src/types/signing.d.ts +++ b/frontend/src/types/signing.d.ts @@ -6,7 +6,7 @@ interface PubKey { type Account = { '@type': string; address: string; - pub_key: PubKey; + pub_key?: PubKey; account_number: string; sequence: string; }; @@ -107,7 +107,7 @@ interface Tx { interface TxResponse { height: string; txhash: string; - tx: Tx; + tx?: Tx; codespace: string; code: number; data: string; diff --git a/frontend/src/types/squid.d.ts b/frontend/src/types/squid.d.ts new file mode 100644 index 000000000..6e3f30d55 --- /dev/null +++ b/frontend/src/types/squid.d.ts @@ -0,0 +1,60 @@ + + + + +interface NativeCurrency { + name: string; + symbol: string; + decimals: number; + icon: string; +} + +interface ChainNativeContracts { + wrappedNativeToken: string; + ensRegistry: string; + multicall: string; + usdcToken: string; +} + +interface Bridges { + axelar: { + gateway: string; + }; + cctp: { + cctpDomain: number; + tokenMessenger: string; + }; +} + +interface SquidContracts { + squidRouter: string; + defaultCrosschainToken: string; + squidMulticall: string; + squidFeeCollector: string; +} + +interface Compliance { + trmIdentifier: string; +} + +interface ChainData { + axelarChainName: string; + networkIdentifier: string; + chainType: string; + rpc: string; + networkName: string; + chainId: string; + nativeCurrency: NativeCurrency; + swapAmountForGas: string; + sameChainSwapsSupported: boolean; + chainIconURI: string; + blockExplorerUrls: string[]; + chainNativeContracts: ChainNativeContracts; + bridges: Bridges; + squidContracts: SquidContracts; + compliance: Compliance; + estimatedRouteDuration: number; + estimatedBoostRouteDuration: number; + enableBoostByDefault: boolean; +} + diff --git a/frontend/src/types/staking.d.ts b/frontend/src/types/staking.d.ts index 14ecc35a7..b39438e4a 100644 --- a/frontend/src/types/staking.d.ts +++ b/frontend/src/types/staking.d.ts @@ -5,7 +5,7 @@ interface GetValidatorsResponse { pagination: Pagination; } -interface Validator { +export interface Validator { operator_address: string; consensus_pubkey: PubKey; jailed: boolean; @@ -119,6 +119,7 @@ interface Params { } interface TxRedelegateInputs { + isAuthzMode: false; basicChainInfo: BasicChainInfo; delegator: string; srcVal: string; @@ -130,6 +131,7 @@ interface TxRedelegateInputs { } interface TxUndelegateInputs { + isAuthzMode: false; basicChainInfo: BasicChainInfo; delegator: string; validator: string; @@ -140,6 +142,7 @@ interface TxUndelegateInputs { } interface TxDelegateInputs { + isAuthzMode: false; basicChainInfo: BasicChainInfo; delegator: string; validator: string; @@ -150,6 +153,7 @@ interface TxDelegateInputs { } interface TxReStakeInputs { + isAuthzMode: false; basicChainInfo: BasicChainInfo; msgs: Msg[]; memo: string; @@ -160,6 +164,7 @@ interface TxReStakeInputs { } interface TxCancelUnbondingInputs { + isAuthzMode: false; basicChainInfo: BasicChainInfo; delegator: string; validator: string; @@ -357,3 +362,22 @@ interface AllValidatorsProps { allValidatorsDialogOpen: boolean; toggleValidatorsDialog: () => void; } + +interface ValidatorProfileInfo { + rank: string; + commission: number; + totalStakedInUSD: string; + chainID: string; + tokens: number; + operatorAddress: string; + validatorStatus: string; + validatorInfo: Validator; +} + +interface ValidatorInfo { + address: string; + label: string; // moniker name + identity: string; + description: string; + commission: number; +} \ No newline at end of file diff --git a/frontend/src/types/store.d.ts b/frontend/src/types/store.d.ts index 6ce5fd78e..458b72268 100644 --- a/frontend/src/types/store.d.ts +++ b/frontend/src/types/store.d.ts @@ -1,4 +1,6 @@ interface BasicChainInfo { + restURLs: string[]; + rpcURLs: string[]; baseURL: string; chainID: string; aminoConfig: AminoConfig; @@ -11,9 +13,33 @@ interface BasicChainInfo { feeCurrencies: Currency[]; explorerTxHashEndpoint: string; chainName: string; + chainLogo: string; + decimals: number; + valPrefix: string; + govV1: boolean; + isCustomNetwork: boolean; + enableModules: EnableModule; + supportedWallets: string[]; } interface Coin { amount: string; denom: string; } + +interface AllChainInfo { + restURLs: string[]; + baseURL: string; + chainID: string; + aminoConfig: AminoConfig; + rest: string; + rpc: string; + prefix: string; + feeAmount: number; + feeCurrencies: Currency[]; + explorerTxHashEndpoint: string; + chainName: string; + chainLogo: string; + decimals: number; + valPrefix: string; +} diff --git a/frontend/src/types/swaps.d.ts b/frontend/src/types/swaps.d.ts new file mode 100644 index 000000000..0b395e4fe --- /dev/null +++ b/frontend/src/types/swaps.d.ts @@ -0,0 +1,64 @@ +import { TxStatus } from './enums'; +import { RouteData } from '@0xsquid/sdk'; +import { SigningStargateClient } from '@cosmjs/stargate'; + +interface ChainConfig { + label: string; + logoURI: string; + chainID: string; +} + +interface AssetConfig { + label: string; + symbol: string; + logoURI: string; + denom: string; + decimals: number; + name: string; +} + +interface SwapState { + sourceChain: ChainConfig | null; + sourceAsset: AssetConfig | null; + destChain: ChainConfig | null; + destAsset: AssetConfig | null; + amountIn: string; + amountOut: string; + toAddress: string; + fromAddress: string; + slippage: string; + txStatus: { + status: TxStatus; + error: string; + }; + txSuccess: { + txHash: string; + }; + txDestSuccess: { + status: string; + msg: string; + }; + explorerEndpoint: string; +} + +interface TxSwapInputs { + rpcURLs: string[]; + swapRoute: RouteData; + signerAddress: string; + sourceChainID: string; + destChainID: string; + explorerEndpoint: string; + baseURLs: string[]; +} + +interface TxSwapServiceInputs { + signer: SigningStargateClient; + route: RouteData; + signerAddress: string; +} + +interface SwapPathObject { + type: string; + /* eslint-disable @typescript-eslint/no-explicit-any */ + value: any; +} diff --git a/frontend/src/types/transaction.d.ts b/frontend/src/types/transaction.d.ts index 6d9211d42..f3638e49a 100644 --- a/frontend/src/types/transaction.d.ts +++ b/frontend/src/types/transaction.d.ts @@ -15,22 +15,6 @@ interface Transaction { isIBCPending: boolean; } -interface AddTransactionInputs { - transactions: Transaction[]; - address: string; - chainID: string; -} - -interface LoadTransactionsInputs { - address: string; -} - -interface UpdateIBCTransactionInputs { - txHash: string; - address: string; - chainID: string; -} - interface UiTx { showMsgs: [string, string, boolean]; isTxSuccess: boolean; @@ -41,3 +25,27 @@ interface UiTx { isIBC: boolean; isIBCPending: boolean; } + +/* eslint-disable @typescript-eslint/no-explicit-any */ +interface ParsedTransaction { + code: number; + gas_used: string; + gas_wanted: string; + height: string; + raw_log: string; + timestamp: string; + memo: string; + messages: any[]; + chain_id: string; + fee: Coin[]; + address: string; + txhash: string; + isIBCTxn: boolean; + isIBCPending: boolean; +} + +interface RepeatTransactionInputs { + basicChainInfo: BasicChainInfo; + messages: Msg[]; + feegranter: string; +} diff --git a/frontend/src/types/types.d.ts b/frontend/src/types/types.d.ts index b12c44159..75bcf2f32 100644 --- a/frontend/src/types/types.d.ts +++ b/frontend/src/types/types.d.ts @@ -4,6 +4,13 @@ interface Msg { value: any; } + +interface NewMsg { + ['@type']: string; + /*eslint-disable-next-line */ + [key:string]: any; +} + type KeyLimitPagination = { key?: string; limit?: number; diff --git a/frontend/src/types/window.d.ts b/frontend/src/types/window.d.ts index 44d20f20c..b51f23316 100644 --- a/frontend/src/types/window.d.ts +++ b/frontend/src/types/window.d.ts @@ -8,5 +8,11 @@ declare global { wallet: KeplrWindow | any; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ cosmostation: any; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + metamask: any; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + ethereum: any; + + GA_INITIALIZED?:boolean; } } diff --git a/frontend/src/utils/RequestWrapper.ts b/frontend/src/utils/RequestWrapper.ts new file mode 100644 index 000000000..fa9f0d9c7 --- /dev/null +++ b/frontend/src/utils/RequestWrapper.ts @@ -0,0 +1,19 @@ +import Axios from 'axios'; +import { cleanURL } from './util'; + +export const axiosGetRequestWrapper = async ( + baseURIs: string[], + endPoint: string +) => { + let errMsg = ''; + + try { + const uri = `${cleanURL(baseURIs[0])}${endPoint}`; + return await Axios.get(uri); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (err: any) { + errMsg = err.message; + } + + throw new Error(errMsg); +}; diff --git a/frontend/src/utils/address.ts b/frontend/src/utils/address.ts new file mode 100644 index 000000000..02abf79a9 --- /dev/null +++ b/frontend/src/utils/address.ts @@ -0,0 +1,11 @@ +import { fromBech32, toBech32 } from '@cosmjs/encoding'; + +export const getAddressByPrefix = (address: string, prefix: string) => { + try { + const rawAddress = fromBech32(address); + return toBech32(prefix, rawAddress.data); + /* eslint-disable-next-line */ + } catch (err: any) { + return ''; + } +}; diff --git a/frontend/src/utils/authorizations.ts b/frontend/src/utils/authorizations.ts new file mode 100644 index 000000000..ab27b5225 --- /dev/null +++ b/frontend/src/utils/authorizations.ts @@ -0,0 +1,215 @@ +import { IBC_SEND_TYPE_URL } from './constants'; +import { convertToSpacedName } from './util'; + +interface AuthzMenuItem { + txn: string; + typeURL: string; +} + +export function authzMsgTypes(): AuthzMenuItem[] { + return [ + { + txn: 'Send', + typeURL: '/cosmos.bank.v1beta1.MsgSend', + }, + { + txn: 'Grant Authz', + typeURL: '/cosmos.authz.v1beta1.MsgGrant', + }, + { + txn: 'Revoke Authz', + typeURL: '/cosmos.authz.v1beta1.MsgRevoke', + }, + { + txn: 'Grant Feegrant', + typeURL: '/cosmos.feegrant.v1beta1.MsgGrantAllowance', + }, + { + txn: 'Revoke Feegrant', + typeURL: '/cosmos.feegrant.v1beta1.MsgRevokeAllowance', + }, + { + txn: 'Submit Proposal', + typeURL: '/cosmos.gov.v1beta1.MsgSubmitProposal', + }, + { + txn: 'Vote', + typeURL: '/cosmos.gov.v1beta1.MsgVote', + }, + { + txn: 'Deposit', + typeURL: '/cosmos.gov.v1beta1.MsgDeposit', + }, + { + txn: 'Withdraw Rewards', + typeURL: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', + }, + { + txn: 'Redelegate', + typeURL: '/cosmos.staking.v1beta1.MsgBeginRedelegate', + }, + { + txn: 'Delegate', + typeURL: '/cosmos.staking.v1beta1.MsgDelegate', + }, + { + txn: 'Undelegate', + typeURL: '/cosmos.staking.v1beta1.MsgUndelegate', + }, + { + txn: 'Withdraw Commission', + typeURL: '/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission', + }, + { + txn: 'Unjail', + typeURL: '/cosmos.slashing.v1beta1.MsgUnjail', + }, + { + txn: 'Set Withdraw Address', + typeURL: '/cosmos.distribution.v1beta1.MsgSetWithdrawAddress', + }, + { + txn: 'Cancel Unbonding', + typeURL: '/cosmos.staking.v1beta1.MsgCancelUnbondingDelegation', + }, + { + txn: 'IBC Transfer', + typeURL: '/ibc.applications.transfer.v1.MsgTransfer', + }, + ]; +} + +export const MAP_TXN_MSG_TYPES: Record = { + send: '/cosmos.bank.v1beta1.MsgSend', + grant_authz: '/cosmos.authz.v1beta1.MsgGrant', + revoke_authz: '/cosmos.authz.v1beta1.MsgRevoke', + grant_feegrant: '/cosmos.feegrant.v1beta1.MsgGrantAllowance', + revoke_feegrant: '/cosmos.feegrant.v1beta1.MsgRevokeAllowance', + submit_proposal: '/cosmos.gov.v1beta1.MsgSubmitProposal', + vote: '/cosmos.gov.v1beta1.MsgVote', + deposit: '/cosmos.gov.v1beta1.MsgDeposit', + withdraw_rewards: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', + redelegate: '/cosmos.staking.v1beta1.MsgBeginRedelegate', + delegate: '/cosmos.staking.v1beta1.MsgDelegate', + undelegate: '/cosmos.staking.v1beta1.MsgUndelegate', + withdraw_commission: + '/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission', + unjail: '/cosmos.slashing.v1beta1.MsgUnjail', + set_withdraw_address: '/cosmos.distribution.v1beta1.MsgSetWithdrawAddress', + cancel_unbonding: '/cosmos.staking.v1beta1.MsgCancelUnbondingDelegation', + ibc_transfer: '/ibc.applications.transfer.v1.MsgTransfer', +}; + +export const grantAuthzFormDefaultValues = () => { + const date = new Date(); + const expiration = new Date(date.setTime(date.getTime() + 365 * 86400000)); + return { + grant_authz: { expiration: expiration }, + revoke_authz: { expiration: expiration }, + grant_feegrant: { expiration: expiration }, + revoke_feegrant: { expiration: expiration }, + submit_proposal: { expiration: expiration }, + vote: { expiration: expiration }, + deposit: { expiration: expiration }, + withdraw_rewards: { expiration: expiration }, + withdraw_commission: { expiration: expiration }, + unjail: { expiration: expiration }, + send: { expiration: expiration, spend_limit: '' }, + delegate: { expiration: expiration, max_tokens: '' }, + undelegate: { expiration: expiration, max_tokens: '' }, + redelegate: { expiration: expiration, max_tokens: '' }, + set_withdraw_address: { expiration: expiration }, + cancel_unbonding: { expiration: expiration }, + ibc_transfer: { expiration: expiration }, + }; +}; +export function getTypeURLName(url: string) { + if (!url) { + return '-'; + } + const temp = url.split('.'); + if (temp?.length > 0) { + const msg = temp[temp?.length - 1]; + return msg.slice(3, msg.length); + } + return '-'; +} + +function getStakeAuthzType(type: string): string { + switch (type) { + case 'AUTHORIZATION_TYPE_DELEGATE': + return '/cosmos.staking.v1beta1.MsgDelegate'; + case 'AUTHORIZATION_TYPE_UNDELEGATE': + return '/cosmos.staking.v1beta1.MsgUndelegate'; + case 'AUTHORIZATION_TYPE_REDELEGATE': + return '/cosmos.staking.v1beta1.MsgBeginRedelegate'; + default: + throw new Error('unsupported stake authorization type'); + } +} + +export function getMsgNameFromAuthz(authorization: Authorization): string { + switch (authorization.authorization['@type']) { + case '/cosmos.bank.v1beta1.SendAuthorization': + return 'Send'; + case '/cosmos.authz.v1beta1.GenericAuthorization': + if (authorization?.authorization?.msg === IBC_SEND_TYPE_URL) { + return 'IBC Transfer'; + } + return convertToSpacedName( + getTypeURLName(authorization.authorization.msg) + ); + case '/cosmos.staking.v1beta1.StakeAuthorization': + const temp = getStakeAuthzType( + authorization?.authorization.authorization_type + ).split('.'); + if (temp.length === 0) { + return 'Unknown'; + } + return convertToSpacedName(temp[temp.length - 1]); + default: + return 'Unknown'; + } +} + +export function getTypeURLFromAuthorization( + authorization: Authorization +): string { + switch (authorization.authorization['@type']) { + case '/cosmos.bank.v1beta1.SendAuthorization': + return '/cosmos.bank.v1beta1.MsgSend'; + case '/cosmos.authz.v1beta1.GenericAuthorization': + return authorization.authorization.msg; + case '/cosmos.staking.v1beta1.StakeAuthorization': + return getStakeAuthzType(authorization?.authorization.authorization_type); + default: + throw new Error('unsupported authorization'); + } +} + +export const getAllTypeURLsFromAuthorization = ( + authorizations: Authorization[] +): string[] => { + const typeURLs: string[] = []; + authorizations.forEach((authorization) => { + typeURLs.push(getTypeURLFromAuthorization(authorization)); + }); + return typeURLs; +}; + +export const GENRIC_GRANTS = [ + 'grant_authz', + 'revoke_authz', + 'grant_feegrant', + 'revoke_feegrant', + 'submit_proposal', + 'vote', + 'deposit', + 'withdraw_rewards', + 'withdraw_commission', + 'unjail', + 'set_withdraw_address', + 'cancel_unbonding', + 'ibc_transfer', +]; +export const STAKE_GRANTS = ['delegate', 'undelegate', 'redelegate']; diff --git a/frontend/src/utils/chainDenoms.json b/frontend/src/utils/chainDenoms.json index fe7512560..592730ad1 100644 --- a/frontend/src/utils/chainDenoms.json +++ b/frontend/src/utils/chainDenoms.json @@ -10,7 +10,8 @@ "decimals": 6, "description": "Akash Staking Coin", "image": "akash/asset/akt.png", - "coinGeckoId": "akash-network" + "coinGeckoId": "akash-network", + "color": "#C71E10" }, { "denom": "ibc/2E5D0AC026AC1AFA65A23023BA4F24BB8DDF94F118EDC0BAD6F625BFC557CDED", @@ -204,187 +205,8 @@ "decimals": 6, "description": "Cosmos Staking Coin", "image": "cosmos/asset/atom.png", - "coinGeckoId": "cosmos" - }, - { - "denom": "poolDFB8434D5A80B4EAFA94B6878BD5B85265AC6C5D37204AB899B1C3C52543DA7E", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolDFB8434D5A80B4EAFA94B6878BD5B85265AC6C5D37204AB899B1C3C52543DA7E", - "origin_type": "pool", - "symbol": "GDEX-1", - "decimals": 6, - "description": "pool/1", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolE71FE13681A283B7015E4E4C4852B0EDA72CC97A5CDE2ECA2A6C8C06C86AC775", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolE71FE13681A283B7015E4E4C4852B0EDA72CC97A5CDE2ECA2A6C8C06C86AC775", - "origin_type": "pool", - "symbol": "GDEX-2", - "decimals": 6, - "description": "pool/2", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolD639A99414646D7136C65C6845D0EB3456EDD3D6C2C43050D3FA3A24995B0E75", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolD639A99414646D7136C65C6845D0EB3456EDD3D6C2C43050D3FA3A24995B0E75", - "origin_type": "pool", - "symbol": "GDEX-3", - "decimals": 6, - "description": "pool/3", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "pool2B9C24833CAA268C9081EC251693A724E8D343FC25A841FF00FD37B047BA4DEA", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "pool2B9C24833CAA268C9081EC251693A724E8D343FC25A841FF00FD37B047BA4DEA", - "origin_type": "pool", - "symbol": "GDEX-4", - "decimals": 6, - "description": "pool/4", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "pool32DD066BE949E5FDCC7DC09EBB67C7301D0CA957C2EF56A39B37430165447DAC", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "pool32DD066BE949E5FDCC7DC09EBB67C7301D0CA957C2EF56A39B37430165447DAC", - "origin_type": "pool", - "symbol": "GDEX-5", - "decimals": 6, - "description": "pool/5", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "pool4BFAFC499776D30A4FA0D6033135F00CC4EFC770D19A74CAD37433B579F77FC0", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "pool4BFAFC499776D30A4FA0D6033135F00CC4EFC770D19A74CAD37433B579F77FC0", - "origin_type": "pool", - "symbol": "GDEX-6", - "decimals": 6, - "description": "pool/6", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "pool7AE391C099D1D88CC85A9FA8A0DC5650BF8DDE0DCE7D0824C073802C020A7747", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "pool7AE391C099D1D88CC85A9FA8A0DC5650BF8DDE0DCE7D0824C073802C020A7747", - "origin_type": "pool", - "symbol": "GDEX-7", - "decimals": 6, - "description": "pool/7", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolF2805980C54E1474BDCCF70EF5FE881F3B8EFCF8BA3198765C01D91904521788", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolF2805980C54E1474BDCCF70EF5FE881F3B8EFCF8BA3198765C01D91904521788", - "origin_type": "pool", - "symbol": "GDEX-8", - "decimals": 6, - "description": "pool/8", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolBD5F1AF7A8B1F068C178F1D637DF126968EC10AB204A10116E320B2B8AF4FAC2", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolBD5F1AF7A8B1F068C178F1D637DF126968EC10AB204A10116E320B2B8AF4FAC2", - "origin_type": "pool", - "symbol": "GDEX-9", - "decimals": 6, - "description": "pool/9", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolB457CE9240C221C0F76952FED6506F74375EDD38B32A6020B7DDDFD5A4867D5C", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolB457CE9240C221C0F76952FED6506F74375EDD38B32A6020B7DDDFD5A4867D5C", - "origin_type": "pool", - "symbol": "GDEX-10", - "decimals": 6, - "description": "pool/10", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolD1121E78E731AFD35FEA13CF9FA0044A1472F73A0EE784160CCAAAAE5C7AAD7E", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolD1121E78E731AFD35FEA13CF9FA0044A1472F73A0EE784160CCAAAAE5C7AAD7E", - "origin_type": "pool", - "symbol": "GDEX-11", - "decimals": 6, - "description": "pool/11", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "pool60EFB07817D6B193A9FADA611404B8E11D82D6B7F0D10D57D3134C93E2BF7414", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "pool60EFB07817D6B193A9FADA611404B8E11D82D6B7F0D10D57D3134C93E2BF7414", - "origin_type": "pool", - "symbol": "GDEX-12", - "decimals": 6, - "description": "pool/12", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolCF8B847997F5EB92B9C8DBAE41656F61D6BE708B1B42D31063291813014AD63F", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolCF8B847997F5EB92B9C8DBAE41656F61D6BE708B1B42D31063291813014AD63F", - "origin_type": "pool", - "symbol": "GDEX-13", - "decimals": 6, - "description": "pool/13", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolAC9AF7B48E4497A0A9AF109E4286464A0EF06E7C35AD79198F03AB17A6A4CCA7", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolAC9AF7B48E4497A0A9AF109E4286464A0EF06E7C35AD79198F03AB17A6A4CCA7", - "origin_type": "pool", - "symbol": "GDEX-14", - "decimals": 6, - "description": "pool/14", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" - }, - { - "denom": "poolFD005C5AB01714A4B62E87F5213F5D5CDE357773D70712916A93664BCE5A6931", - "type": "pool", - "origin_chain": "cosmos", - "origin_denom": "poolFD005C5AB01714A4B62E87F5213F5D5CDE357773D70712916A93664BCE5A6931", - "origin_type": "pool", - "symbol": "GDEX-15", - "decimals": 6, - "description": "pool/15", - "image": "cosmos/asset/pool.png", - "coinGeckoId": "" + "coinGeckoId": "cosmos", + "color": "#9C6CFF" }, { "denom": "ibc/14F9BC3E44B8A9C1BE1FB08980FAB87034C9905EF17CF2F5008FC085218811CC", @@ -985,176 +807,376 @@ }, "image": "stride/asset/statom.png", "coinGeckoId": "stride-staked-atom" - } - ], - "evmos": [ - { - "denom": "aevmos", - "type": "staking", - "origin_chain": "evmos", - "origin_denom": "aevmos", - "origin_type": "staking", - "symbol": "EVMOS", - "decimals": 18, - "description": "Evmos Staking Coin", - "image": "evmos/asset/evmos.png", - "coinGeckoId": "evmos" }, { - "denom": "ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701", + "denom": "ibc/054892D6BB43AF8B93AAC28AA5FD7019D2C59A15DAFD6F45C1FA2BF9BDA22454", "type": "ibc", - "origin_chain": "cosmos", - "origin_denom": "uatom", - "origin_type": "staking", - "symbol": "ATOM", + "origin_chain": "stride", + "origin_denom": "stuosmo", + "origin_type": "native", + "symbol": "stOSMO", "decimals": 6, "enable": true, - "path": "cosmos>evmos", - "channel": "channel-3", + "path": "stride>cosmos", + "channel": "channel-391", "port": "transfer", "counter_party": { - "channel": "channel-292", + "channel": "channel-0", "port": "transfer", - "denom": "uatom" + "denom": "stuosmo" }, - "image": "cosmos/asset/atom.png", - "coinGeckoId": "cosmos" + "image": "stride/asset/stosmo.png", + "coinGeckoId": "stride-staked-osmo" }, { - "denom": "ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518", + "denom": "ibc/715BD634CF4D914C3EE93B0F8A9D2514B743F6FE36BC80263D1BC5EE4B3C5D40", "type": "ibc", - "origin_chain": "osmosis", - "origin_denom": "uosmo", - "origin_type": "staking", - "symbol": "OSMO", + "origin_chain": "stride", + "origin_denom": "stustars", + "origin_type": "native", + "symbol": "stSTARS", "decimals": 6, "enable": true, - "path": "osmosis>evmos", - "channel": "channel-0", + "path": "stride>cosmos", + "channel": "channel-391", "port": "transfer", "counter_party": { - "channel": "channel-204", + "channel": "channel-0", "port": "transfer", - "denom": "uosmo" + "denom": "stustars" }, - "image": "osmosis/asset/osmo.png", - "coinGeckoId": "osmosis" + "image": "stride/asset/ststars.png", + "coinGeckoId": "stride-staked-stars" }, { - "denom": "ibc/F7E92EE59B5428793F3EF5C1A4CB2494F61A9D0C9A69469D02390714A1372E16", + "denom": "ibc/88DCAA43A9CD099E1F9BBB80B9A90F64782EBA115A84B2CD8398757ADA4F4B40", "type": "ibc", - "origin_chain": "osmosis", - "origin_denom": "uion", + "origin_chain": "stride", + "origin_denom": "stujuno", "origin_type": "native", - "symbol": "ION", + "symbol": "stJUNO", "decimals": 6, "enable": true, - "path": "osmosis>evmos", - "channel": "channel-0", + "path": "stride>cosmos", + "channel": "channel-391", "port": "transfer", "counter_party": { - "channel": "channel-204", + "channel": "channel-0", "port": "transfer", - "denom": "uion" + "denom": "stujuno" }, - "image": "osmosis/asset/ion.png", - "coinGeckoId": "ion" + "image": "stride/asset/stjuno.png", + "coinGeckoId": "stride-staked-juno" }, { - "denom": "ibc/ADF401C952ADD9EE232D52C8303B8BE17FE7953C8D420F20769AF77240BD0C58", + "denom": "ibc/B38AAA0F7A3EC4D7C8E12DFA33FF93205FE7A42738A4B0590E2FF15BC60A612B", "type": "ibc", - "origin_chain": "injective", - "origin_denom": "inj", - "origin_type": "staking", - "symbol": "INJ", + "origin_chain": "stride", + "origin_denom": "staevmos", + "origin_type": "native", + "symbol": "stEVMOS", "decimals": 18, "enable": true, - "path": "injective>evmos", - "channel": "channel-10", + "path": "stride>cosmos", + "channel": "channel-391", "port": "transfer", "counter_party": { - "channel": "channel-83", + "channel": "channel-0", "port": "transfer", - "denom": "inj" + "denom": "staevmos" }, - "image": "injective/asset/inj.png", - "coinGeckoId": "injective-protocol" + "image": "stride/asset/stevmos.png", + "coinGeckoId": "stride-staked-evmos" }, { - "denom": "ibc/1FA2E811AA853A2AE028D82D490B1E967312DB871C9A40B19684FACB1DDD7881", + "denom": "ibc/B011C1A0AD5E717F674BA59FD8E05B2F946E4FD41C9CB3311C95F7ED4B815620", "type": "ibc", - "origin_chain": "crypto-org", - "origin_denom": "basecro", - "origin_type": "staking", - "symbol": "CRO", - "decimals": 8, + "origin_chain": "stride", + "origin_denom": "stinj", + "origin_type": "native", + "symbol": "stINJ", + "decimals": 18, "enable": true, - "path": "crypto-org>evmos", - "channel": "channel-31", + "path": "stride>cosmos", + "channel": "channel-391", "port": "transfer", "counter_party": { - "channel": "channel-57", + "channel": "channel-0", "port": "transfer", - "denom": "basecro" + "denom": "stinj" }, - "image": "crypto-org/asset/cro.png", - "coinGeckoId": "crypto-com-chain" + "image": "stride/asset/stinj.png", + "coinGeckoId": "stride-staked-injective" }, { - "denom": "ibc/7F0C2CB6E79CC36D29DA7592899F98E3BEFD2CF77A94340C317032A78812393D", + "denom": "ibc/D41ECC8FEF1B7E9C4BCC58B1362588420853A9D0B898EDD513D9B79AFFA195C8", "type": "ibc", - "origin_chain": "gravity-bridge", - "origin_denom": "ugraviton", - "origin_type": "staking", - "symbol": "GRAV", + "origin_chain": "stride", + "origin_denom": "stuumee", + "origin_type": "native", + "symbol": "stUMEE", "decimals": 6, "enable": true, - "path": "gravity-bridge>evmos", - "channel": "channel-8", + "path": "stride>cosmos", + "channel": "channel-391", "port": "transfer", "counter_party": { - "channel": "channel-65", + "channel": "channel-0", "port": "transfer", - "denom": "ugraviton" + "denom": "stuumee" }, - "image": "gravity-bridge/asset/grav.png", - "coinGeckoId": "graviton" + "image": "stride/asset/stumee.png", + "coinGeckoId": "" }, { - "denom": "ibc/693989F95CF3279ADC113A6EF21B02A51EC054C95A9083F2E290126668149433", + "denom": "ibc/E92E07E68705FAD13305EE9C73684B30A7B66A52F54C9890327E0A4C0F1D22E3", "type": "ibc", - "origin_chain": "ethereum", - "origin_denom": "usdc", - "origin_type": "erc20", - "symbol": "gUSDC", + "origin_chain": "stride", + "origin_denom": "stucmdx", + "origin_type": "native", + "symbol": "stCMDX", "decimals": 6, "enable": true, - "path": "ethereum>gravity-bridge>evmos", - "channel": "channel-8", + "path": "stride>cosmos", + "channel": "channel-391", "port": "transfer", "counter_party": { - "channel": "channel-65", + "channel": "channel-0", "port": "transfer", - "denom": "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + "denom": "stucmdx" }, - "image": "ethereum/asset/usdc.png", - "coinGeckoId": "usd-coin", - "contract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + "image": "stride/asset/stcmdx.png", + "coinGeckoId": "" }, { - "denom": "ibc/DF63978F803A2E27CA5CC9B7631654CCF0BBC788B3B7F0A10200508E37C70992", + "denom": "ibc/5CAE744C89BC70AE7B38019A1EDF83199B7E10F00F160E7F4F12BCA7A32A7EE5", "type": "ibc", - "origin_chain": "ethereum", - "origin_denom": "usdt", - "origin_type": "erc20", - "symbol": "gUSDT", + "origin_chain": "stride", + "origin_denom": "stuluna", + "origin_type": "native", + "symbol": "stLUNA", "decimals": 6, "enable": true, - "path": "ethereum>gravity-bridge>evmos", - "channel": "channel-8", + "path": "stride>cosmos", + "channel": "channel-391", "port": "transfer", "counter_party": { - "channel": "channel-65", + "channel": "channel-0", + "port": "transfer", + "denom": "stuluna" + }, + "image": "stride/asset/stluna.png", + "coinGeckoId": "stride-staked-luna" + }, + { + "denom": "ibc/A4D99E716D91A579AC3A9684AAB7B5CB0A0861DD3DD942901D970EDB6787860E", + "type": "ibc", + "origin_chain": "stride", + "origin_denom": "stusomm", + "origin_type": "native", + "symbol": "stSOMM", + "decimals": 6, + "enable": true, + "path": "stride>cosmos", + "channel": "channel-391", + "port": "transfer", + "counter_party": { + "channel": "channel-0", + "port": "transfer", + "denom": "stusomm" + }, + "image": "stride/asset/stsomm.png", + "coinGeckoId": "" + }, + { + "denom": "ibc/F663521BF1836B00F5F177680F74BFB9A8B5654A694D0D2BC249E03CF2509013", + "type": "ibc", + "origin_chain": "noble", + "origin_denom": "uusdc", + "origin_type": "native", + "symbol": "USDC", + "decimals": 6, + "enable": true, + "path": "noble>cosmos", + "channel": "channel-536", + "port": "transfer", + "counter_party": { + "channel": "channel-4", + "port": "transfer", + "denom": "uusdc" + }, + "image": "noble/asset/usdc.png", + "coinGeckoId": "usd-coin" + } + ], + "evmos": [ + { + "denom": "aevmos", + "type": "staking", + "origin_chain": "evmos", + "origin_denom": "aevmos", + "origin_type": "staking", + "symbol": "EVMOS", + "decimals": 18, + "description": "Evmos Staking Coin", + "image": "evmos/asset/evmos.png", + "coinGeckoId": "evmos" + }, + { + "denom": "ibc/A4DB47A9D3CF9A068D454513891B526702455D3EF08FB9EB558C561F9DC2B701", + "type": "ibc", + "origin_chain": "cosmos", + "origin_denom": "uatom", + "origin_type": "staking", + "symbol": "ATOM", + "decimals": 6, + "enable": true, + "path": "cosmos>evmos", + "channel": "channel-3", + "port": "transfer", + "counter_party": { + "channel": "channel-292", + "port": "transfer", + "denom": "uatom" + }, + "image": "cosmos/asset/atom.png", + "coinGeckoId": "cosmos" + }, + { + "denom": "ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518", + "type": "ibc", + "origin_chain": "osmosis", + "origin_denom": "uosmo", + "origin_type": "staking", + "symbol": "OSMO", + "decimals": 6, + "enable": true, + "path": "osmosis>evmos", + "channel": "channel-0", + "port": "transfer", + "counter_party": { + "channel": "channel-204", + "port": "transfer", + "denom": "uosmo" + }, + "image": "osmosis/asset/osmo.png", + "coinGeckoId": "osmosis" + }, + { + "denom": "ibc/F7E92EE59B5428793F3EF5C1A4CB2494F61A9D0C9A69469D02390714A1372E16", + "type": "ibc", + "origin_chain": "osmosis", + "origin_denom": "uion", + "origin_type": "native", + "symbol": "ION", + "decimals": 6, + "enable": true, + "path": "osmosis>evmos", + "channel": "channel-0", + "port": "transfer", + "counter_party": { + "channel": "channel-204", + "port": "transfer", + "denom": "uion" + }, + "image": "osmosis/asset/ion.png", + "coinGeckoId": "ion" + }, + { + "denom": "ibc/ADF401C952ADD9EE232D52C8303B8BE17FE7953C8D420F20769AF77240BD0C58", + "type": "ibc", + "origin_chain": "injective", + "origin_denom": "inj", + "origin_type": "staking", + "symbol": "INJ", + "decimals": 18, + "enable": true, + "path": "injective>evmos", + "channel": "channel-10", + "port": "transfer", + "counter_party": { + "channel": "channel-83", + "port": "transfer", + "denom": "inj" + }, + "image": "injective/asset/inj.png", + "coinGeckoId": "injective-protocol" + }, + { + "denom": "ibc/1FA2E811AA853A2AE028D82D490B1E967312DB871C9A40B19684FACB1DDD7881", + "type": "ibc", + "origin_chain": "crypto-org", + "origin_denom": "basecro", + "origin_type": "staking", + "symbol": "CRO", + "decimals": 8, + "enable": true, + "path": "crypto-org>evmos", + "channel": "channel-31", + "port": "transfer", + "counter_party": { + "channel": "channel-57", + "port": "transfer", + "denom": "basecro" + }, + "image": "crypto-org/asset/cro.png", + "coinGeckoId": "crypto-com-chain" + }, + { + "denom": "ibc/7F0C2CB6E79CC36D29DA7592899F98E3BEFD2CF77A94340C317032A78812393D", + "type": "ibc", + "origin_chain": "gravity-bridge", + "origin_denom": "ugraviton", + "origin_type": "staking", + "symbol": "GRAV", + "decimals": 6, + "enable": true, + "path": "gravity-bridge>evmos", + "channel": "channel-8", + "port": "transfer", + "counter_party": { + "channel": "channel-65", + "port": "transfer", + "denom": "ugraviton" + }, + "image": "gravity-bridge/asset/grav.png", + "coinGeckoId": "graviton" + }, + { + "denom": "ibc/693989F95CF3279ADC113A6EF21B02A51EC054C95A9083F2E290126668149433", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "usdc", + "origin_type": "erc20", + "symbol": "gUSDC", + "decimals": 6, + "enable": true, + "path": "ethereum>gravity-bridge>evmos", + "channel": "channel-8", + "port": "transfer", + "counter_party": { + "channel": "channel-65", + "port": "transfer", + "denom": "gravity0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + }, + "image": "ethereum/asset/usdc.png", + "coinGeckoId": "usd-coin", + "contract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + { + "denom": "ibc/DF63978F803A2E27CA5CC9B7631654CCF0BBC788B3B7F0A10200508E37C70992", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "usdt", + "origin_type": "erc20", + "symbol": "gUSDT", + "decimals": 6, + "enable": true, + "path": "ethereum>gravity-bridge>evmos", + "channel": "channel-8", + "port": "transfer", + "counter_party": { + "channel": "channel-65", "port": "transfer", "denom": "gravity0xdAC17F958D2ee523a2206206994597C13D831ec7" }, @@ -1393,7 +1415,8 @@ "decimals": 6, "description": "JUNO Staking Coin", "image": "juno/asset/juno.png", - "coinGeckoId": "juno-network" + "coinGeckoId": "juno-network", + "color": "#F0827D" }, { "denom": "ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518", @@ -1415,26 +1438,6 @@ "image": "osmosis/asset/osmo.png", "coinGeckoId": "osmosis" }, - { - "denom": "ibc/F7E92EE59B5428793F3EF5C1A4CB2494F61A9D0C9A69469D02390714A1372E16", - "type": "ibc", - "origin_chain": "osmosis", - "origin_denom": "uion", - "origin_type": "native", - "symbol": "ION", - "decimals": 6, - "enable": true, - "path": "osmosis>juno", - "channel": "channel-0", - "port": "transfer", - "counter_party": { - "channel": "channel-42", - "port": "transfer", - "denom": "uion" - }, - "image": "osmosis/asset/ion.png", - "coinGeckoId": "ion" - }, { "denom": "ibc/008BFD000A10BCE5F0D4DD819AE1C1EC2942396062DABDD6AE64A655ABC7085B", "type": "ibc", @@ -1455,26 +1458,6 @@ "image": "bitsong/asset/btsg.png", "coinGeckoId": "bitsong" }, - { - "denom": "ibc/A5A405107D27B0C4CDF9C6CCB6FF05EB8D1B7F9F322BE5C1B2315B2758329B87", - "type": "ibc", - "origin_chain": "rizon", - "origin_denom": "uatolo", - "origin_type": "staking", - "symbol": "ATOLO", - "decimals": 6, - "enable": true, - "path": "rizon>juno", - "channel": "channel-76", - "port": "transfer", - "counter_party": { - "channel": "channel-3", - "port": "transfer", - "denom": "uatolo" - }, - "image": "rizon/asset/atolo.png", - "coinGeckoId": "rizon" - }, { "denom": "ibc/F6B367385300865F654E110976B838502504231705BAC0849B0651C226385885", "type": "ibc", @@ -1656,61 +1639,21 @@ "coinGeckoId": "secret" }, { - "denom": "ibc/0CB5D60E57FD521FA39D11E3E410144389010AC5EF5F292BC9BDD832FA2FDBF9", + "denom": "ibc/B9F7C1E4CE9219B5AF06C47B18661DBD49CCD7A6C18FF789E2FB62BB365CFF9C", "type": "ibc", - "origin_chain": "bitcanna", - "origin_denom": "ubcna", - "origin_type": "staking", - "symbol": "BCNA", + "origin_chain": "emoney", + "origin_denom": "eeur", + "origin_type": "native", + "symbol": "EEUR", "decimals": 6, "enable": true, - "path": "bitcanna>juno", - "channel": "channel-50", + "path": "emoney>juno", + "channel": "channel-9", "port": "transfer", "counter_party": { - "channel": "channel-10", + "channel": "channel-15", "port": "transfer", - "denom": "ubcna" - }, - "image": "bitcanna/asset/bcna.png", - "coinGeckoId": "bitcanna" - }, - { - "denom": "ibc/52423136339C1CE8C91F6A586DFE41591BDDD4665AE526DFFA8421F9ACF95196", - "type": "ibc", - "origin_chain": "emoney", - "origin_denom": "ungm", - "origin_type": "staking", - "symbol": "NGM", - "decimals": 6, - "enable": true, - "path": "emoney>juno", - "channel": "channel-9", - "port": "transfer", - "counter_party": { - "channel": "channel-15", - "port": "transfer", - "denom": "ungm" - }, - "image": "emoney/asset/ngm.png", - "coinGeckoId": "e-money" - }, - { - "denom": "ibc/B9F7C1E4CE9219B5AF06C47B18661DBD49CCD7A6C18FF789E2FB62BB365CFF9C", - "type": "ibc", - "origin_chain": "emoney", - "origin_denom": "eeur", - "origin_type": "native", - "symbol": "EEUR", - "decimals": 6, - "enable": true, - "path": "emoney>juno", - "channel": "channel-9", - "port": "transfer", - "counter_party": { - "channel": "channel-15", - "port": "transfer", - "denom": "eeur" + "denom": "eeur" }, "image": "emoney/asset/eeur.png", "coinGeckoId": "e-money-eur" @@ -1897,6 +1840,26 @@ "denom": "unois" }, "image": "nois/asset/nois.png" + }, + { + "denom": "ibc/4A482FA914A4B9B05801ED81C33713899F322B24F76A06F4B8FE872485EA22FF", + "type": "ibc", + "origin_chain": "noble", + "origin_denom": "uusdc", + "origin_type": "native", + "symbol": "USDC", + "decimals": 6, + "enable": true, + "path": "noble>juno", + "channel": "channel-224", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "uusdc" + }, + "image": "noble/asset/usdc.png", + "coinGeckoId": "usd-coin" } ], "osmosis": [ @@ -1910,7 +1873,8 @@ "decimals": 6, "description": "Osmosis Staking Coin", "image": "osmosis/asset/osmo.png", - "coinGeckoId": "osmosis" + "coinGeckoId": "osmosis", + "color": "#9248DB" }, { "denom": "uion", @@ -1924,6 +1888,26 @@ "image": "osmosis/asset/ion.png", "coinGeckoId": "ion" }, + { + "denom": "ibc/831F0B1BBB1D08A2B75311892876D71565478C532967545476DF4C2D7492E48C", + "type": "ibc", + "origin_chain": "dydx", + "origin_denom": "adydx", + "origin_type": "staking", + "symbol": "DYDX", + "decimals": 18, + "enable": true, + "path": "dydx>osmosis", + "channel": "channel-6787", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "adydx" + }, + "image": "dydx/asset/dydx.png", + "coinGeckoId": "dydx-chain" + }, { "denom": "ibc/A8CA5EE328FA10C9519DF6057DA1F69682D28F7D0F5CCC7ECB72E3DCA2D157A4", "type": "ibc", @@ -2124,6 +2108,26 @@ "image": "akash/asset/akt.png", "coinGeckoId": "akash-network" }, + { + "denom": "ibc/B9606D347599F0F2FDF82BA3EE339000673B7D274EA50F59494DC51EFCD42163", + "type": "ibc", + "origin_chain": "onomy-protocol", + "origin_denom": "anom", + "origin_type": "staking", + "symbol": "NOM", + "decimals": 18, + "enable": true, + "path": "onomy-protocol>osmosis", + "channel": "channel-525", + "port": "transfer", + "counter_party": { + "channel": "channel-0", + "port": "transfer", + "denom": "anom" + }, + "image": "onomy-protocol/asset/nom.png", + "coinGeckoId": "onomy-protocol" + }, { "denom": "ibc/B9E0A1A524E98BB407D3CED8720EFEFD186002F90C1B1B7964811DD0CCC12228", "type": "ibc", @@ -2424,6 +2428,26 @@ "image": "evmos/asset/evmos.png", "coinGeckoId": "evmos" }, + { + "denom": "ibc/DEE262653B9DE39BCEF0493D47E0DFC4FE62F7F046CF38B9FDEFEBE98D149A71", + "type": "ibc", + "origin_chain": "evmos", + "origin_denom": "neok", + "origin_type": "erc20", + "symbol": "NEOK", + "decimals": 18, + "enable": true, + "path": "evmos>osmosis", + "channel": "channel-204", + "port": "transfer", + "counter_party": { + "channel": "channel-0", + "port": "transfer", + "denom": "erc20/0x655ecB57432CC1370f65e5dc2309588b71b473A9" + }, + "image": "evmos/asset/neok.png", + "coinGeckoId": "" + }, { "denom": "ibc/41999DF04D9441DAC0DF5D8291DF4333FBCBA810FFD63FDCE34FDF41EF37B6F7", "type": "ibc", @@ -2824,6 +2848,27 @@ "image": "kava/asset/kava.png", "coinGeckoId": "kava" }, + { + "denom": "ibc/4ABBEF4C8926DDDB320AE5188CFD63267ABBCEFC0583E4AE05D6E5AA2401DDAB", + "type": "ibc", + "origin_chain": "kava", + "origin_denom": "erc20/tether/usdt", + "origin_type": "native", + "symbol": "USDt", + "decimals": 6, + "description": "Tether USDt", + "enable": true, + "path": "kava>osmosis", + "channel": "channel-143", + "port": "transfer", + "counter_party": { + "channel": "channel-1", + "port": "transfer", + "denom": "erc20/tether/usdt" + }, + "image": "ethereum/asset/usdt.png", + "coinGeckoId": "tether" + }, { "denom": "ibc/A0CC0CF735BFB30E730C70019D4218A1244FF383503FF7579C9201AB93CA9293", "type": "ibc", @@ -2864,6 +2909,26 @@ "image": "persistence/asset/stkatom.png", "coinGeckoId": "stkatom" }, + { + "denom": "ibc/ECBE78BF7677320A93E7BA1761D144BCBF0CBC247C290C049655E106FE5DC68E", + "type": "ibc", + "origin_chain": "persistence", + "origin_denom": "stk/uosmo", + "origin_type": "native", + "symbol": "stkOSMO", + "decimals": 6, + "enable": true, + "path": "persistence>osmosis", + "channel": "channel-4", + "port": "transfer", + "counter_party": { + "channel": "channel-6", + "port": "transfer", + "denom": "stk/uosmo" + }, + "image": "persistence/asset/stkosmo.png", + "coinGeckoId": "stkosmo" + }, { "denom": "ibc/8061A06D3BD4D52C4A28FFECF7150D370393AF0BA661C3776C54FF32836C3961", "type": "ibc", @@ -3046,7 +3111,7 @@ "denom": "uusdc" }, "image": "ethereum/asset/usdc.png", - "coinGeckoId": "usd-coin", + "coinGeckoId": "axlusdc", "contract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, { @@ -3088,7 +3153,7 @@ "denom": "wbtc-satoshi" }, "image": "ethereum/asset/wbtc.png", - "coinGeckoId": "wrapped-bitcoin", + "coinGeckoId": "axlwbtc", "contract": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" }, { @@ -3109,7 +3174,7 @@ "denom": "weth-wei" }, "image": "ethereum/asset/weth.png", - "coinGeckoId": "weth", + "coinGeckoId": "axlweth", "contract": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, { @@ -3255,7 +3320,7 @@ "denom": "uusdt" }, "image": "ethereum/asset/usdt.png", - "coinGeckoId": "tether", + "coinGeckoId": "axelar-usdt", "contract": "0xdac17f958d2ee523a2206206994597c13d831ec7" }, { @@ -3447,7 +3512,7 @@ "contract": "juno1g2g7ucurum66d42g8k5twk34yegdq8c82858gz0tq2fc75zy7khssgnhjl" }, { - "denom": "ibc/c6b6bfcb6ee49a7cab1a7e7b021de35b99d525ac660844952f0f6c78dcb2a57b", + "denom": "ibc/C6B6BFCB6EE49A7CAB1A7E7B021DE35B99D525AC660844952F0F6C78DCB2A57B", "type": "ibc", "origin_chain": "juno", "origin_denom": "sejuno", @@ -3467,6 +3532,26 @@ "coinGeckoId": "", "contract": "juno1dd0k0um5rqncfueza62w9sentdfh3ec4nw4aq4lk5hkjl63vljqscth9gv" }, + { + "denom": "ibc/884EBC228DFCE8F1304D917A712AA9611427A6C1ECC3179B2E91D7468FB091A2", + "type": "ibc", + "origin_chain": "injective", + "origin_denom": "factory/inj1h0ypsdtjfcjynqu3m75z2zwwz5mmrj8rtk2g52/uhava", + "origin_type": "native", + "symbol": "HAVA", + "decimals": 6, + "enable": true, + "path": "injective>osmosis", + "channel": "channel-122", + "port": "transfer", + "counter_party": { + "channel": "channel-8", + "port": "transfer", + "denom": "factory/inj1h0ypsdtjfcjynqu3m75z2zwwz5mmrj8rtk2g52/uhava" + }, + "image": "injective/asset/hava.png", + "coinGeckoId": "" + }, { "denom": "ibc/6F18EFEBF1688AA77F7EAC17065609494DC1BA12AFC78E9AEC832AF70A11BEF3", "type": "ibc", @@ -3510,7 +3595,7 @@ "contract": "terra1gwrz9xzhqsygyr5asrgyq3pu0ewpn00mv2zenu86yvx2nlwpe8lqppv584" }, { - "denom": "ibc/c2df5c3949ca835b221c575625991f09bab4e48fb9c11a4ee357194f736111e3", + "denom": "ibc/C2DF5C3949CA835B221C575625991F09BAB4E48FB9C11A4EE357194F736111E3", "type": "ibc", "origin_chain": "juno", "origin_denom": "bjuno", @@ -3692,7 +3777,7 @@ "coinGeckoId": "arable-protocol" }, { - "denom": "ibc/01e94a5ff29b8ddefc86f412cc3927f7330e9b523cc63a6194b1108f5276025c", + "denom": "ibc/01E94A5FF29B8DDEFC86F412CC3927F7330E9B523CC63A6194B1108F5276025C", "type": "ibc", "origin_chain": "medasdigital", "origin_denom": "umedas", @@ -3849,7 +3934,7 @@ "denom": "eco.uC.NCT" }, "image": "regen/asset/nct.png", - "coinGeckoId": "toucan-protocol-nature-carbon-tonne" + "coinGeckoId": "" }, { "denom": "ibc/BB0AFE2AFBD6E883690DAE4B9168EAC2B306BCC9C9292DACBB4152BBB08DB25F", @@ -3860,256 +3945,947 @@ "symbol": "AIOZ", "decimals": 18, "enable": true, - "path": "aioz>osmosis", - "channel": "channel-779", + "path": "aioz>osmosis", + "channel": "channel-779", + "port": "transfer", + "counter_party": { + "channel": "channel-1", + "port": "transfer", + "denom": "attoaioz" + }, + "image": "aioz/asset/aioz.png", + "coinGeckoId": "aioz-network" + }, + { + "denom": "ibc/C360EF34A86D334F625E4CBB7DA3223AEA97174B61F35BB3758081A8160F7D9B", + "type": "ibc", + "origin_chain": "odin", + "origin_denom": "loki", + "origin_type": "staking", + "symbol": "ODIN", + "decimals": 6, + "enable": true, + "path": "odin>osmosis", + "channel": "channel-258", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "loki" + }, + "image": "odin/asset/odin.png", + "coinGeckoId": "odin-protocol" + }, + { + "denom": "ibc/10E5E5B06D78FFBB61FD9F89209DEE5FD4446ED0550CBB8E3747DA79E10D9DC6", + "type": "ibc", + "origin_chain": "arbitrum", + "origin_denom": "arb", + "origin_type": "native", + "symbol": "ARB", + "decimals": 18, + "enable": true, + "path": "arbitrum>axelar>osmosis", + "channel": "channel-208", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "arb-wei" + }, + "image": "arbitrum/asset/arb.png", + "coinGeckoId": "arbitrum", + "contract": "0x912ce59144191c1204e64559fe8253a0e49e6548" + }, + { + "denom": "ibc/EDD6F0D66BCD49C1084FB2C35353B4ACD7B9191117CE63671B61320548F7C89D", + "type": "ibc", + "origin_chain": "whitewhale", + "origin_denom": "uwhite", + "origin_type": "erc20", + "symbol": "WHALE", + "decimals": 6, + "enable": true, + "path": "whitewhale>osmosis", + "channel": "channel-642", + "port": "transfer", + "counter_party": { + "channel": "channel-5", + "port": "transfer", + "denom": "uwhite" + }, + "image": "whitewhale/asset/whale.png", + "coinGeckoId": "white-whale" + }, + { + "denom": "ibc/23CA6C8D1AB2145DD13EB1E089A2E3F960DC298B468CCE034E19E5A78B61136E", + "type": "ibc", + "origin_chain": "comdex", + "origin_denom": "ucmst", + "origin_type": "native", + "symbol": "CMST", + "decimals": 6, + "enable": true, + "path": "comdex>osmosis", + "channel": "channel-87", + "port": "transfer", + "counter_party": { + "channel": "channel-1", + "port": "transfer", + "denom": "ucmst" + }, + "image": "comdex/asset/cmst.png", + "coinGeckoId": "composite" + }, + { + "denom": "ibc/02F196DA6FD0917DD5FEA249EE61880F4D941EE9059E7964C5C9B50AF103800F", + "type": "ibc", + "origin_chain": "stride", + "origin_denom": "stuumee", + "origin_type": "native", + "symbol": "stUMEE", + "decimals": 6, + "enable": true, + "path": "stride>osmosis", + "channel": "channel-326", + "port": "transfer", + "counter_party": { + "channel": "channel-5", + "port": "transfer", + "denom": "stuumee" + }, + "image": "stride/asset/stumee.png", + "coinGeckoId": "stride-staked-umee" + }, + { + "denom": "ibc/C5579A9595790017C600DD726276D978B9BF314CF82406CE342720A9C7911A01", + "type": "ibc", + "origin_chain": "stride", + "origin_denom": "staevmos", + "origin_type": "native", + "symbol": "stEVMOS", + "decimals": 18, + "enable": true, + "path": "stride>osmosis", + "channel": "channel-326", + "port": "transfer", + "counter_party": { + "channel": "channel-5", + "port": "transfer", + "denom": "staevmos" + }, + "image": "stride/asset/stevmos.png", + "coinGeckoId": "" + }, + { + "denom": "ibc/5DD1F95ED336014D00CE2520977EC71566D282F9749170ADC83A392E0EA7426A", + "type": "ibc", + "origin_chain": "stride", + "origin_denom": "stustars", + "origin_type": "native", + "symbol": "stSTARS", + "decimals": 6, + "enable": true, + "path": "stride>osmosis", + "channel": "channel-326", + "port": "transfer", + "counter_party": { + "channel": "channel-5", + "port": "transfer", + "denom": "stustars" + }, + "image": "stride/asset/ststars.png", + "coinGeckoId": "stride-staked-stars" + }, + { + "denom": "ibc/84502A75BCA4A5F68D464C00B3F610CE2585847D59B52E5FFB7C3C9D2DDCD3FE", + "type": "ibc", + "origin_chain": "stride", + "origin_denom": "stujuno", + "origin_type": "native", + "symbol": "stJUNO", + "decimals": 6, + "enable": true, + "path": "stride>osmosis", + "channel": "channel-326", + "port": "transfer", + "counter_party": { + "channel": "channel-5", + "port": "transfer", + "denom": "stujuno" + }, + "image": "stride/asset/stjuno.png", + "coinGeckoId": "stride-staked-juno" + }, + { + "denom": "ibc/1B708808D372E959CD4839C594960309283424C775F4A038AAEBE7F83A988477", + "type": "ibc", + "origin_chain": "quasar", + "origin_denom": "uqsr", + "origin_type": "staking", + "symbol": "QSR", + "decimals": 6, + "enable": true, + "path": "quasar>osmosis", + "channel": "channel-688", + "port": "transfer", + "counter_party": { + "channel": "channel-1", + "port": "transfer", + "denom": "uqsr" + }, + "image": "quasar/asset/qsr.png", + "coinGeckoId": "quasar-2" + }, + { + "denom": "ibc/23AB778D694C1ECFC59B91D8C399C115CC53B0BD1C61020D8E19519F002BDD85", + "type": "ibc", + "origin_chain": "archway", + "origin_denom": "aarch", + "origin_type": "staking", + "symbol": "ARCH", + "decimals": 18, + "enable": true, + "path": "archway>osmosis", + "channel": "channel-1429", + "port": "transfer", + "counter_party": { + "channel": "channel-1", + "port": "transfer", + "denom": "aarch" + }, + "image": "archway/asset/arch.png", + "coinGeckoId": "archway" + }, + { + "denom": "ibc/9B6FBABA36BB4A3BF127AE5E96B572A5197FD9F3111D895D8919B07BC290764A", + "type": "ibc", + "origin_chain": "odin", + "origin_denom": "mgeo", + "origin_type": "native", + "symbol": "GEO", + "decimals": 6, + "enable": true, + "path": "odin>osmosis", + "channel": "channel-258", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "mgeo" + }, + "image": "odin/asset/geo.png", + "coinGeckoId": "geodb" + }, + { + "denom": "ibc/0CD46223FEABD2AEAAAF1F057D01E63BCA79B7D4BD6B68F1EB973A987344695D", + "type": "ibc", + "origin_chain": "odin", + "origin_denom": "mO9W", + "origin_type": "native", + "symbol": "O9W", + "decimals": 6, + "enable": true, + "path": "odin>osmosis", + "channel": "channel-258", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "mO9W" + }, + "image": "odin/asset/o9w.png" + }, + { + "denom": "ibc/CFF40564FDA3E958D9904B8B479124987901168494655D9CC6B7C0EC0416020B", + "type": "ibc", + "origin_chain": "stargaze", + "origin_denom": "factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust", + "origin_type": "native", + "symbol": "STRDST", + "decimals": 6, + "enable": true, + "path": "stargaze>osmosis", + "channel": "channel-75", + "port": "transfer", + "counter_party": { + "channel": "channel-0", + "port": "transfer", + "denom": "ustars" + }, + "image": "stargaze/asset/dust.png", + "coinGeckoId": "" + }, + { + "denom": "ibc/71DAA4CAFA4FE2F9803ABA0696BA5FC0EFC14305A2EA8B4E01880DB851B1EC02", + "type": "ibc", + "origin_chain": "stargaze", + "origin_denom": "factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/uBRNCH", + "origin_type": "native", + "symbol": "BRNCH", + "decimals": 6, + "enable": true, + "path": "stargaze>osmosis", + "channel": "channel-75", + "port": "transfer", + "counter_party": { + "channel": "channel-0", + "port": "transfer", + "denom": "ustars" + }, + "image": "stargaze/asset/brnch.png", + "coinGeckoId": "" + }, + { + "denom": "ibc/F3166F4D31D6BA1EC6C9F5536F5DDDD4CC93DBA430F7419E7CDC41C497944A65", + "type": "ibc", + "origin_chain": "coreum", + "origin_denom": "ucore", + "origin_type": "staking", + "symbol": "CORE", + "decimals": 6, + "enable": true, + "path": "coreum>osmosis", + "channel": "channel-2188", + "port": "transfer", + "counter_party": { + "channel": "channel-2", + "port": "transfer", + "denom": "ucore" + }, + "image": "coreum/asset/core.png", + "coinGeckoId": "coreum" + }, + { + "denom": "ibc/EAF76AD1EEF7B16D167D87711FB26ABE881AC7D9F7E6D0CF313D5FA530417208", + "type": "ibc", + "origin_chain": "quicksilver", + "origin_denom": "uqsomm", + "origin_type": "native", + "symbol": "qSOMM", + "decimals": 6, + "enable": true, + "path": "quicksilver>osmosis", + "channel": "channel-522", + "port": "transfer", + "counter_party": { + "channel": "channel-2", + "port": "transfer", + "denom": "uqsomm" + }, + "image": "quicksilver/asset/qsomm.png", + "coinGeckoId": "" + }, + { + "denom": "ibc/71F11BC0AF8E526B80E44172EBA9D3F0A8E03950BB882325435691EBC9450B1D", + "type": "ibc", + "origin_chain": "sei", + "origin_denom": "usei", + "origin_type": "staking", + "symbol": "SEI", + "decimals": 6, + "enable": true, + "path": "sei>osmosis", + "channel": "channel-782", + "port": "transfer", + "counter_party": { + "channel": "channel-0", + "port": "transfer", + "denom": "usei" + }, + "image": "sei/asset/sei.png", + "coinGeckoId": "sei-network" + }, + { + "denom": "ibc/5A0060579D24FBE5268BEA74C3281E7FE533D361C41A99307B4998FEC611E46B", + "type": "ibc", + "origin_chain": "stride", + "origin_denom": "stusomm", + "origin_type": "native", + "symbol": "stSOMM", + "decimals": 6, + "enable": true, + "path": "stride>osmosis", + "channel": "channel-326", + "port": "transfer", + "counter_party": { + "channel": "channel-5", + "port": "transfer", + "denom": "stusomm" + }, + "image": "stride/asset/stsomm.png" + }, + { + "denom": "ibc/785AFEC6B3741100D15E7AF01374E3C4C36F24888E96479B1C33F5C71F364EF9", + "type": "ibc", + "origin_chain": "terra", + "origin_denom": "uluna", + "origin_type": "staking", + "symbol": "LUNA", + "decimals": 6, + "description": "Terra Staking Coin", + "enable": true, + "path": "terra>osmosis", + "channel": "channel-251", + "port": "transfer", + "counter_party": { + "channel": "channel-1", + "port": "transfer", + "denom": "uluna" + }, + "image": "terra/asset/luna.png", + "coinGeckoId": "terra-luna-2" + }, + { + "denom": "ibc/1E43D59E565D41FB4E54CA639B838FFD5BCFC20003D330A56CB1396231AA1CBA", + "type": "ibc", + "origin_chain": "gateway", + "origin_denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/8sYgCzLRJC3J7qPn2bNbx6PiGcarhyx8rBhVaNnfvHCA", + "origin_type": "spl_token", + "symbol": "SOL", + "decimals": 8, + "enable": true, + "path": "solana>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/8sYgCzLRJC3J7qPn2bNbx6PiGcarhyx8rBhVaNnfvHCA" + }, + "image": "solana/asset/solana.png", + "coinGeckoId": "solana", + "contract": "So11111111111111111111111111111111111111112" + }, + { + "denom": "ibc/CA3733CB0071F480FAE8EF0D9C3D47A49C6589144620A642BBE0D59A293D110E", + "type": "ibc", + "origin_chain": "solana", + "origin_denom": "bonk", + "origin_type": "spl_token", + "symbol": "BONK", + "decimals": 5, + "enable": true, + "path": "solana>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/95mnwzvJZJ3fKz77xfGN2nR5to9pZmH8YNvaxgLgw5AR" + }, + "image": "solana/asset/bonk.png", + "coinGeckoId": "bonk", + "contract": "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" + }, + { + "denom": "ibc/6B99DB46AA9FF47162148C1726866919E44A6A5E0274B90912FD17E19A337695", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "usdc", + "origin_type": "erc20", + "symbol": "USDC", + "decimals": 6, + "enable": true, + "path": "ethereum>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/GGh9Ufn1SeDGrhzEkMyRKt5568VbbxZK2yvWNsd6PbXt" + }, + "image": "ethereum/asset/usdc.png", + "coinGeckoId": "usd-coin", + "contract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + }, + { + "denom": "ibc/2108F2D81CBE328F371AD0CEF56691B18A86E08C3651504E42487D9EE92DDE9C", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "usdt", + "origin_type": "erc20", + "symbol": "USDT", + "decimals": 6, + "enable": true, + "path": "ethereum>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/8iuAc6DSeLvi2JDUtwJxLytsZT8R19itXebZsNReLLNi" + }, + "image": "ethereum/asset/usdt.png", + "coinGeckoId": "tether", + "contract": "0xdAC17F958D2ee523a2206206994597C13D831ec7" + }, + { + "denom": "ibc/E4CD61E1FA3EB04EF1BF924D676AB9FD55E84A0DCF4E78C11CCA0E14E5B42672", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "wbtc", + "origin_type": "erc20", + "symbol": "WBTC", + "decimals": 8, + "enable": true, + "path": "ethereum>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/BGkuAcga2WArUghF8L6kt6uCAhAzrxmn1QcbQqi5r5bd" + }, + "image": "ethereum/asset/wbtc.png", + "coinGeckoId": "wrapped-bitcoin", + "contract": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + }, + { + "denom": "ibc/62F82550D0B96522361C89B0DA1119DE262FBDFB25E5502BC5101B5C0D0DBAAC", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "weth", + "origin_type": "erc20", + "symbol": "WETH", + "decimals": 8, + "enable": true, + "path": "ethereum>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/5BWqpR48Lubd55szM5i62zK7TFkddckhbT48yy6mNbDp" + }, + "image": "ethereum/asset/weth.png", + "coinGeckoId": "weth", + "contract": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + { + "denom": "ibc/898ACF6F5DEBF535103BBD52E3E5B70A311AD097B198A152483F69290B4210C0", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "dai", + "origin_type": "erc20", + "symbol": "DAI", + "decimals": 8, + "enable": true, + "path": "ethereum>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/EKiMEqDnPKokFGcSXDvGMk6Gt1BJ6BC7BDZzTmEpWLH1" + }, + "image": "ethereum/asset/dai.png", + "coinGeckoId": "dai", + "contract": "0x6B175474E89094C44Da98b954EedeAC495271d0F" + }, + { + "denom": "ibc/BF75AE1500CB7EC458E91A11731F1B6AC1F1FE1FA937A88564955ED6A83CA2FB", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "wsteth", + "origin_type": "erc20", + "symbol": "wstETH", + "decimals": 8, + "enable": true, + "path": "ethereum>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/5TSQTEhJ5Q6r1YqCmCxTRSPiV2pGx5rZUQf6g2XH4e1b" + }, + "image": "ethereum/asset/wsteth.png", + "coinGeckoId": "wrapped-steth", + "contract": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" + }, + { + "denom": "ibc/6207D35D2C08F2162575C3C4BFD524226E50639121A273045F1B393AF67DCEB3", + "type": "ibc", + "origin_chain": "ethereum", + "origin_denom": "tbtc", + "origin_type": "erc20", + "symbol": "tBTC", + "decimals": 8, + "enable": true, + "path": "ethereum>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/BhqTYfQogyt7jX7cx7x8uHEQP1x9fdtdBZtK4Swghgpw" + }, + "image": "ethereum/asset/tbtc.png", + "coinGeckoId": "tbtc", + "contract": "0x18084fbA666a33d37592fA2633fD49a74DD93a88" + }, + { + "denom": "ibc/B1C287C2701774522570010EEBCD864BCB7AB714711B3AA218699FDD75E832F5", + "type": "ibc", + "origin_chain": "sui", + "origin_denom": "sui", + "origin_type": "native", + "symbol": "SUI", + "decimals": 8, + "enable": true, + "path": "sui>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/46YEtoSN1AcwgGSRoWruoS6bnVh8XpMp5aQTpKohCJYh" + }, + "image": "sui/asset/sui.png", + "coinGeckoId": "sui", + "contract": "0x2::sui::SUI" + }, + { + "denom": "ibc/A4D176906C1646949574B48C1928D475F2DF56DE0AC04E1C99B08F90BC21ABDE", + "type": "ibc", + "origin_chain": "aptos", + "origin_denom": "aptos", + "origin_type": "native", + "symbol": "APT", + "decimals": 8, + "enable": true, + "path": "aptos>gateway>osmosis", + "channel": "channel-2186", + "port": "transfer", + "counter_party": { + "channel": "channel-3", + "port": "transfer", + "denom": "factory/wormhole14ejqjyq8um4p3xfqj74yld5waqljf88fz25yxnma0cngspxe3les00fpjx/5wS2fGojbL9RhGEAeQBdkHPUAciYDxjDTMYvdf9aDn2r" + }, + "image": "aptos/asset/aptos.png", + "coinGeckoId": "aptos", + "contract": "0x1::aptos_coin::AptosCoin" + }, + { + "denom": "ibc/208B2F137CDE510B44C41947C045CFDC27F996A9D990EA64460BDD5B3DBEB2ED", + "type": "ibc", + "origin_chain": "passage", + "origin_denom": "upasg", + "origin_type": "native", + "symbol": "PASG", + "decimals": 6, + "enable": true, + "path": "passage>osmosis", + "channel": "channel-2494", + "port": "transfer", + "counter_party": { + "channel": "channel-0", + "port": "transfer", + "denom": "upasg" + }, + "image": "passage/asset/pasg.png", + "coinGeckoId": "passage" + }, + { + "denom": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", + "type": "ibc", + "origin_chain": "noble", + "origin_denom": "uusdc", + "origin_type": "native", + "symbol": "USDC", + "decimals": 6, + "enable": true, + "path": "noble>osmosis", + "channel": "channel-750", "port": "transfer", "counter_party": { "channel": "channel-1", "port": "transfer", - "denom": "attoaioz" + "denom": "uusdc" }, - "image": "aioz/asset/aioz.png", - "coinGeckoId": "aioz-network" + "image": "noble/asset/usdc.png", + "coinGeckoId": "usd-coin" }, { - "denom": "ibc/C360EF34A86D334F625E4CBB7DA3223AEA97174B61F35BB3758081A8160F7D9B", + "denom": "ibc/D79E7D83AB399BFFF93433E54FAA480C191248FC556924A2A8351AE2638B3877", "type": "ibc", - "origin_chain": "odin", - "origin_denom": "loki", + "origin_chain": "celestia", + "origin_denom": "utia", "origin_type": "staking", - "symbol": "ODIN", + "symbol": "TIA", "decimals": 6, "enable": true, - "path": "odin>osmosis", - "channel": "channel-258", + "path": "celestia>osmosis", + "channel": "channel-6994", "port": "transfer", "counter_party": { - "channel": "channel-3", + "channel": "channel-2", "port": "transfer", - "denom": "loki" + "denom": "utia" }, - "image": "odin/asset/odin.png", - "coinGeckoId": "odin-protocol" + "image": "celestia/asset/tia.png", + "coinGeckoId": "celestia" }, { - "denom": "ibc/10E5E5B06D78FFBB61FD9F89209DEE5FD4446ED0550CBB8E3747DA79E10D9DC6", + "denom": "ibc/FBB3FEF80ED2344D821D4F95C31DBFD33E4E31D5324CAD94EF756E67B749F668", "type": "ibc", - "origin_chain": "arbitrum", - "origin_denom": "arb", - "origin_type": "native", - "symbol": "ARB", + "origin_chain": "ethereum", + "origin_denom": "wbtc", + "origin_type": "erc20", + "symbol": "yieldeth-wei", "decimals": 18, "enable": true, - "path": "arbitrum>axelar>osmosis", + "path": "ethereum>axelar>osmosis", "channel": "channel-208", "port": "transfer", "counter_party": { "channel": "channel-3", "port": "transfer", - "denom": "arb-wei" + "denom": "yieldeth-wei" }, - "image": "arbitrum/asset/arb.png", - "coinGeckoId": "arbitrum", - "contract": "0x912ce59144191c1204e64559fe8253a0e49e6548" + "image": "ethereum/asset/yieldeth.png", + "coinGeckoId": "yieldeth-sommelier", + "contract": "0xb5b29320d2Dde5BA5BAFA1EbcD270052070483ec" }, { - "denom": "ibc/EDD6F0D66BCD49C1084FB2C35353B4ACD7B9191117CE63671B61320548F7C89D", + "denom": "factory/osmo1mlng7pz4pnyxtpq0akfwall37czyk9lukaucsrn30ameplhhshtqdvfm5c/ulvn", + "type": "native", + "origin_chain": "osmosis", + "origin_denom": "factory/osmo1mlng7pz4pnyxtpq0akfwall37czyk9lukaucsrn30ameplhhshtqdvfm5c/ulvn", + "origin_type": "native", + "symbol": "LVN", + "decimals": 6, + "description": "Levana Governance Token", + "image": "osmosis/asset/levana.png", + "coinGeckoId": "levana-protocol" + }, + { + "denom": "ibc/613BF0BF2F2146AE9941E923725745E931676B2C14E9768CD609FA0849B2AE13", "type": "ibc", - "origin_chain": "whitewhale", - "origin_denom": "uwhite", - "origin_type": "erc20", - "symbol": "WHALE", + "origin_chain": "kyve", + "origin_denom": "ukyve", + "origin_type": "staking", + "symbol": "KYVE", "decimals": 6, "enable": true, - "path": "whitewhale>osmosis", - "channel": "channel-642", + "path": "kyve>osmosis", + "channel": "channel-767", "port": "transfer", "counter_party": { - "channel": "channel-5", + "channel": "channel-0", "port": "transfer", - "denom": "uwhite" + "denom": "ukyve" }, - "image": "whitewhale/asset/whale.png", - "coinGeckoId": "white-whale" + "image": "kyve/asset/kyve.png", + "coinGeckoId": "kyve-network" }, { - "denom": "ibc/23CA6C8D1AB2145DD13EB1E089A2E3F960DC298B468CCE034E19E5A78B61136E", + "denom": "ibc/B1E0166EA0D759FDF4B207D1F5F12210D8BFE36F2345CEFC76948CE2B36DFBAF", "type": "ibc", - "origin_chain": "comdex", - "origin_denom": "ucmst", - "origin_type": "native", - "symbol": "CMST", - "decimals": 6, + "origin_chain": "Planq", + "origin_denom": "aplanq", + "origin_type": "staking", + "symbol": "PLQ", + "decimals": 18, "enable": true, - "path": "comdex>osmosis", - "channel": "channel-87", + "path": "planq>osmosis", + "channel": "channel-492", "port": "transfer", "counter_party": { "channel": "channel-1", "port": "transfer", - "denom": "ucmst" + "denom": "aplanq" }, - "image": "comdex/asset/cmst.png", - "coinGeckoId": "composite" + "image": "Planq/asset/planq.png", + "coinGeckoId": "planq" }, { - "denom": "ibc/02F196DA6FD0917DD5FEA249EE61880F4D941EE9059E7964C5C9B50AF103800F", - "type": "ibc", - "origin_chain": "stride", - "origin_denom": "stuumee", + "denom": "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", + "type": "native", + "origin_chain": "osmosis", + "origin_denom": "factory/osmo1f5vfcph2dvfeqcqkhetwv75fda69z7e5c2dldm3kvgj23crkv6wqcn47a0/umilkTIA", "origin_type": "native", - "symbol": "stUMEE", + "symbol": "milkTIA", + "decimals": 6, + "description": "MilkyWay's liquid staked TIA", + "image": "osmosis/asset/milktia.png", + "coinGeckoId": "milkyway-staked-tia" + }, + { + "denom": "ibc/6928AFA9EA721938FED13B051F9DBF1272B16393D20C49EA5E4901BB76D94A90", + "type": "ibc", + "origin_chain": "nois", + "origin_denom": "unois", + "origin_type": "staking", + "symbol": "NOIS", "decimals": 6, "enable": true, - "path": "stride>osmosis", - "channel": "channel-326", + "path": "nois>osmosis", + "channel": "channel-8277", "port": "transfer", "counter_party": { - "channel": "channel-5", + "channel": "channel-37", "port": "transfer", - "denom": "stuumee" + "denom": "unois" }, - "image": "stride/asset/stumee.png", - "coinGeckoId": "" + "image": "nois/asset/nois.png" }, { - "denom": "ibc/C5579A9595790017C600DD726276D978B9BF314CF82406CE342720A9C7911A01", + "denom": "ibc/9B8EC667B6DF55387DC0F3ACC4F187DA6921B0806ED35DE6B04DE96F5AB81F53", "type": "ibc", - "origin_chain": "stride", - "origin_denom": "staevmos", + "origin_chain": "chihuahua", + "origin_denom": "factory/chihuahua13jawsn574rf3f0u5rhu7e8n6sayx5gkw3eddhp/uwoof", "origin_type": "native", - "symbol": "stEVMOS", - "decimals": 18, + "symbol": "WOOF", + "decimals": 6, "enable": true, - "path": "stride>osmosis", - "channel": "channel-326", + "path": "chihuahua>osmosis", + "channel": "channel-113", "port": "transfer", "counter_party": { - "channel": "channel-5", + "channel": "channel-7", "port": "transfer", - "denom": "staevmos" + "denom": "factory/chihuahua13jawsn574rf3f0u5rhu7e8n6sayx5gkw3eddhp/uwoof" }, - "image": "stride/asset/stevmos.png", + "image": "chihuahua/asset/woof.png", "coinGeckoId": "" }, { - "denom": "ibc/5DD1F95ED336014D00CE2520977EC71566D282F9749170ADC83A392E0EA7426A", + "denom": "ibc/95C9B5870F95E21A242E6AF9ADCB1F212EE4A8855087226C36FBE43FC41A77B8", "type": "ibc", - "origin_chain": "stride", - "origin_denom": "stustars", - "origin_type": "native", - "symbol": "stSTARS", - "decimals": 6, + "origin_chain": "xpla", + "origin_denom": "axpla", + "origin_type": "staking", + "symbol": "XPLA", + "decimals": 18, + "description": "XPLA Staking Coin", "enable": true, - "path": "stride>osmosis", - "channel": "channel-326", + "path": "xpla>osmosis", + "channel": "channel-1634", "port": "transfer", "counter_party": { - "channel": "channel-5", + "channel": "channel-9", "port": "transfer", - "denom": "stustars" + "denom": "axpla" }, - "image": "stride/asset/ststars.png", - "coinGeckoId": "stride-staked-stars" + "image": "xpla/asset/xpla.png", + "coinGeckoId": "xpla" }, { - "denom": "ibc/84502A75BCA4A5F68D464C00B3F610CE2585847D59B52E5FFB7C3C9D2DDCD3FE", + "denom": "ibc/37CB3078432510EE57B9AFA8DBE028B33AE3280A144826FEAC5F2334CF2C5539", "type": "ibc", - "origin_chain": "stride", - "origin_denom": "stujuno", + "origin_chain": "nyx", + "origin_denom": "unym", "origin_type": "native", - "symbol": "stJUNO", + "symbol": "NYM", "decimals": 6, "enable": true, - "path": "stride>osmosis", - "channel": "channel-326", + "path": "nyx>osmosis", + "channel": "channel-15464", "port": "transfer", "counter_party": { - "channel": "channel-5", + "channel": "channel-8", "port": "transfer", - "denom": "stujuno" + "denom": "unym" }, - "image": "stride/asset/stjuno.png", - "coinGeckoId": "stride-staked-juno" + "image": "nyx/asset/nym.png" }, { - "denom": "ibc/1B708808D372E959CD4839C594960309283424C775F4A038AAEBE7F83A988477", + "denom": "factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc", + "type": "native", + "origin_chain": "ethereum", + "origin_denom": "factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc", + "origin_type": "native", + "symbol": "wbtc", + "decimals": 8, + "description": "Native Coin", + "image": "ethereum/asset/wbtc.png", + "coinGeckoId": "wrapped-bitcoin" + }, + { + "denom": "ibc/94ED1F172BC633DFC56D7E26551D8B101ADCCC69052AC44FED89F97FF658138F", "type": "ibc", - "origin_chain": "quasar", - "origin_denom": "uqsr", - "origin_type": "staking", - "symbol": "QSR", + "origin_chain": "stargaze", + "origin_denom": "factory/stars1xx5976njvxpl9n4v8huvff6cudhx7yuu8e7rt4/usneaky", + "origin_type": "native", + "symbol": "SNEAKY", "decimals": 6, "enable": true, - "path": "quasar>osmosis", - "channel": "channel-688", + "path": "stargaze>osmosis", + "channel": "channel-75", "port": "transfer", "counter_party": { - "channel": "channel-1", + "channel": "channel-0", "port": "transfer", - "denom": "uqsr" + "denom": "factory/stars1xx5976njvxpl9n4v8huvff6cudhx7yuu8e7rt4/usneaky" }, - "image": "quasar/asset/qsr.png", - "coinGeckoId": "quasar-2" + "image": "stargaze/asset/sneaky.png", + "coinGeckoId": "" }, { - "denom": "ibc/23AB778D694C1ECFC59B91D8C399C115CC53B0BD1C61020D8E19519F002BDD85", + "denom": "ibc/8E697BDABE97ACE8773C6DF7402B2D1D5104DD1EEABE12608E3469B7F64C15BA", "type": "ibc", - "origin_chain": "archway", - "origin_denom": "aarch", - "origin_type": "staking", - "symbol": "ARCH", - "decimals": 18, + "origin_chain": "jackal", + "origin_denom": "ujkl", + "origin_type": "native", + "symbol": "JKL", + "decimals": 6, "enable": true, - "path": "archway>osmosis", - "channel": "channel-1429", + "path": "jackal>osmosis", + "channel": "channel-412", "port": "transfer", "counter_party": { - "channel": "channel-1", + "channel": "channel-0", "port": "transfer", - "denom": "aarch" + "denom": "ujkl" }, - "image": "archway/asset/arch.png", - "coinGeckoId": "archway" + "image": "jackal/asset/jkl.png", + "coinGeckoId": "jackal-protocol" }, { - "denom": "ibc/9B6FBABA36BB4A3BF127AE5E96B572A5197FD9F3111D895D8919B07BC290764A", - "type": "ibc", - "origin_chain": "odin", - "origin_denom": "mgeo", + "denom": "factory/osmo1pfyxruwvtwk00y8z06dh2lqjdj82ldvy74wzm3/WOSMO", + "type": "native", + "origin_chain": "osmosis", + "origin_denom": "factory/osmo1pfyxruwvtwk00y8z06dh2lqjdj82ldvy74wzm3/WOSMO", "origin_type": "native", - "symbol": "GEO", + "symbol": "WOSMO", "decimals": 6, + "description": "The first native memecoin on Osmosis. Crafted by the deftest of hands in the lab of lunacy. It's scientifically anarchic, professionally foolish, and your ticket to the madhouse.", + "image": "osmosis/asset/wosmo.png", + "coinGeckoId": "" + }, + { + "denom": "ibc/9A76CDF0CBCEF37923F32518FA15E5DC92B9F56128292BC4D63C4AEA76CBB110", + "type": "ibc", + "origin_chain": "dymension", + "origin_denom": "adym", + "origin_type": "staking", + "symbol": "DYM", + "decimals": 18, "enable": true, - "path": "odin>osmosis", - "channel": "channel-258", + "path": "dymension>osmosis", + "channel": "channel-19774", "port": "transfer", "counter_party": { - "channel": "channel-3", + "channel": "channel-2", "port": "transfer", - "denom": "mgeo" + "denom": "adym" }, - "image": "odin/asset/geo.png", - "coinGeckoId": "geodb" + "image": "dymension/asset/dym.png", + "coinGeckoId": "dymension" }, { - "denom": "ibc/0CD46223FEABD2AEAAAF1F057D01E63BCA79B7D4BD6B68F1EB973A987344695D", - "type": "ibc", - "origin_chain": "odin", - "origin_denom": "mO9W", + "denom": "factory/osmo10n8rv8npx870l69248hnp6djy6pll2yuzzn9x8/BADKID", + "type": "native", + "origin_chain": "osmosis", + "origin_denom": "factory/osmo10n8rv8npx870l69248hnp6djy6pll2yuzzn9x8/BADKID", "origin_type": "native", - "symbol": "O9W", + "symbol": "BADKID", "decimals": 6, - "enable": true, - "path": "odin>osmosis", - "channel": "channel-258", - "port": "transfer", - "counter_party": { - "channel": "channel-3", - "port": "transfer", - "denom": "mO9W" - }, - "image": "odin/asset/o9w.png" + "description": "A clan of 11y bad kids crafting chaos on the Cosmos eco. One bad memecoin to rule them all $BADKID. Airdropped to Badkids NFT holders and $STARS stakers. It's so bad, your wallet's throwing a tantrum for it.", + "image": "osmosis/asset/badkid.png", + "coinGeckoId": "" } ], "passage": [ @@ -4123,7 +4899,8 @@ "decimals": 6, "description": "Passage Staking Coin", "image": "passage/asset/pasg.png", - "coinGeckoId": "passage" + "coinGeckoId": "passage", + "color": "#5DA5FB" } ], "regen": [ @@ -4137,7 +4914,8 @@ "decimals": 6, "description": "Regen Staking Coin", "image": "regen/asset/regen.png", - "coinGeckoId": "regen" + "coinGeckoId": "regen", + "color": "#43AD6B" }, { "denom": "eco.uC.NCT", @@ -4223,7 +5001,8 @@ "decimals": 6, "description": "Stargaze Staking Coin", "image": "stargaze/asset/stars.png", - "coinGeckoId": "stargaze" + "coinGeckoId": "stargaze", + "color": "#E38CD4" }, { "denom": "ibc/448C1061CE97D86CC5E86374CD914870FB8EBA16C58661B5F1D3F46729A2422D", @@ -4357,15 +5136,47 @@ "coinGeckoId": "" }, { - "denom": "factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/mGAZE", + "denom": "factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/uBRNCH", + "type": "native", + "origin_chain": "stargaze", + "origin_denom": "factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/uBRNCH", + "origin_type": "native", + "symbol": "BRNCH", + "decimals": 6, + "description": "ohhNFT LP token.", + "image": "stargaze/asset/brnch.png", + "coinGeckoId": "" + }, + { + "denom": "ibc/4A1C18CA7F50544760CF306189B810CE4C1CB156C7FC870143D401FE7280E591", + "type": "ibc", + "origin_chain": "noble", + "origin_denom": "uusdc", + "origin_type": "native", + "symbol": "USDC", + "decimals": 6, + "enable": true, + "path": "noble>stargaze", + "channel": "channel-204", + "port": "transfer", + "counter_party": { + "channel": "channel-11", + "port": "transfer", + "denom": "uusdc" + }, + "image": "noble/asset/usdc.png", + "coinGeckoId": "usd-coin" + }, + { + "denom": "factory/stars1xx5976njvxpl9n4v8huvff6cudhx7yuu8e7rt4/usneaky", "type": "native", "origin_chain": "stargaze", - "origin_denom": "factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/mGAZE", + "origin_denom": "factory/stars1xx5976njvxpl9n4v8huvff6cudhx7yuu8e7rt4/usneaky", "origin_type": "native", - "symbol": "GAZE", + "symbol": "SNEAKY", "decimals": 6, - "description": "The native meme token of stargaze.", - "image": "stargaze/asset/gaze.png", + "description": "The native coin of Sneaky Productions", + "image": "stargaze/asset/sneaky.png", "coinGeckoId": "" } ], @@ -4380,7 +5191,8 @@ "decimals": 6, "description": "Umee Staking Coin", "image": "umee/asset/umee.png", - "coinGeckoId": "umee" + "coinGeckoId": "umee", + "color": "#BB90F8" }, { "denom": "ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518", @@ -4460,7 +5272,7 @@ "denom": "weth-wei" }, "image": "ethereum/asset/weth.png", - "coinGeckoId": "weth", + "coinGeckoId": "axlweth", "contract": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, { @@ -4481,7 +5293,7 @@ "denom": "wbtc-satoshi" }, "image": "ethereum/asset/wbtc.png", - "coinGeckoId": "wrapped-bitcoin", + "coinGeckoId": "axlwbtc", "contract": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" }, @@ -4503,7 +5315,7 @@ "denom": "uusdc" }, "image": "ethereum/asset/usdc.png", - "coinGeckoId": "usd-coin", + "coinGeckoId": "axlusdc", "contract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, { @@ -4727,28 +5539,8 @@ "denom": "uusdt" }, "image": "ethereum/asset/usdt.png", - "coinGeckoId": "tether", + "coinGeckoId": "axelar-usdt", "contract": "0xdac17f958d2ee523a2206206994597c13d831ec7" - }, - { - "denom": "ibc/452372B8214E22C625E98958C5EDFB939C48E6590E40B711E9D30206EF8EDC9B", - "type": "ibc", - "origin_chain": "terra", - "origin_denom": "uluna", - "origin_type": "staking", - "symbol": "LUNA", - "decimals": 6, - "enable": true, - "path": "terra>umee", - "channel": "channel-45", - "port": "transfer", - "counter_party": { - "channel": "channel-93", - "port": "transfer", - "denom": "uluna" - }, - "image": "terra/asset/luna.png", - "coinGeckoId": "terra-luna-2" } ], "agoric": [], @@ -4763,7 +5555,8 @@ "decimals": 6, "description": "Desmos Staking Coin", "image": "desmos/asset/dsm.png", - "coinGeckoId": "desmos" + "coinGeckoId": "desmos", + "color": "#ED6C53" }, { "denom": "ibc/13B2C536BB057AC79D5616B8EA1B9540EC1F2170718CAFF6F0083C966FFFED0B", @@ -4817,7 +5610,8 @@ "decimals": 6, "description": "OmniFlix Staking Coin", "image": "omniflix/asset/flix.png", - "coinGeckoId": "omniflix-network" + "coinGeckoId": "omniflix-network", + "color": "#6C63FF" }, { "denom": "ibc/A8C2D23A1E6F95DA4E48BA349667E322BD7A6C996D8A4AAE8BA72E190F3D1477", @@ -4931,7 +5725,8 @@ "decimals": 6, "description": "Quicksilver Staking Coin", "image": "quicksilver/asset/qck.png", - "coinGeckoId": "quicksilver" + "coinGeckoId": "quicksilver", + "color": "#9B9B9B" }, { "denom": "uqstars", @@ -5145,7 +5940,8 @@ "decimals": 6, "description": "Tgrade Staking Coin", "image": "tgrade/asset/tgd.png", - "coinGeckoId": "tgrade" + "coinGeckoId": "tgrade", + "color": "#C7177B" }, { "denom": "ibc/4B322204B4F59D770680FE4D7A565DDC3F37BFF035474B717476C66A4F83DD72", diff --git a/frontend/src/utils/chainsInfo.ts b/frontend/src/utils/chainsInfo.ts index aab4372ab..e8c58e0d1 100644 --- a/frontend/src/utils/chainsInfo.ts +++ b/frontend/src/utils/chainsInfo.ts @@ -1,3 +1,5 @@ +import { COSMOSTATION, KEPLR, LEAP } from "@/constants/wallet"; + export const networks: Network[] = [ { enableModules: { @@ -16,15 +18,26 @@ export const networks: Network[] = [ toolbar: 'https://raw.githubusercontent.com/vitwit/chain-registry/master/agoric/images/bld.png', }, + isCustomNetwork: false, + supportedWallets: [], keplrExperimental: false, leapExperimental: false, isTestnet: false, + govV1: false, explorerTxHashEndpoint: 'https://atomscan.com/agoric/transactions/', config: { chainId: 'agoric-3', chainName: 'Agoric', - rest: 'https://agoric-api.polkachu.com', - rpc: 'https://agoric-rpc.polkachu.com', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://agoric-rpc.polkachu.com', + 'https://rpc-agoric-ia.cosmosia.notional.ventures', + 'https://agoric-rpc.stakeandrelax.net', + ], currencies: [ { coinDenom: 'BLD', @@ -75,80 +88,89 @@ export const networks: Network[] = [ theme: { primaryColor: '#fff', gradient: - 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', + 'linear-gradient(180deg, #BF2A4560 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: true, + feegrant: true, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/akash/akt.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/akash/images/akash-logo.png', + }, + keplrExperimental: false, + leapExperimental: false, + supportedWallets: [KEPLR, LEAP, COSMOSTATION], + isTestnet: false, + govV1: false, + isCustomNetwork: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/akash/txs/', + config: { + chainId: 'akashnet-2', + chainName: 'Akash', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://akash-rpc.lavenderfive.com:443', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://akash-rpc.lavenderfive.com:443', + 'https://akash-rpc.polkachu.com', + 'https://rpc-akash.cosmos-spaces.cloud', + 'https://api.resolute.vitwit.com/akash_rpc', + ], + currencies: [ + { + coinDenom: 'AKT', + coinMinimalDenom: 'uakt', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'akash', + bech32PrefixAccPub: 'akashpub', + bech32PrefixValAddr: 'akashvaloper', + bech32PrefixValPub: 'akashvaloperpub', + bech32PrefixConsAddr: 'akashgvalcons', + bech32PrefixConsPub: 'akashvalconspub', + }, + bip44: { + coinType: 118, + }, + feeCurrencies: [ + { + coinDenom: 'AKT', + coinMinimalDenom: 'uakt', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.03, + }, + }, + ], + stakeCurrency: { + coinDenom: 'AKT', + coinMinimalDenom: 'uakt', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#F24E29', + gradient: 'linear-gradient(180deg, #F24E2960 0%, #12131C80 100%)', }, }, }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/akash/akt.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/akash/images/akash-logo.png', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/akash/txs/', - // config: { - // chainId: 'akashnet-2', - // chainName: 'Akash', - // rest: 'https://api.resolute.vitwit.com/akash_api', - // rpc: 'https://api.resolute.vitwit.com/akash_rpc', - // currencies: [ - // { - // coinDenom: 'AKT', - // coinMinimalDenom: 'uakt', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'akash', - // bech32PrefixAccPub: 'akashpub', - // bech32PrefixValAddr: 'akashvaloper', - // bech32PrefixValPub: 'akashvaloperpub', - // bech32PrefixConsAddr: 'akashgvalcons', - // bech32PrefixConsPub: 'akashvalconspub', - // }, - // bip44: { - // coinType: 118, - // }, - // feeCurrencies: [ - // { - // coinDenom: 'AKT', - // coinMinimalDenom: 'uakt', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.01, - // average: 0.025, - // high: 0.03, - // }, - // }, - // ], - // stakeCurrency: { - // coinDenom: 'AKT', - // coinMinimalDenom: 'uakt', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, { enableModules: { authz: true, @@ -166,15 +188,24 @@ export const networks: Network[] = [ toolbar: 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/cosmoshub/images/cosmoshub-logo.png', }, + supportedWallets: [KEPLR, LEAP, COSMOSTATION], keplrExperimental: false, leapExperimental: false, isTestnet: false, + govV1: true, + isCustomNetwork: false, explorerTxHashEndpoint: 'https://www.mintscan.io/cosmos/txs/', config: { chainId: 'cosmoshub-4', chainName: 'CosmosHub', - rest: 'https://api-cosmoshub-ia.cosmosia.notional.ventures', + rest: 'https://api.resolute.vitwit.com', rpc: 'https://cosmos-rpc.polkachu.com', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://cosmos-rpc.polkachu.com', + 'https://rpc-cosmoshub.blockapsis.com', + 'https://cosmos-rpc.quickapi.com:443', + ], currencies: [ { coinDenom: 'ATOM', @@ -210,155 +241,173 @@ export const networks: Network[] = [ coinMinimalDenom: 'uatom', coinDecimals: 6, }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#272B40', + gradient: 'linear-gradient(180deg, #272B4060 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: true, + feegrant: true, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/desmos/dsm.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/master/desmos/images/dsm.png', + }, + keplrExperimental: true, + leapExperimental: true, + isTestnet: false, + govV1: true, + isCustomNetwork: false, + supportedWallets: [], + explorerTxHashEndpoint: 'https://www.mintscan.io/desmos/txs/', + config: { + chainId: 'desmos-mainnet', + chainName: 'Desmos', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://rpc.mainnet.desmos.network', + 'https://desmos-rpc.lavenderfive.com', + 'https://api.resolute.vitwit.com/desmos_rpc', + ], + currencies: [ + { + coinDenom: 'DSM', + coinMinimalDenom: 'udsm', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'desmos', + bech32PrefixAccPub: 'desmospub', + bech32PrefixValAddr: 'desmosvaloper', + bech32PrefixValPub: 'desmosvaloperpub', + bech32PrefixConsAddr: 'desmosgvalcons', + bech32PrefixConsPub: 'desmosvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'DSM', + coinMinimalDenom: 'udsm', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.03, + high: 0.05, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'DSM', + coinMinimalDenom: 'udsm', + coinDecimals: 6, + }, image: 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', theme: { primaryColor: '#fff', gradient: - 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', + 'linear-gradient(180deg, #F27D5260 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: true, + feegrant: true, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/evmos/evmos.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/evmos/images/evmos-logo.png', + }, + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/evmos/txs/', + govV1: false, + config: { + chainId: 'evmos_9001-2', + chainName: 'Evmos', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://rpc-evmos-ia.cosmosia.notional.ventures:443', + 'https://evmos-rpc.polkachu.com', + 'https://evmos-rpc.publicnode.com:443', + ], + currencies: [ + { + coinDenom: 'EVMOS', + coinMinimalDenom: 'aevmos', + coinDecimals: 18, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'evmos', + bech32PrefixAccPub: 'evmospub', + bech32PrefixValAddr: 'evmosvaloper', + bech32PrefixValPub: 'evmosvaloperpub', + bech32PrefixConsAddr: 'evmosgvalcons', + bech32PrefixConsPub: 'evmosvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'EVMOS', + coinMinimalDenom: 'aevmos', + coinDecimals: 18, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.03, + }, + }, + ], + bip44: { + coinType: 60, + }, + stakeCurrency: { + coinDenom: 'EVMOS', + coinMinimalDenom: 'aevmos', + coinDecimals: 18, + }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#fff', + gradient: 'linear-gradient(180deg, #F2453560 0%, #12131C80 100%)', }, }, + isCustomNetwork: false, + supportedWallets: [] }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/desmos/dsm.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/master/desmos/images/dsm.png', - // }, - // keplrExperimental: true, - // leapExperimental: true, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/desmos/txs/', - // config: { - // chainId: 'desmos-mainnet', - // chainName: 'Desmos', - // rest: 'https://api.resolute.vitwit.com/desmos_api', - // rpc: 'https://api.resolute.vitwit.com/desmos_rpc', - // currencies: [ - // { - // coinDenom: 'DSM', - // coinMinimalDenom: 'udsm', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'desmos', - // bech32PrefixAccPub: 'desmospub', - // bech32PrefixValAddr: 'desmosvaloper', - // bech32PrefixValPub: 'desmosvaloperpub', - // bech32PrefixConsAddr: 'desmosgvalcons', - // bech32PrefixConsPub: 'desmosvalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'DSM', - // coinMinimalDenom: 'udsm', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.01, - // average: 0.03, - // high: 0.05, - // }, - // }, - // ], - // bip44: { - // coinType: 118, - // }, - // stakeCurrency: { - // coinDenom: 'DSM', - // coinMinimalDenom: 'udsm', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/evmos/evmos.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/evmos/images/evmos-logo.png', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/evmos/txs/', - // config: { - // chainId: 'evmos_9001-2', - // chainName: 'Evmos', - // rest: 'https://evmos.kingnodes.com', - // rpc: 'https://rpc-evmos.ecostake.com', - // currencies: [ - // { - // coinDenom: 'EVMOS', - // coinMinimalDenom: 'aevmos', - // coinDecimals: 18, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'evmos', - // bech32PrefixAccPub: 'evmospub', - // bech32PrefixValAddr: 'evmosvaloper', - // bech32PrefixValPub: 'evmosvaloperpub', - // bech32PrefixConsAddr: 'evmosgvalcons', - // bech32PrefixConsPub: 'evmosvalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'EVMOS', - // coinMinimalDenom: 'aevmos', - // coinDecimals: 18, - // gasPriceStep: { - // low: 0.01, - // average: 0.025, - // high: 0.03, - // }, - // }, - // ], - // bip44: { - // coinType: 60, - // }, - // stakeCurrency: { - // coinDenom: 'ATOM', - // coinMinimalDenom: 'uatom', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, { enableModules: { authz: true, @@ -379,12 +428,20 @@ export const networks: Network[] = [ keplrExperimental: false, leapExperimental: false, isTestnet: false, + govV1: true, explorerTxHashEndpoint: 'https://www.mintscan.io/juno/txs/', config: { chainId: 'juno-1', chainName: 'Juno', - rest: 'https://api.resolute.vitwit.com/juno_api', - rpc: 'https://api.resolute.vitwit.com/juno_rpc', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://juno-rpc.lavenderfive.com:443', + 'https://juno-rpc.polkachu.com', + 'https://rpc-juno.ecostake.com', + 'https://api.resolute.vitwit.com/juno_rpc', + ], currencies: [ { coinDenom: 'JUNO', @@ -420,229 +477,176 @@ export const networks: Network[] = [ coinMinimalDenom: 'ujuno', coinDecimals: 6, }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#fff', + gradient: 'linear-gradient(180deg, #F2798360 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [], + }, + { + enableModules: { + authz: true, + feegrant: true, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/omniflixhub/flix.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/master/omniflixhub/images/flix.png', + }, + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + govV1: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/omniflix/txs/', + config: { + chainId: 'omniflixhub-1', + chainName: 'OmniflixHub', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://rpc-omniflixhub-ia.cosmosia.notional.ventures', + 'https://omniflix-rpc.publicnode.com:443', + 'https://omniflixhub-rpc.lavenderfive.com', + 'https://api.resolute.vitwit.com/omniflix_rpc', + ], + currencies: [ + { + coinDenom: 'FLIX', + coinMinimalDenom: 'uflix', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'omniflix', + bech32PrefixAccPub: 'omniflixpub', + bech32PrefixValAddr: 'omniflixvaloper', + bech32PrefixValPub: 'omniflixvaloperpub', + bech32PrefixConsAddr: 'omniflixgvalcons', + bech32PrefixConsPub: 'omniflixvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'FLIX', + coinMinimalDenom: 'uflix', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.0025, + high: 0.025, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'FLIX', + coinMinimalDenom: 'uflix', + coinDecimals: 6, + }, image: 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', theme: { primaryColor: '#fff', gradient: - 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', + 'linear-gradient(180deg, #D91E7560 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: false, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/osmosis/osmo.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/osmosis/images/osmosis-logo.png', + }, + supportedWallets: [KEPLR, LEAP, COSMOSTATION], + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + govV1: true, + isCustomNetwork: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/osmosis/txs/', + config: { + chainId: 'osmosis-1', + chainName: 'Osmosis', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://rpc.osmosis.zone', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://rpc.osmosis.zone', + 'https://rpc-osmosis.blockapsis.com', + 'https://osmosis-rpc.quickapi.com:443', + ], + currencies: [ + { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'osmo', + bech32PrefixAccPub: 'osmopub', + bech32PrefixValAddr: 'osmovaloper', + bech32PrefixValPub: 'osmovaloperpub', + bech32PrefixConsAddr: 'osmogvalcons', + bech32PrefixConsPub: 'osmovalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.03, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#5A0DA6', + gradient: 'linear-gradient(180deg, #5A0DA660 0%, #12131C80 100%)', }, }, }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/omniflixhub/flix.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/master/omniflixhub/images/flix.png', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/omniflix/txs/', - // config: { - // chainId: 'omniflixhub-1', - // chainName: 'OmniflixHub', - // rest: 'https://api.resolute.vitwit.com/omniflix_api', - // rpc: 'https://api.resolute.vitwit.com/omniflix_rpc', - // currencies: [ - // { - // coinDenom: 'FLIX', - // coinMinimalDenom: 'uflix', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'omniflix', - // bech32PrefixAccPub: 'omniflixpub', - // bech32PrefixValAddr: 'omniflixvaloper', - // bech32PrefixValPub: 'omniflixvaloperpub', - // bech32PrefixConsAddr: 'omniflixgvalcons', - // bech32PrefixConsPub: 'omniflixvalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'FLIX', - // coinMinimalDenom: 'uflix', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.01, - // average: 0.025, - // high: 0.03, - // }, - // }, - // ], - // bip44: { - // coinType: 118, - // }, - // stakeCurrency: { - // coinDenom: 'FLIX', - // coinMinimalDenom: 'uflix', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/osmosis/osmo.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/osmosis/images/osmosis-logo.png', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/osmosis/txs/', - // config: { - // chainId: 'osmosis-1', - // chainName: 'Osmosis', - // rest: 'https://osmosis-api.polkachu.com', - // rpc: 'https://rpc.osmosis.zone', - // currencies: [ - // { - // coinDenom: 'OSMO', - // coinMinimalDenom: 'uosmo', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'osmosis', - // bech32PrefixAccPub: 'osmosispub', - // bech32PrefixValAddr: 'osmosisvaloper', - // bech32PrefixValPub: 'osmosisvaloperpub', - // bech32PrefixConsAddr: 'osmosisgvalcons', - // bech32PrefixConsPub: 'osmosisvalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'OSMO', - // coinMinimalDenom: 'uosmo', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.01, - // average: 0.025, - // high: 0.03, - // }, - // }, - // ], - // bip44: { - // coinType: 118, - // }, - // stakeCurrency: { - // coinDenom: 'OSMO', - // coinMinimalDenom: 'uosmo', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: true, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/passage/pasg.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/aleem/staking-assets/passage3d/images/passage3d-logo.png', - // }, - // keplrExperimental: true, - // leapExperimental: true, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://passage.aneka.io/txs/', - // config: { - // chainId: 'passage-testnet-1', - // chainName: 'Passage-testnet', - // rest: 'https://api.resolute.vitwit.com/passage_testapi', - // rpc: 'https://api.resolute.vitwit.com/passage_testrpc', - // bip44: { - // coinType: 118, - // }, - // currencies: [ - // { - // coinDenom: 'PASG', - // coinMinimalDenom: 'upasg', - // coinDecimals: 6, - // coinGeckoId: 'passage', - // }, - // ], - // walletUrlForStaking: 'https://resolute.vitwit.com/passage/staking', - // bech32Config: { - // bech32PrefixAccAddr: 'pasg', - // bech32PrefixAccPub: 'pasgpub', - // bech32PrefixValAddr: 'pasgvaloper', - // bech32PrefixValPub: 'pasgvaloperpub', - // bech32PrefixConsAddr: 'pasgvalcons', - // bech32PrefixConsPub: 'pasgvalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'PASG', - // coinMinimalDenom: 'upasg', - // coinDecimals: 6, - // coinGeckoId: 'passage', - // gasPriceStep: { - // low: 0, - // average: 0, - // high: 0.01, - // }, - // }, - // ], - // stakeCurrency: { - // coinDenom: 'PASG', - // coinMinimalDenom: 'upasg', - // coinDecimals: 6, - // coinGeckoId: 'passage', - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, { enableModules: { authz: true, @@ -663,12 +667,22 @@ export const networks: Network[] = [ keplrExperimental: true, leapExperimental: true, isTestnet: false, + govV1: false, explorerTxHashEndpoint: 'https://mintscan.io/passage/txs/', config: { chainId: 'passage-2', chainName: 'Passage', - rest: 'https://api.passage.vitwit.com', - rpc: 'https://rpc.passage.vitwit.com', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://rpc.passage.vitwit.com', + 'https://rpc-passage.ecostake.com', + 'https://passage-rpc.polkachu.com', + 'https://rpc-passage-ia.cosmosia.notional.ventures', + ], bip44: { coinType: 118, }, @@ -713,9 +727,11 @@ export const networks: Network[] = [ theme: { primaryColor: '#fff', gradient: - 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', + 'linear-gradient(180deg, #72727360 0%, #12131C80 100%)', }, }, + isCustomNetwork: false, + supportedWallets: [] }, { enableModules: { @@ -734,15 +750,24 @@ export const networks: Network[] = [ toolbar: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/dydx/images/dydx.svg', }, + supportedWallets: [KEPLR, LEAP, COSMOSTATION], keplrExperimental: false, leapExperimental: true, isTestnet: false, + govV1: true, + isCustomNetwork: false, explorerTxHashEndpoint: 'https://mintscan.io/dydx/txs/', config: { chainId: 'dydx-mainnet-1', chainName: 'DYDX', - rest: 'https://dydx-rest.publicnode.com', + rest: 'https://api.resolute.vitwit.com', rpc: 'https://dydx-rpc.publicnode.com:443', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://dydx-rpc.publicnode.com:443', + 'https://dydx-dao-rpc.polkachu.com', + 'https://dydx-rpc.lavenderfive.com:443', + ], currencies: [ { coinDenom: 'DYDX', @@ -784,433 +809,1215 @@ export const networks: Network[] = [ image: 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', theme: { - primaryColor: '#fff', - gradient: - 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', + primaryColor: '#4F4DB5', + gradient: 'linear-gradient(180deg, #4F4DB560 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: true, + feegrant: true, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/quicksilver/qck.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/master/quicksilver/images/quicksilver-chain-logo.png', + }, + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + govV1: true, + explorerTxHashEndpoint: 'https://www.mintscan.io/quicksilver/txs/', + config: { + chainId: 'quicksilver-2', + chainName: 'Quicksilver', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://quicksilver-rpc.staketab.org:443', + 'https://rpc.quicksilver.zone:443', + 'https://quicksilver-rpc.lavenderfive.com:443', + 'https://api.resolute.vitwit.com/quicksilver_rpc', + ], + currencies: [ + { + coinDenom: 'QCK', + coinMinimalDenom: 'uqck', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'quick', + bech32PrefixAccPub: 'quickpub', + bech32PrefixValAddr: 'quickvaloper', + bech32PrefixValPub: 'quickvaloperpub', + bech32PrefixConsAddr: 'quickgvalcons', + bech32PrefixConsPub: 'quickvalconspub', + }, + bip44: { + coinType: 118, + }, + feeCurrencies: [ + { + coinDenom: 'QCK', + coinMinimalDenom: 'uqck', + coinDecimals: 6, + gasPriceStep: { + low: 0.0001, + average: 0.0001, + high: 0.00025, + }, + }, + ], + stakeCurrency: { + coinDenom: 'QCK', + coinMinimalDenom: 'uqck', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #BFBFBF60 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: true, + group: true, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/regen/regen.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/regen/images/regen-logo.png', + }, + keplrExperimental: false, + leapExperimental: true, + isTestnet: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/regen/txs/', + govV1: true, + config: { + chainId: 'regen-1', + chainName: 'Regen', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://regen-mainnet-rpc.autostake.com:443', + 'https://rpc-regen-ia.cosmosia.notional.ventures', + 'https://regen-rpc.publicnode.com:443', + 'https://api.resolute.vitwit.com/regen_rpc', + ], + currencies: [ + { + coinDenom: 'REGEN', + coinMinimalDenom: 'uregen', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'regen', + bech32PrefixAccPub: 'regenpub', + bech32PrefixValAddr: 'regenvaloper', + bech32PrefixValPub: 'regenvaloperpub', + bech32PrefixConsAddr: 'regengvalcons', + bech32PrefixConsPub: 'regenvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'REGEN', + coinMinimalDenom: 'uregen', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.03, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'REGEN', + coinMinimalDenom: 'uregen', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #5ABF9060 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: true, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/stargaze/stars.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/stargaze/images/stargaze-logo.png', + }, + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + govV1: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/stargaze/txs/', + config: { + chainId: 'stargaze-1', + chainName: 'Stargaze', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://stargaze-rpc.polkachu.com', + 'https://stargaze-rpc.publicnode.com:443', + 'https://rpc-stargaze-ia.cosmosia.notional.ventures', + ], + currencies: [ + { + coinDenom: 'STARS', + coinMinimalDenom: 'ustars', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'stars', + bech32PrefixAccPub: 'starspub', + bech32PrefixValAddr: 'starsvaloper', + bech32PrefixValPub: 'starsvaloperpub', + bech32PrefixConsAddr: 'starsgvalcons', + bech32PrefixConsPub: 'starsvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'STARS', + coinMinimalDenom: 'ustars', + coinDecimals: 6, + gasPriceStep: { + low: 1, + average: 1.1, + high: 1.2, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'STARS', + coinMinimalDenom: 'ustars', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #9AD9CD60 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: false, + feegrant: false, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/dymension/images/dymension-logo.svg', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/dymension/images/dymension-logo.svg', + }, + keplrExperimental: false, + leapExperimental: true, + isTestnet: false, + govV1: true, + explorerTxHashEndpoint: 'https://explorer.nodestake.org/dymension/tx/', + config: { + chainId: 'dymension_1100-1', + chainName: 'Dymension', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://rpc.dymension.nodestake.org', + 'https://dymension-mainnet-rpc.autostake.com:443', + 'https://dymension-rpc.lavenderfive.com:443', + ], + currencies: [ + { + coinDenom: 'DYM', + coinMinimalDenom: 'adym', + coinDecimals: 18, + }, + ], + bip44: { + coinType: 118, + }, + bech32Config: { + bech32PrefixAccAddr: 'dym', + bech32PrefixAccPub: 'dympub', + bech32PrefixValAddr: 'dymvaloper', + bech32PrefixValPub: 'dymvaloperpub', + bech32PrefixConsAddr: 'dymvalcons', + bech32PrefixConsPub: 'dymvalconspub', + }, + walletUrlForStaking: 'https://resolute.vitwit.com/dymension/staking', + feeCurrencies: [ + { + coinDenom: 'DYM', + coinMinimalDenom: 'adym', + coinDecimals: 18, + coinGeckoId: 'dym', + gasPriceStep: { + low: 0.02, + average: 0.02, + high: 0.02, + }, + }, + ], + stakeCurrency: { + coinDenom: 'DYM', + coinMinimalDenom: 'adym', + coinDecimals: 18, + coinGeckoId: 'dym', + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/dymension/images/dymension-logo.svg', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #e9c3a460 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: true, + group: true, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/umee/umee.png', + toolbar: + 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/umee/images/umee-logo.png', + }, + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/umee/txs/', + govV1: true, + config: { + chainId: 'umee-1', + chainName: 'Umee', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://umee-rpc.quantnode.tech', + 'https://rpc-umee-ia.cosmosia.notional.ventures', + 'https://umee-rpc.polkachu.com', + 'https://api.resolute.vitwit.com/umee_rpc', + ], + currencies: [ + { + coinDenom: 'UMEE', + coinMinimalDenom: 'uumee', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'umee', + bech32PrefixAccPub: 'umeepub', + bech32PrefixValAddr: 'umeevaloper', + bech32PrefixValPub: 'umeevaloperpub', + bech32PrefixConsAddr: 'umeegvalcons', + bech32PrefixConsPub: 'umeevalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'UMEE', + coinMinimalDenom: 'uumee', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.03, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'UMEE', + coinMinimalDenom: 'uumee', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #C9ACF260 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: true, + group: false, + }, + aminoConfig: { + authz: false, + feegrant: false, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/celestia/images/celestia.svg', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/celestia/images/celestia.svg', + }, + supportedWallets: [KEPLR, LEAP, COSMOSTATION], + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + govV1: false, + isCustomNetwork: false, + explorerTxHashEndpoint: 'https://mintscan.io/celestia/txs/', + config: { + chainId: 'celestia', + chainName: 'Celestia', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://public-celestia-rpc.numia.xyz', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://public-celestia-rpc.numia.xyz', + 'https://rpc.celestia.nodestake.top', + 'https://celestia-rpc.lavenderfive.com:443', + ], + currencies: [ + { + coinDenom: 'TIA', + coinMinimalDenom: 'utia', + coinDecimals: 6, + }, + ], + bip44: { + coinType: 118, + }, + bech32Config: { + bech32PrefixAccAddr: 'celestia', + bech32PrefixAccPub: 'celestiapub', + bech32PrefixValAddr: 'celestiavaloper', + bech32PrefixValPub: 'celestiavaloperpub', + bech32PrefixConsAddr: 'celestiavalcons', + bech32PrefixConsPub: 'celestiavalconspub', + }, + walletUrlForStaking: 'https://resolute.vitwit.com/celestia/staking', + feeCurrencies: [ + { + coinDenom: 'TIA', + coinMinimalDenom: 'utia', + coinDecimals: 6, + coinGeckoId: 'celestia', + gasPriceStep: { + low: 0.01, + average: 0.02, + high: 0.1, + }, + }, + ], + stakeCurrency: { + coinDenom: 'TIA', + coinMinimalDenom: 'utia', + coinDecimals: 6, + coinGeckoId: 'celestia', + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/celestia/images/celestia.svg', + theme: { + primaryColor: '#7A2BF9', + gradient: 'linear-gradient(180deg, #7A2BF960 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: true, + feegrant: true, + group: true, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/839911133aaf453c42f0ffc56b0f6cfb52c33858/quasar/images/quasar.svg', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/839911133aaf453c42f0ffc56b0f6cfb52c33858/quasar/images/quasar.svg', + }, + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/quasar/txs/', + govV1: false, + config: { + chainId: 'quasar-1', + chainName: 'Quasar', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://quasar-rpc.publicnode.com:443', + 'https://quasar-rpc.polkachu.com', + 'https://quasar-mainnet-rpc.autostake.com:443', + ], + currencies: [ + { + coinDenom: 'QSR', + coinMinimalDenom: 'uqsr', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'quasar', + bech32PrefixAccPub: 'quasarpub', + bech32PrefixValAddr: 'quasarvaloper', + bech32PrefixValPub: 'quasarvaloperpub', + bech32PrefixConsAddr: 'quasargvalcons', + bech32PrefixConsPub: 'quasarvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'QSR', + coinMinimalDenom: 'uqsr', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.03, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'QSR', + coinMinimalDenom: 'uqsr', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/839911133aaf453c42f0ffc56b0f6cfb52c33858/quasar/images/quasar.svg', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #6DD4EF60 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: true, + group: true, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/comdex/images/cmdx.png', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/comdex/images/cmdx.png', + }, + keplrExperimental: true, + leapExperimental: true, + isTestnet: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/comdex/txs/', + govV1: true, + config: { + chainId: 'comdex-1', + chainName: 'Comdex', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://rpc.comdex.one', + 'https://comdex-rpc.polkachu.com', + 'https://comdex-rpc.publicnode.com:443', + ], + currencies: [ + { + coinDenom: 'CMDX', + coinMinimalDenom: 'ucmdx', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'comdex', + bech32PrefixAccPub: 'comdexpub', + bech32PrefixValAddr: 'comdexvaloper', + bech32PrefixValPub: 'comdexvaloperpub', + bech32PrefixConsAddr: 'comdexgvalcons', + bech32PrefixConsPub: 'comdexvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'CMDX', + coinMinimalDenom: 'ucmdx', + coinDecimals: 6, + gasPriceStep: { + low: 0, + average: 0.025, + high: 0.04, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'CMDX', + coinMinimalDenom: 'ucmdx', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/comdex/images/cmdx.png', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #F2415060 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: true, + group: true, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/gravitybridge/images/grav.png', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/gravitybridge/images/grav.png', + }, + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/gravity-bridge/txs/', + govV1: false, + config: { + chainId: 'gravity-bridge-3', + chainName: 'GravityBridge', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://gravitybridge-rpc.lavenderfive.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://gravitybridge-rpc.lavenderfive.com', + 'https://gravity-rpc.polkachu.com', + 'https://rpc-gravitybridge-ia.cosmosia.notional.ventures', + ], + currencies: [ + { + coinDenom: 'GRAV', + coinMinimalDenom: 'ugraviton', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'gravity', + bech32PrefixAccPub: 'gravitypub', + bech32PrefixValAddr: 'gravityvaloper', + bech32PrefixValPub: 'gravityvaloperpub', + bech32PrefixConsAddr: 'gravitygvalcons', + bech32PrefixConsPub: 'gravityvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'GRAV', + coinMinimalDenom: 'ugraviton', + coinDecimals: 6, + gasPriceStep: { + low: 0, + average: 0, + high: 0.035, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'GRAV', + coinMinimalDenom: 'ugraviton', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/gravitybridge/images/grav.png', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #0339A660 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: true, + group: true, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/mars/images/mars-icon.png', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/mars/images/mars-icon.png', + }, + keplrExperimental: false, + leapExperimental: false, + isTestnet: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/mars-protocol/txs/', + govV1: true, + config: { + chainId: 'mars-1', + chainName: 'MarsHub', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: [ + 'https://api.resolute.vitwit.com' + ], + rpcURIs: [ + 'https://rpc.marsprotocol.io:443', + 'https://mars-rpc.lavenderfive.com:443', + 'https://mars-rest.publicnode.com', + ], + currencies: [ + { + coinDenom: 'MARS', + coinMinimalDenom: 'umars', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'mars', + bech32PrefixAccPub: 'marspub', + bech32PrefixValAddr: 'marsvaloper', + bech32PrefixValPub: 'marsvaloperpub', + bech32PrefixConsAddr: 'marsgvalcons', + bech32PrefixConsPub: 'marsvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'MARS', + coinMinimalDenom: 'umars', + coinDecimals: 6, + gasPriceStep: { + low: 0, + average: 0, + high: 0.01, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'MARS', + coinMinimalDenom: 'umars', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/mars/images/mars-icon.png', + theme: { + primaryColor: '#fff', + gradient: + 'linear-gradient(180deg, #F2522E60 0%, #12131C80 100%)', + }, + }, + isCustomNetwork: false, + supportedWallets: [] + }, + { + enableModules: { + authz: true, + feegrant: true, + group: true, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/archway/images/archway.png', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/archway/images/archway.png', + }, + supportedWallets: ['KEPLR'], + keplrExperimental: true, + leapExperimental: false, + isTestnet: false, + isCustomNetwork: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/archway/txs/', + govV1: false, + config: { + chainId: 'archway-1', + chainName: 'Archway', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://rpc.mainnet.archway.io', + 'https://archway-rpc.lavenderfive.com:443', + 'https://rpc.archway.nodestake.top', + ], + currencies: [ + { + coinDenom: 'ARCH', + coinMinimalDenom: 'aarch', + coinDecimals: 18, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'archway', + bech32PrefixAccPub: 'archwaypub', + bech32PrefixValAddr: 'archwayvaloper', + bech32PrefixValPub: 'archwayvaloperpub', + bech32PrefixConsAddr: 'archwaygvalcons', + bech32PrefixConsPub: 'archwayvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'ARCH', + coinMinimalDenom: 'aarch', + coinDecimals: 18, + gasPriceStep: { + low: 1000000000000, + average: 1500000000000, + high: 2000000000000, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'ARCH', + coinMinimalDenom: 'aarch', + coinDecimals: 18, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/archway/images/archway.png', + theme: { + primaryColor: '#F24405', + gradient: 'linear-gradient(180deg, #F2440560 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: false, + feegrant: false, + group: false, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/stake.png', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/stake.png', + }, + supportedWallets: ['KEPLR'], + keplrExperimental: true, + leapExperimental: false, + isTestnet: false, + isCustomNetwork: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/noble/txs/', + govV1: false, + config: { + chainId: 'noble-1', + chainName: 'noble', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://rpc.mainnet.archway.io', + 'https://archway-rpc.lavenderfive.com:443', + 'https://rpc.archway.nodestake.top', + ], + currencies: [ + { + coinDenom: 'USDC', + coinMinimalDenom: 'uusdc', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'noble', + bech32PrefixAccPub: 'noblepub', + bech32PrefixValAddr: 'noblevaloper', + bech32PrefixValPub: 'noblevaloperpub', + bech32PrefixConsAddr: 'noblevalcons', + bech32PrefixConsPub: 'noblevalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'USDC', + coinMinimalDenom: 'uusdc', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.01, + high: 0.02, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'USDC', + coinMinimalDenom: 'uusdc', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/stake.png', + theme: { + primaryColor: '#a8bbfb', + gradient: 'linear-gradient(180deg, #F2440560 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: false, + feegrant: false, + group: false, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/neutron/images/neutron-raw.png', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/neutron/images/neutron-raw.png', + }, + supportedWallets: ['KEPLR'], + keplrExperimental: true, + leapExperimental: false, + isTestnet: false, + isCustomNetwork: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/neutron/txs/', + govV1: false, + config: { + chainId: 'neutron-1', + chainName: 'neutron', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://rpc.mainnet.archway.io', + 'https://archway-rpc.lavenderfive.com:443', + 'https://rpc.archway.nodestake.top', + ], + currencies: [ + { + coinDenom: 'NTRN', + coinMinimalDenom: 'untrn', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'neutron', + bech32PrefixAccPub: 'neutronpub', + bech32PrefixValAddr: 'neutronvaloper', + bech32PrefixValPub: 'neutronvaloperpub', + bech32PrefixConsAddr: 'neutronvalcons', + bech32PrefixConsPub: 'neutronvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'NTRN', + coinMinimalDenom: 'untrn', + coinDecimals: 6, + gasPriceStep: { + low: 0.0053, + average: 0.0053, + high: 0.0053, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'NTRN', + coinMinimalDenom: 'untrn', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/neutron/images/neutron-raw.png', + theme: { + primaryColor: '#000000', + gradient: 'linear-gradient(180deg, #000000 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: false, + feegrant: false, + group: false, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/sentinel/images/dvpn.png', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/sentinel/images/dvpn.png', + }, + supportedWallets: ['KEPLR'], + keplrExperimental: true, + leapExperimental: false, + isTestnet: false, + isCustomNetwork: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/sentinel/txs/', + govV1: false, + config: { + chainId: 'sentinelhub-2', + chainName: 'sentinel', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://rpc.mainnet.archway.io', + 'https://archway-rpc.lavenderfive.com:443', + 'https://rpc.archway.nodestake.top', + ], + currencies: [ + { + coinDenom: 'DVPN', + coinMinimalDenom: 'udvpn', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'sent', + bech32PrefixAccPub: 'sentpub', + bech32PrefixValAddr: 'sentvaloper', + bech32PrefixValPub: 'sentvaloperpub', + bech32PrefixConsAddr: 'sentvalcons', + bech32PrefixConsPub: 'sentvalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'DVPN', + coinMinimalDenom: 'udvpn', + coinDecimals: 6, + gasPriceStep: { + low: 0.1, + average: 0.25, + high: 0.4, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'DVPN', + coinMinimalDenom: 'udvpn', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/sentinel/images/dvpn.png', + theme: { + primaryColor: '#10a7ef', + gradient: 'linear-gradient(180deg, #10a7ef 0%, #12131C80 100%)', + }, + }, + }, + { + enableModules: { + authz: false, + feegrant: false, + group: false, + }, + aminoConfig: { + authz: true, + feegrant: true, + group: false, + }, + showAirdrop: false, + logos: { + menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/saga/images/saga.png', + toolbar: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/saga/images/saga.png', + }, + supportedWallets: ['KEPLR'], + keplrExperimental: true, + leapExperimental: false, + isTestnet: false, + isCustomNetwork: false, + explorerTxHashEndpoint: 'https://www.mintscan.io/saga/txs/', + govV1: false, + config: { + chainId: 'ssc-1', + chainName: 'saga', + rest: 'https://api.resolute.vitwit.com', + rpc: 'https://api.resolute.vitwit.com', + restURIs: ['https://api.resolute.vitwit.com'], + rpcURIs: [ + 'https://rpc.mainnet.archway.io', + 'https://archway-rpc.lavenderfive.com:443', + 'https://rpc.archway.nodestake.top', + ], + currencies: [ + { + coinDenom: 'SAGA', + coinMinimalDenom: 'usaga', + coinDecimals: 6, + }, + ], + bech32Config: { + bech32PrefixAccAddr: 'saga', + bech32PrefixAccPub: 'sagapub', + bech32PrefixValAddr: 'sagavaloper', + bech32PrefixValPub: 'sagavaloperpub', + bech32PrefixConsAddr: 'sagavalcons', + bech32PrefixConsPub: 'sagavalconspub', + }, + feeCurrencies: [ + { + coinDenom: 'SAGA', + coinMinimalDenom: 'usaga', + coinDecimals: 6, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.04, + }, + }, + ], + bip44: { + coinType: 118, + }, + stakeCurrency: { + coinDenom: 'STAKE', + coinMinimalDenom: 'usaga', + coinDecimals: 6, + }, + image: + 'https://raw.githubusercontent.com/cosmos/chain-registry/master/saga/images/saga.png', + theme: { + primaryColor: '#040404', + gradient: 'linear-gradient(180deg, #040404 0%, #12131C80 100%)', }, }, }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/quicksilver/qck.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/master/quicksilver/images/quicksilver-chain-logo.png', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/quicksilver/txs/', - // config: { - // chainId: 'quicksilver-2', - // chainName: 'Quicksilver', - // rest: 'https://api.resolute.vitwit.com/quicksilver_api', - // rpc: 'https://api.resolute.vitwit.com/quicksilver_rpc', - // currencies: [ - // { - // coinDenom: 'QCK', - // coinMinimalDenom: 'uqck', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'quick', - // bech32PrefixAccPub: 'quickpub', - // bech32PrefixValAddr: 'quickvaloper', - // bech32PrefixValPub: 'quickvaloperpub', - // bech32PrefixConsAddr: 'quickgvalcons', - // bech32PrefixConsPub: 'quickvalconspub', - // }, - // bip44: { - // coinType: 118, - // }, - // feeCurrencies: [ - // { - // coinDenom: 'QCK', - // coinMinimalDenom: 'uqck', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.0001, - // average: 0.0001, - // high: 0.00025, - // }, - // }, - // ], - // stakeCurrency: { - // coinDenom: 'QCK', - // coinMinimalDenom: 'uqck', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: true, - // }, - // aminoConfig: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/regen/regen.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/regen/images/regen-logo.png', - // }, - // keplrExperimental: false, - // leapExperimental: true, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/regen/txs/', - // config: { - // chainId: 'regen-1', - // chainName: 'Regen', - // rest: 'https://api.resolute.vitwit.com/regen_api', - // rpc: 'https://api.resolute.vitwit.com/regen_rpc', - // currencies: [ - // { - // coinDenom: 'REGEN', - // coinMinimalDenom: 'uregen', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'regen', - // bech32PrefixAccPub: 'regenpub', - // bech32PrefixValAddr: 'regenvaloper', - // bech32PrefixValPub: 'regenvaloperpub', - // bech32PrefixConsAddr: 'regengvalcons', - // bech32PrefixConsPub: 'regenvalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'REGEN', - // coinMinimalDenom: 'uregen', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.01, - // average: 0.025, - // high: 0.03, - // }, - // }, - // ], - // bip44: { - // coinType: 118, - // }, - // stakeCurrency: { - // coinDenom: 'REGEN', - // coinMinimalDenom: 'uregen', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/stargaze/stars.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/stargaze/images/stargaze-logo.png', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/stargaze/txs/', - // config: { - // chainId: 'stargaze-1', - // chainName: 'Stargaze', - // rest: 'https://api.resolute.vitwit.com/stargaze_api', - // rpc: 'https://api.resolute.vitwit.com/stargaze_rpc', - // currencies: [ - // { - // coinDenom: 'STARS', - // coinMinimalDenom: 'ustars', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'stars', - // bech32PrefixAccPub: 'starspub', - // bech32PrefixValAddr: 'starsvaloper', - // bech32PrefixValPub: 'starsvaloperpub', - // bech32PrefixConsAddr: 'starsgvalcons', - // bech32PrefixConsPub: 'starsvalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'STARS', - // coinMinimalDenom: 'ustars', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.01, - // average: 0.025, - // high: 0.03, - // }, - // }, - // ], - // bip44: { - // coinType: 118, - // }, - // stakeCurrency: { - // coinDenom: 'STARS', - // coinMinimalDenom: 'ustars', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/tgrade/tgrade.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/master/tgrade/images/tgrade-logo-gradient_h.png', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/tgrade/txs/', - // config: { - // chainId: 'tgrade-mainnet-1', - // chainName: 'Tgrade', - // rest: 'https://api.resolute.vitwit.com/tgrade_api', - // rpc: 'https://api.resolute.vitwit.com/tgrade_rpc', - // currencies: [ - // { - // coinDenom: 'TGD', - // coinMinimalDenom: 'utgd', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'tgrade', - // bech32PrefixAccPub: 'tgradepub', - // bech32PrefixValAddr: 'tgradevaloper', - // bech32PrefixValPub: 'tgradevaloperpub', - // bech32PrefixConsAddr: 'tgradegvalcons', - // bech32PrefixConsPub: 'tgradevalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'TGD', - // coinMinimalDenom: 'utgd', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.05, - // average: 0.075, - // high: 0.1, - // }, - // }, - // ], - // bip44: { - // coinType: 118, - // }, - // stakeCurrency: { - // coinDenom: 'TGD', - // coinMinimalDenom: 'utgd', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: true, - // }, - // aminoConfig: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/vitwit/aneka-resources/d234799b2da3dc0b148829259866d07618b9773b/assets/umee/umee.png', - // toolbar: - // 'https://raw.githubusercontent.com/vitwit/chain-registry/08711dbf4cbc12d37618cecd290ad756c07d538b/umee/images/umee-logo.png', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://www.mintscan.io/umee/txs/', - // config: { - // chainId: 'umee-1', - // chainName: 'Umee', - // rest: 'https://api.resolute.vitwit.com/umee_api', - // rpc: 'https://api.resolute.vitwit.com/umee_rpc', - // currencies: [ - // { - // coinDenom: 'UMEE', - // coinMinimalDenom: 'uumee', - // coinDecimals: 6, - // }, - // ], - // bech32Config: { - // bech32PrefixAccAddr: 'umee', - // bech32PrefixAccPub: 'umeepub', - // bech32PrefixValAddr: 'umeevaloper', - // bech32PrefixValPub: 'umeevaloperpub', - // bech32PrefixConsAddr: 'umeegvalcons', - // bech32PrefixConsPub: 'umeevalconspub', - // }, - // feeCurrencies: [ - // { - // coinDenom: 'UMEE', - // coinMinimalDenom: 'uumee', - // coinDecimals: 6, - // gasPriceStep: { - // low: 0.01, - // average: 0.025, - // high: 0.03, - // }, - // }, - // ], - // bip44: { - // coinType: 118, - // }, - // stakeCurrency: { - // coinDenom: 'UMEE', - // coinMinimalDenom: 'uumee', - // coinDecimals: 6, - // }, - // image: - // 'https://raw.githubusercontent.com/leapwallet/assets/2289486990e1eaf9395270fffd1c41ba344ef602/images/logo.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, - // { - // enableModules: { - // authz: true, - // feegrant: true, - // group: false, - // }, - // aminoConfig: { - // authz: false, - // feegrant: false, - // group: false, - // }, - // showAirdrop: false, - // logos: { - // menu: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/celestia/images/celestia.svg', - // toolbar: - // 'https://raw.githubusercontent.com/cosmos/chain-registry/master/celestia/images/celestia.svg', - // }, - // keplrExperimental: false, - // leapExperimental: false, - // isTestnet: false, - // explorerTxHashEndpoint: 'https://mintscan.io/celestia/txs/', - // config: { - // chainId: 'celestia', - // chainName: 'Celestia', - // rest: 'https://public-celestia-lcd.numia.xyz', - // rpc: 'https://public-celestia-rpc.numia.xyz', - // currencies: [ - // { - // coinDenom: 'TIA', - // coinMinimalDenom: 'utia', - // coinDecimals: 6, - // }, - // ], - // bip44: { - // coinType: 118, - // }, - // bech32Config: { - // bech32PrefixAccAddr: 'celestia', - // bech32PrefixAccPub: 'celestiapub', - // bech32PrefixValAddr: 'celestiavaloper', - // bech32PrefixValPub: 'celestiavaloperpub', - // bech32PrefixConsAddr: 'celestiavalcons', - // bech32PrefixConsPub: 'celestiavalconspub', - // }, - // walletUrlForStaking: 'https://resolute.vitwit.com/celestia/staking', - // feeCurrencies: [ - // { - // coinDenom: 'TIA', - // coinMinimalDenom: 'utia', - // coinDecimals: 6, - // coinGeckoId: 'celestia', - // gasPriceStep: { - // low: 0.01, - // average: 0.015, - // high: 0.05, - // }, - // }, - // ], - // stakeCurrency: { - // coinDenom: 'TIA', - // coinMinimalDenom: 'utia', - // coinDecimals: 6, - // coinGeckoId: 'celestia', - // }, - // image: - // 'https://raw.githubusercontent.com/cosmos/chain-registry/master/celestia/images/celestia.svg', - // theme: { - // primaryColor: '#fff', - // gradient: - // 'linear-gradient(180deg, rgba(255,255,255,0.32) 0%, rgba(255,255,255,0) 100%)', - // }, - // }, - // }, ]; diff --git a/frontend/src/utils/commonStyles.ts b/frontend/src/utils/commonStyles.ts index 4bfd95ef5..46b02787e 100644 --- a/frontend/src/utils/commonStyles.ts +++ b/frontend/src/utils/commonStyles.ts @@ -1,5 +1,121 @@ export const dialogBoxPaperPropStyles = { - borderRadius: '24px', - background: - 'linear-gradient(178deg, #241B61 1.71%, #69448D 98.35%, #69448D 98.35%)', + borderRadius: '16px', + background: '#1C1C1D', +}; + +export const customMUITextFieldStyles = { + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '1px solid transparent', + borderRadius: '16px', + color: 'white', + }, + '& .Mui-focused': { + border: '1px solid #ffffff4a', + borderRadius: '16px', + }, +}; + +export const customTextFieldStyles = { + '& .MuiInputBase-input': { + color: '#fffffff0', + fontSize: '14px', + fontWeight: 200, + fontFamily: 'Libre Franklin', + lineHeight: '21px', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-root': { + border: '0.25px solid #ffffff10', + borderRadius: '100px', + height: '40px', + }, + '& .Mui-focused': { + border: '0.25px solid #ffffff4a', + borderRadius: '100px', + }, +}; + +export const multiSelectDropDownStyle = { + '& .MuiOutlinedInput-input': { + color: 'white', + }, + '& .MuiOutlinedInput-root': { + padding: '0px !important', + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: 'white', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none !important', + }, + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + borderRadius: '16px', + '& .MuiFormControl-root label': { + display: 'none !important', + color: 'red', + }, +}; + +export const customSelectStyles = { + '& .MuiOutlinedInput-input': { + color: '#fffffff0', + fontSize: '14px', + }, + '& .MuiOutlinedInput-root': { + padding: '0px !important', + border: 'none', + }, + '& .MuiSvgIcon-root': { + color: '#ffffff80', + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none !important', + }, + '& .MuiTypography-body1': { + color: 'white', + fontSize: '12px', + fontWeight: 200, + }, + borderRadius: '100px', + height: '40px', +}; + +export const paginationComponentStyles = { + '& .MuiPaginationItem-page': { + '&:hover': { + backgroundColor: '#FFFFFF05', + }, + fontSize: '12px', + minWidth: '24px', + height: '24px', + borderRadius: '4px', + color: '#ffffff80', + fontWeight: '200', + }, + '& .Mui-selected': { + backgroundColor: '#FFFFFF05', + fontWeight: '600', + color: '#ffffff', + }, + '& .MuiPaginationItem-icon': { + color: '#fff', + }, + '& .MuiPaginationItem-ellipsis, & .MuiPaginationItem-ellipsisIcon': { + color: 'white', + }, }; diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 9ded5f0b9..a40821839 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -1,6 +1,33 @@ +import { VoteOptionNumber } from '@/types/gov'; + +export const SUPPORTED_WALLETS = [ + { + name: 'Keplr', + logo: '/keplr-wallet-logo.png', + }, + { + name: 'Leap', + logo: '/leap-wallet-logo.png', + }, + { + name: 'Cosmostation', + logo: '/cosmostation-wallet-logo.png', + }, + { + name: 'Metamask', + logo: '/metamask.svg', + } +]; + +export const NotSupportedMetamaskChainIds = [ + 'agoric-3', + 'evmos_9001-2', + 'desmos-mainnet', +]; +export const USD_CURRENCY = 'usd'; export const GAS_FEE = 860000; export const ADD_NETWORK_TEMPLATE_URL = - 'https://raw.githubusercontent.com/vitwit/resolute/b5d184c8da894b2fea0ed40e56a599a1d813c422/frontend/public/add-network-template.json'; + 'https://raw.githubusercontent.com/vitwit/resolute/7436d1f8b545a67e6137cea75be8380c42264d46/frontend/public/add-network-template.json'; export const PROPOSAL_STATUS_VOTING_PERIOD = 'PROPOSAL_STATUS_VOTING_PERIOD'; export const COSMOS_CHAIN_ID = 'cosmoshub-4'; export const OFFCHAIN_VERIFICATION_MESSAGE = @@ -33,11 +60,19 @@ export const SEND_TYPE_URL = '/cosmos.bank.v1beta1.MsgSend'; export const DELEGATE_TYPE_URL = '/cosmos.staking.v1beta1.MsgDelegate'; export const UNDELEGATE_TYPE_URL = '/cosmos.staking.v1beta1.MsgUndelegate'; export const REDELEGATE_TYPE_URL = '/cosmos.staking.v1beta1.MsgBeginRedelegate'; +export const WITHDRAW_DELEGATE_REWARD = '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward' export const IBC_SEND_TYPE_URL = '/ibc.applications.transfer.v1.MsgTransfer'; export const DEPOSIT_TYPE_URL = '/cosmos.gov.v1beta1.MsgDeposit'; export const SEND_TEMPLATE = 'https://api.resolute.vitwit.com/_static/send.csv'; export const VOTE_TYPE_URL = '/cosmos.gov.v1beta1.MsgVote'; -export const MULTI_TRANSFER_MSG_COUNT = 13; +export const MSG_AUTHZ_REVOKE = '/cosmos.authz.v1beta1.MsgRevoke'; +export const MSG_AUTHZ_EXEC = '/cosmos.authz.v1beta1.MsgExec'; +export const MSG_AUTHZ_GRANT = '/cosmos.authz.v1beta1.MsgGrant'; +export const MSG_GRANT_ALLOWANCE = '/cosmos.feegrant.v1beta1.MsgGrantAllowance'; +export const MSG_REVOKE_ALLOWANCE = + '/cosmos.feegrant.v1beta1.MsgRevokeAllowance'; + +export const MULTI_TRANSFER_MSG_COUNT = 3; export const DELETE_TXN_DIALOG_IMAGE_PATH = '/delete-txn-popup-image.png'; export const EMPTY_TXN = { id: NaN, @@ -76,6 +111,7 @@ export const MAP_TXNS = { '/cosmos.bank.v1beta1.MsgSend': 'Send', '/cosmos.staking.v1beta1.MsgBeginRedelegate': 'ReDelegate', '/cosmos.staking.v1beta1.MsgUndelegate': 'UnDelegate', + '/cosmos.gov.v1beta1.MsgVote': 'Vote', Msg: 'Tx Msg', }; export const MULTISIG_TX_TYPES = { @@ -96,51 +132,105 @@ export const MULTISIG_REDELEGATE_TEMPLATE = export const txBroadcastTimeoutMs = 60_000; export const txBroadcastPollIntervalMs = 3_000; export const TRACK_IBC_TX_TIME_INTERVAL = 15000; -export const SIDENAV_MENU_ITEMS = [ - { - name: 'Overview', - icon: '/overview-icon.svg', - activeIcon: '/overview-icon-active.svg', - link: '/', - }, - { - name: 'Transfers', - icon: '/transfers-icon.svg', - activeIcon: '/transfers-icon-active.svg', - link: '/transfers', - }, - { - name: 'Governance', - icon: '/gov-icon.svg', - activeIcon: '/gov-icon-active.svg', - link: '/governance', - }, - { - name: 'Staking', - icon: '/staking-icon.svg', - activeIcon: '/staking-icon-active.svg', - link: '/staking', - }, - { - name: 'Multisig', - icon: '/multisig-icon.svg', - activeIcon: '/multisig-icon-active.svg', - link: '/multisig', - }, -]; -export const ALL_NETWORKS_ICON = '/all-networks-icon.png'; -export const CHANGE_NETWORK_ICON = '/switch-icon.svg'; + +export const SIDENAV_MENU_ITEMS = { + defaultOptions: [ + { + name: 'Overview', + icon: '/overview-icon.svg', + activeIcon: '/overview-icon-active.svg', + link: '/', + authzSupported: true, + isMetaMaskSupports: true, + }, + { + name: 'Transfers', + icon: '/transfers-icon.svg', + activeIcon: '/transfers-icon-active.svg', + link: '/transfers', + authzSupported: true, + isMetaMaskSupports: true, + }, + { + name: 'Governance', + icon: '/gov-icon.svg', + activeIcon: '/gov-icon-active.svg', + link: '/governance', + authzSupported: true, + isMetaMaskSupports: true, + }, + { + name: 'Staking', + icon: '/staking-icon.svg', + activeIcon: '/staking-icon-active.svg', + link: '/staking', + authzSupported: true, + isMetaMaskSupports: true, + }, + { + name: 'Authz', + icon: '/authz-icon.svg', + activeIcon: '/authz-icon-active.svg', + link: '/authz', + authzSupported: false, + isMetaMaskSupports: false, + }, + { + name: 'Multisig', + icon: '/multisig-icon.svg', + activeIcon: '/multisig-icon-active.svg', + link: '/multisig', + authzSupported: false, + isMetaMaskSupports: false, + }, + { + name: 'Feegrant', + icon: '/feegrant-icon.svg', + activeIcon: '/feegrant-icon-active.svg', + link: '/feegrant', + authzSupported: false, + isMetaMaskSupports: false, + }, + ], + moreOptions: [ + { + name: 'Multiops', + icon: '/multiops-icon.svg', + activeIcon: '/multiops-icon-active.svg', + link: '/multiops', + authzSupported: false, + isMetaMaskSupports: false, + }, + { + name: 'CosmWasm', + icon: '/cosmwasm-icon.svg', + activeIcon: '/cosmwasm-icon-active.svg', + link: '/cosmwasm', + authzSupported: false, + isMetaMaskSupports: false, + }, + { + name: 'History', + icon: '/history-icon.svg', + activeIcon: '/history-icon.svg', + link: '/history', + authzSupported: true, + isMetaMaskSupports: true, + }, + ], +}; +export const ALL_NETWORKS_ICON = '/icons/all-networks-icon.png'; export const TXN_SUCCESS_ICON = '/transaction-success-icon.svg'; export const TXN_FAILED_ICON = '/transaction-failed-icon.svg'; -export const HELP_ICON = '/help-icon.svg'; -export const REPORT_ICON = '/report-icon.svg'; export const GITHUB_ISSUES_PAGE_LINK = - 'https://github.com/vitwit/resolute/issues'; + 'https://github.com/vitwit/resolute/issues/new'; export const TELEGRAM_LINK = 'https://web.telegram.org/k/#-1982236507'; export const LOGOUT_ICON = '/logout-icon.svg'; export const TRANSFERS_CARDS_COUNT = 5; export const NO_MESSAGES_ILLUSTRATION = '/no-messages-illustration.png'; export const NO_DELEGATIONS_MSG = `Looks like you haven't staked anything yet, go ahead and explore !`; +export const OVERVIEW_NO_DELEGATIONS = + "Looks like you haven't staked anything yet, Select a network to delegate your tokens!"; export const VOTE_OPTIONS = ['Yes', 'Abstain', 'No', 'No With Veto']; export const MAP_TXN_TYPES: Record = { '/cosmos.staking.v1beta1.MsgDelegate': ['delegated', 'delegating'], @@ -155,3 +245,166 @@ export const TWITTER_ICON = '/twitter-icon.png'; export const TWITTER_LINK = 'https://twitter.com/vitwit_'; export const MIN_SALT_VALUE = 99999; export const MAX_SALT_VALUE = 99999999; +export const NO_GRANTS_BY_ME_TEXT = "You haven't granted any permission yet"; +export const NO_GRANTS_TO_ME_TEXT = "You don't have any grants"; +export const SECP256K1_PUBKEY_TYPE = '/cosmos.crypto.secp256k1.PubKey'; +export const MULTISIG_LEGACY_AMINO_PUBKEY_TYPE = + '/cosmos.crypto.multisig.LegacyAminoPubKey'; +export const GENERIC_AUTHORIZATION_TYPE = + '/cosmos.authz.v1beta1.GenericAuthorization'; +export const STAKE_AUTHORIZATION_TYPE = + '/cosmos.staking.v1beta1.StakeAuthorization'; +export const SEND_AUTHORIZATION_TYPE = '/cosmos.bank.v1beta1.SendAuthorization'; +export const MULTISIG_PUBKEY_OBJECT = { + name: 'pubKey', + value: '', + label: 'Public Key (Secp256k1)', + placeHolder: 'E. g. AtgCrYjD+21d1+og3inzVEOGbCf5uhXnVeltFIo7RcRp', + required: true, + disabled: false, + isPubKey: false, + address: '', + pubKey: '', + error: '', +}; +export const AXIOS_RETRIES_COUNT = 2; +export const MAX_TRY_END_POINTS = 1; +export const NO_FEEGRANTS_BY_ME_TEXT = "You haven't granted any allowance yet"; +export const NO_FEEGRANTS_TO_ME_TEXT = "You don't have any feegrants"; +export const SQUID_ID = process.env.NEXT_PUBLIC_SQUID_ID || ''; +export const SQUID_CLIENT_API = 'https://api.0xsquid.com'; +export const SQUID_CHAINS_API = 'https://v2.api.squidrouter.com/v2/chains'; +export const ALERT_TYPE_MAP: Record = { + success: 'success', + error: 'error', + info: 'info', +}; +export const WITVAL = 'witval'; +export const VITWIT = 'vitwit'; +export const VITWIT_NEW_MONIKER = 'vitwit%20(previously%20witval)'; +export const VITWIT_VALIDATOR_NAMES = [ + 'vitwit', + 'witval', + 'validator/vitwit%20(previously%20witval)', + 'vitwit (previously witval)', +]; +export const POLYGON_API = 'https://staking-api.polygon.technology/api/v2'; + +export const POLYGON_CONFIG = { + baseURL: 'https://staking-api.polygon.technology/api/v2', + decimals: 18, + coinGeckoId: 'matic', + logo: '/polygon-logo.svg', + witval: { + profile: 'https://staking.polygon.technology/validators/50', + }, +}; + +export const OASIS_CONFIG = { + baseURL: 'https://oasisscan.com', + decimals: 9, + coinGeckoId: 'rose', + logo: '/oasis-network-logo.png', + witval: { + profile: + 'https://www.oasisscan.com/validators/detail/oasis1qzc687uuywnel4eqtdn6x3t9hkdvf6sf2gtv4ye9', + commission: 19, + operatorAddress: 'oasis1qzc687uuywnel4eqtdn6x3t9hkdvf6sf2gtv4ye9', + }, +}; + +export const COIN_GECKO_IDS: Record = { + ubld: 'BLD', + umars: 'Mars Protocol', + ucmdx: 'cmdx', +}; + +export const MULTISEND_PLACEHOLDER = `Enter here\n\nExample:\ncosmos1hzq8fmhmd52fdhjprj2uj8ht3q0wxxc29th0l6, 35uatom\ncosmos1h0t3funxenm54ke2z9tfdtgrctex575ufpz3kw, 2506uatom`; + +export const voteOptionNumber: VoteOptionNumber = { + yes: 1, + no: 3, + abstain: 2, + veto: 4, +}; + +export const voteOptions: Record = { + VOTE_OPTION_YES: 'yes', + VOTE_OPTION_ABSTAIN: 'abstain', + VOTE_OPTION_NO: 'no', + VOTE_OPTION_NO_WITH_VETO: 'veto', + VOTE_OPTION_UNSPECIFIED: '', +}; + +export const MULTIOPS_MSG_TYPES: Record = { + send: 'Send', + delegate: 'Delegate', + undelegate: 'Undelegate', + redelegate: 'Redelegate', + vote: 'Vote', + deposit: 'Deposit', +}; +export const MULTIOPS_NOTE = `Note: Please ensure to allocate additional gas if the +transaction involves multiple messages, and be sure to +select the appropriate fee option in the signing +wallet.`; +export const MULTIOPS_SAMPLE_FILES: Record = { + delegate: + 'https://raw.githubusercontent.com/vitwit/resolute/a6a02cc1b74ee34604e6df35cfce7a46c39980ea/frontend/src/example-files/delegate.csv', + deposit: + 'https://raw.githubusercontent.com/vitwit/resolute/a6a02cc1b74ee34604e6df35cfce7a46c39980ea/frontend/src/example-files/deposit.csv', + redelegate: + 'https://raw.githubusercontent.com/vitwit/resolute/a6a02cc1b74ee34604e6df35cfce7a46c39980ea/frontend/src/example-files/redelegate.csv', + send: 'https://raw.githubusercontent.com/vitwit/resolute/a6a02cc1b74ee34604e6df35cfce7a46c39980ea/frontend/src/example-files/send.csv', + undelegate: + 'https://raw.githubusercontent.com/vitwit/resolute/a6a02cc1b74ee34604e6df35cfce7a46c39980ea/frontend/src/example-files/undelegate.csv', + vote: 'https://raw.githubusercontent.com/vitwit/resolute/a6a02cc1b74ee34604e6df35cfce7a46c39980ea/frontend/src/example-files/vote.csv', +}; +export const SWAP_ROUTE_ERROR = 'Failed to fetch routes.'; +export const DUMMY_WALLET_MNEMONIC = + process.env.NEXT_PUBLIC_DUMMY_WALLET_MNEMONIC || ''; +export const INCREASE = 'increase'; +export const DECREASE = 'decrease'; +export const IBC_SWAP_DESCRIPTION = + 'Swap tokens across various interchain networks'; +export const MULTISIG_DESCRIPTION = + 'A multisig account is an account that requires multiple signatures to sign transactions'; +export const COSMWASM_DESCRIPTION = + 'CosmWasm is a smart contracting platform built for the Cosmos ecosystem.'; +export const DEPLOY_CONTRACT_DESCRIPTION = 'Deploy your Cosmwasm Contract'; +export const IBC_SWAP_GRADIENT = `linear-gradient( + 180deg, + rgba(68, 83, 223, 0.1) 12.5%, + rgba(127, 92, 237, 0.1) 87.5% + )`; + +export const IBC_SEND_ALERT = '*Avoid IBC transfer to centralized exchanges'; +export const VITWIT_VALIDATOR_DESCRIPTION = + 'Vitwit excels in providing top-notch infrastructure services for the Cosmos blockchain ecosystem. We specialize in setting up and managing validators, relayers, and offering expert advisory services.'; +export const TRANSFERS_TYPES: Record< + string, + { title: string; description: string } +> = { + 'multi-send': { + title: 'Multi Transfer', + description: 'Transfer tokens to multiple addressess within a network', + }, + single: { + title: 'Single Transfer', + description: 'Transfer tokens within or across different networks', + }, + 'ibc-swap': { + title: 'IBC Swap', + description: + 'Exchange of assets between different networks using the Inter-Blockchain Communication protocol', + }, +}; +export const ALL_NETWORKS_GRADIENT = + 'linear-gradient(180deg, #6155b275 0.5%, #12131C80 100%)'; +export const AUTHZ = 'authz'; +export const FEEGRANT = 'feegrant'; +export const TXN_BUILDER_DESCRIPTION = + 'Transaction builder allows to create single transaction with multiple messages of same or different type.'; +export const GENERAL_SETTINGS_DESCRIPTION = 'Settings to enhance your application’s functionality'; +export const SUCCESS = 'SUCCESS'; +export const FAILED = 'FAILED'; \ No newline at end of file diff --git a/frontend/src/utils/contants.ts b/frontend/src/utils/contants.ts deleted file mode 100644 index 5aafe4eea..000000000 --- a/frontend/src/utils/contants.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const supportedWallets = [ - { - name: 'Keplr', - logo: '/keplr-wallet-logo.png', - }, - { - name: 'Leap', - logo: '/leap-wallet-logo.png', - }, - { - name: 'Cosmostation', - logo: '/cosmostation-wallet-logo.png', - }, -]; - -export const USD_CURRENCY = 'usd'; diff --git a/frontend/src/utils/dataTime.ts b/frontend/src/utils/dataTime.ts index bf68d484e..6c6f703b1 100644 --- a/frontend/src/utils/dataTime.ts +++ b/frontend/src/utils/dataTime.ts @@ -35,30 +35,40 @@ export function getTimeDifferenceToFutureDate( ): string { const now = new Date(); const futureDateObj = new Date(futureDate); + if (isNaN(futureDateObj.getTime())) { return 'Invalid date'; } + let timeDifference; + if (past) { - timeDifference = now.getTime() - futureDateObj.getTime(); + timeDifference = Math.abs(now.getTime() - futureDateObj.getTime()); } else { timeDifference = futureDateObj.getTime() - now.getTime(); } + const seconds = Math.floor(timeDifference / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); - const months = Math.floor(days / 30); - const years = Math.floor(days / 365); + const months = Math.floor(days / 30.44); // Average days in a month + const years = Math.floor(days / 365.25); // Average days in a year + const getTimeString = (value: number, unit: string) => - `${value} ${value === 1 ? unit : unit + `s`}`; + `${value} ${unit}${value === 1 ? '' : 's'}`; + if (seconds < 60) { - return getTimeString(seconds, 'second'); + if (seconds >= 0) + return getTimeString(seconds, 'second'); + else { + return '-' + } } else if (minutes < 60) { return getTimeString(minutes, 'minute'); } else if (hours < 24) { return getTimeString(hours, 'hour'); - } else if (days < 30) { + } else if (days < 30.44) { return getTimeString(days, 'day'); } else if (months < 12) { return getTimeString(months, 'month'); diff --git a/frontend/src/utils/datetime.ts b/frontend/src/utils/datetime.ts index b4994cea3..14848d5f5 100644 --- a/frontend/src/utils/datetime.ts +++ b/frontend/src/utils/datetime.ts @@ -11,4 +11,11 @@ export function getDaysLeft(end_date: string): number { export function getLocalDate(value: string): string { return moment(value).format('YYYY-MM-DD'); -} \ No newline at end of file +} + +export const isTimeExpired = (expirationTime: string | null): boolean => { + if (expirationTime == null) return false; + const expirationDate = new Date(expirationTime); + const currentTime = new Date(); + return currentTime.getTime() > expirationDate.getTime(); +}; diff --git a/frontend/src/utils/denom.ts b/frontend/src/utils/denom.ts index 1f5af617d..39ea4e031 100644 --- a/frontend/src/utils/denom.ts +++ b/frontend/src/utils/denom.ts @@ -1,4 +1,4 @@ -import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; +import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; export function formatVotingPower(token: number, coinDecimals: number): string { const temp = token / 10.0 ** coinDecimals; @@ -19,12 +19,16 @@ export function parseTokens( displayName: string, coinDecimals: number ): string { + if (!tokens) { + return '0.0'; + } + if (tokens.length === 0) { - return "0.0"; + return '0.0'; } return `${parseFloat( - (Number(tokens[0].amount) / 10.0 ** coinDecimals).toFixed(coinDecimals) + (Number(tokens[0]?.amount) / 10.0 ** coinDecimals).toFixed(coinDecimals) )} ${displayName}`; } @@ -33,8 +37,12 @@ export function parseBalance( coinDecimals: number, minimalDenom: string ): number { + if (!tokens) { + return 0.0; + } + const precision = coinDecimals > 6 ? 6 : coinDecimals; - if (tokens.length === 0) { + if (tokens?.length === 0) { return 0.0; } @@ -50,19 +58,54 @@ export function parseBalance( } export function getDenomBalance(tokens: Coin[], denom: string): number { - if (tokens.length === 0) { + if (tokens && tokens.length === 0) { return 0.0; } - for (let i = 0; i < tokens.length; i++) { - if (tokens[i].denom === denom) return parseFloat(tokens[i].amount); + + if (tokens) { + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].denom === denom) return parseFloat(tokens[i].amount); + } } + return 0.0; } export const formatNumber = (number: number): string => { - return number?.toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }) || "N/A"; + return ( + number?.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) || 'N/A' + ); +}; + +export const getTotalAmount = ( + originDenomInfo: OriginDenomInfo, + msgs: Msg[] +) => { + let totalAmount = 0; + msgs.forEach((msg) => { + const parsedAmount = parseBalance( + msg.value.amount, + originDenomInfo.decimals, + msg.value.amount[0].denom + ); + totalAmount += parsedAmount; + }); + return totalAmount.toFixed(6); }; +export const checkForIBCTokens = (balance: Coin[], nativeMinimalDenom: string) => { + if (!balance?.length) { + return false; + } + if (balance?.length === 1) { + if (balance[0].denom === nativeMinimalDenom) { + return false; + } else { + return true; + } + } + return true; +}; diff --git a/frontend/src/utils/errors.ts b/frontend/src/utils/errors.ts index 7e28ef52f..4e5fd6ba6 100644 --- a/frontend/src/utils/errors.ts +++ b/frontend/src/utils/errors.ts @@ -16,10 +16,21 @@ export const FAILED_TO_BROADCAST_ERROR = 'Failed to broadcast transaction'; export const ADDRESS_NOT_FOUND = 'Address not found on chain, please enter pubKey'; export const INVALID_PUBKEY = 'Invalid PubKey'; -export const MIN_THRESHOLD_ERROR = 'Threshold must be greater than 1'; +export const MIN_THRESHOLD_ERROR = 'Threshold must be a positive value'; export const MIN_PUBKEYS_ERROR = 'At least 1 pubkey is required'; export const DUPLICATE_PUBKEYS_ERROR = 'You have entered duplicate pubkeys'; -export const MAX_THRESHOLD_ERROR = 'Threshold can not be greater than pubkeys'; +export const MAX_THRESHOLD_ERROR = + 'Threshold can not be greater than members count'; export const MAX_PUBKEYS_ERROR = "You can't add more than 7 pub keys"; export const FAILED_TO_GENERATE_MULTISIG = 'Failed to create multisig account'; export const INSUFFICIENT_BALANCE = 'Insufficient balance'; +export const NOT_MULTISIG_MEMBER_ERROR = + 'Cannot import account: You are not a member of the multisig account'; +export const NOT_MULTISIG_ACCOUNT_ERROR = 'Not a multisig account'; +export const CHAIN_NOT_SELECTED_ERROR = 'Please select at least one network from the left'; +export const MSG_NOT_SELECTED_ERROR = 'Please select at least one transaction message from the left'; +export const PERMISSION_NOT_SELECTED_ERROR = + 'Atleast one permission must be selected'; +export const FAILED_TO_FETCH = 'Failed to fetch'; +export const NETWORK_ERROR = 'Network error'; +export const ERR_TXN_NOT_FOUND = 'TXN not found'; \ No newline at end of file diff --git a/frontend/src/utils/feegrant.ts b/frontend/src/utils/feegrant.ts new file mode 100644 index 000000000..98d0a6d49 --- /dev/null +++ b/frontend/src/utils/feegrant.ts @@ -0,0 +1,212 @@ +import { get } from 'lodash'; +import { convertToSnakeCase } from './util'; +import { getTypeURLName } from './util'; + +interface FeegrantMenuItem { + txn: string; + typeURL: string; +} + +export const BASIC_ALLOWANCE = '/cosmos.feegrant.v1beta1.BasicAllowance'; +export const PERIODIC_ALLOWANCE = '/cosmos.feegrant.v1beta1.PeriodicAllowance'; +export const ALLOWED_MSG_ALLOWANCE = + '/cosmos.feegrant.v1beta1.AllowedMsgAllowance'; + +export function feegrantMsgTypes(): FeegrantMenuItem[] { + return [ + { + txn: 'Send', + typeURL: '/cosmos.bank.v1beta1.MsgSend', + }, + { + txn: 'Grant Authz', + typeURL: '/cosmos.authz.v1beta1.MsgGrant', + }, + { + txn: 'Revoke Authz', + typeURL: '/cosmos.authz.v1beta1.MsgRevoke', + }, + { + txn: 'Grant Feegrant', + typeURL: '/cosmos.feegrant.v1beta1.MsgGrantAllowance', + }, + { + txn: 'Revoke Feegrant', + typeURL: '/cosmos.feegrant.v1beta1.MsgRevokeAllowance', + }, + { + txn: 'Submit Proposal', + typeURL: '/cosmos.gov.v1beta1.MsgSubmitProposal', + }, + { + txn: 'Vote', + typeURL: '/cosmos.gov.v1beta1.MsgVote', + }, + { + txn: 'Deposit', + typeURL: '/cosmos.gov.v1beta1.MsgDeposit', + }, + { + txn: 'Withdraw Rewards', + typeURL: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', + }, + { + txn: 'Redelegate', + typeURL: '/cosmos.staking.v1beta1.MsgBeginRedelegate', + }, + { + txn: 'Delegate', + typeURL: '/cosmos.staking.v1beta1.MsgDelegate', + }, + { + txn: 'Undelegate', + typeURL: '/cosmos.staking.v1beta1.MsgUndelegate', + }, + { + txn: 'Withdraw Commission', + typeURL: '/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission', + }, + { + txn: 'Unjail', + typeURL: '/cosmos.slashing.v1beta1.MsgUnjail', + }, + { + txn: 'Set Withdraw Address', + typeURL: '/cosmos.distribution.v1beta1.MsgSetWithdrawAddress', + }, + ]; +} + +export function getMsgNamesFromAllowance(allowance: Allowance): string[] { + switch (allowance.allowance['@type']) { + case BASIC_ALLOWANCE: + return ['All Transactions']; + case PERIODIC_ALLOWANCE: + return ['All Transactions']; + case ALLOWED_MSG_ALLOWANCE: + return parseMsgNames(allowance.allowance.allowed_messages); + default: + console.error(`Unknown allowance type: ${allowance?.allowance['@type']}`); + return ['Unknown']; + } +} + +function parseMsgNames(allowedMsgs: string[]): string[] { + const msgNames: string[] = []; + allowedMsgs.forEach((msg) => { + msgNames.push(getTypeURLName(msg)); + }); + return msgNames; +} + +export const isFeegrantAvailable = ( + allowance: Allowance, + txnMsg: string +): boolean => { + switch (allowance.allowance['@type']) { + case BASIC_ALLOWANCE: + return true; + case PERIODIC_ALLOWANCE: + return true; + case ALLOWED_MSG_ALLOWANCE: + return allowance.allowance.allowed_messages.includes(txnMsg); + default: + return false; + } +}; + +export const MAP_TXN_MSG_TYPES: Record = { + send: '/cosmos.bank.v1beta1.MsgSend', + grant_authz: '/cosmos.authz.v1beta1.MsgGrant', + revoke_authz: '/cosmos.authz.v1beta1.MsgRevoke', + grant_feegrant: '/cosmos.feegrant.v1beta1.MsgGrantAllowance', + revoke_feegrant: '/cosmos.feegrant.v1beta1.MsgRevokeAllowance', + submit_proposal: '/cosmos.gov.v1beta1.MsgSubmitProposal', + vote: '/cosmos.gov.v1beta1.MsgVote', + deposit: '/cosmos.gov.v1beta1.MsgDeposit', + withdraw_rewards: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', + redelegate: '/cosmos.staking.v1beta1.MsgBeginRedelegate', + delegate: '/cosmos.staking.v1beta1.MsgDelegate', + undelegate: '/cosmos.staking.v1beta1.MsgUndelegate', + withdraw_commission: + '/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission', + unjail: '/cosmos.slashing.v1beta1.MsgUnjail', + cancel_unbonding: '/cosmos.staking.v1beta1.MsgCancelUnbondingDelegation', + set_withdraw_address: '/cosmos.distribution.v1beta1.MsgSetWithdrawAddress', +}; + +export const getFeegrantFormDefaultValues = () => { + const date = new Date(); + const MILLISECONDS_IN_A_YEAR = 365 * 86400000; + const expiration = new Date( + date.setTime(date.getTime() + MILLISECONDS_IN_A_YEAR) + ); + + return { + grantee_address: '', + expiration: expiration, + spend_limit: '', + period: '', + period_spend_limit: '', + }; +}; + +export const getMsgListFromMsgNames = (msgNames: string[]) => { + const msgsList: string[] = []; + msgNames.forEach((msg) => { + msgsList.push(MAP_TXN_MSG_TYPES[convertToSnakeCase(msg)]); + }); + return msgsList; +}; + +export const getExpiryDate = ( + allowance: BasicAllowance | PeriodicAllowance | AllowedMsgAllowance +) => { + if (get(allowance, '@type') === BASIC_ALLOWANCE) { + return get(allowance, 'expiration', ''); + } else if (get(allowance, '@type') === PERIODIC_ALLOWANCE) { + return get(allowance, 'basic.expiration', ''); + } else { + if (get(allowance, 'allowance.@type') === BASIC_ALLOWANCE) { + return get(allowance, 'allowance.expiration', ''); + } else { + return get(allowance, 'allowance.basic.expiration', ''); + } + } +}; + +export const getSpendLimit = ( + allowance: BasicAllowance | PeriodicAllowance | AllowedMsgAllowance +) => { + if (get(allowance, '@type') === BASIC_ALLOWANCE) { + return get(allowance, 'spend_limit', []); + } else if (get(allowance, '@type') === PERIODIC_ALLOWANCE) { + return get(allowance, 'basic.spend_limit', []); + } else { + if (get(allowance, 'allowance.@type') === BASIC_ALLOWANCE) { + return get(allowance, 'allowance.spend_limit', []); + } else { + return get(allowance, 'allowance.basic.spend_limit', []); + } + } +}; + +export const getPeriodExpiryDate = ( + allowance: BasicAllowance | PeriodicAllowance | AllowedMsgAllowance +) => { + if (get(allowance, '@type') === PERIODIC_ALLOWANCE) { + return get(allowance, 'period_reset', ''); + } else { + return get(allowance, 'allowance.period_reset', ''); + } +}; + +export const getPeriodSpendLimit = ( + allowance: BasicAllowance | PeriodicAllowance | AllowedMsgAllowance +) => { + if (get(allowance, '@type') === PERIODIC_ALLOWANCE) { + return get(allowance, 'period_spend_limit', []); + } else { + return get(allowance, 'allowance.period_spend_limit', []); + } +}; diff --git a/frontend/src/utils/localStorage.ts b/frontend/src/utils/localStorage.ts index 6e8668fd6..b71104407 100644 --- a/frontend/src/utils/localStorage.ts +++ b/frontend/src/utils/localStorage.ts @@ -1,7 +1,12 @@ export const KEY_WALLET_NAME: string = 'WALLET_NAME'; export const KEY_DARK_MODE: string = 'DARK_MODE'; const AUTH_TOKEN_KEY_NAME: string = 'AUTH_TOKEN'; -const KEY_TRANSACTIONS = (address: string) => 'transactions' + ' ' + address; +const AUTHZ_KEY = 'Authz_key'; +const AUTHZ_VALUE = 'Authz_value'; +const FEEGRANT_KEY = 'feegrant_key'; +const FEEGRANT_VALUE = 'feegrant_value'; +const IBC_TXNS_KEY = 'IBC_Txns'; +const AUTHZ_ALERT_KEY = 'authz_alert'; interface LocalNetworks { [key: string]: Network; @@ -29,6 +34,9 @@ export function removeWalletName() { localStorage.removeItem(KEY_WALLET_NAME); } +export const isMetaMaskWallet = () => + localStorage.getItem(KEY_WALLET_NAME) === 'metamask'; + export function isConnected(): boolean { const connected = localStorage.getItem('CONNECTED'); if (connected && KEY_WALLET_NAME) { @@ -41,6 +49,8 @@ export function logout() { localStorage.removeItem('CONNECTED'); removeAllAuthTokens(); removeWalletName(); + removeAuthzAlertData(); + localStorage.clear(); } export function setLocalNetwork(networkConfig: Network, chainID: string) { @@ -53,6 +63,20 @@ export function setLocalNetwork(networkConfig: Network, chainID: string) { localStorage.setItem('localNetworks', JSON.stringify(localNetworksParsed)); } +export function removeLocalNetwork(chainID: string) { + const localNetworks = localStorage.getItem('localNetworks'); + if (localNetworks) { + const localNetworksParsed: LocalNetworks = JSON.parse(localNetworks); + if (localNetworksParsed[chainID]) { + delete localNetworksParsed[chainID]; + localStorage.setItem( + 'localNetworks', + JSON.stringify(localNetworksParsed) + ); + } + } +} + export function getLocalNetworks(): Network[] { const localNetworks = localStorage.getItem('localNetworks'); const networks: Network[] = []; @@ -76,36 +100,6 @@ export function getMainnets(): Network[] { return []; } -export function getTransactions(address: string): Transaction[] { - const transactions = localStorage.getItem(KEY_TRANSACTIONS(address)); - if (transactions) return JSON.parse(transactions); - return []; -} - -export function addTransactions(transactions: Transaction[], address: string) { - const key = KEY_TRANSACTIONS(address); - let storedTransactions = getTransactions(address); - storedTransactions = [...transactions, ...storedTransactions]; - localStorage.setItem(key, JSON.stringify(storedTransactions)); -} - -export function updateIBCStatus(address: string, txHash: string) { - const txns = getTransactions(address); - let updateNeeded = false; - const updatedTxns = txns.map((tx) => { - if (tx.transactionHash === txHash) { - updateNeeded = true; - return { ...tx, isIBCPending: false }; - } - return tx; - }); - - if (updateNeeded) { - const key = KEY_TRANSACTIONS(address); - localStorage.setItem(key, JSON.stringify(updatedTxns)); - } -} - export function setAuthToken(authToken: AuthToken) { const tokens = localStorage.getItem(AUTH_TOKEN_KEY_NAME); let authTokens = []; @@ -158,3 +152,130 @@ export function getAuthToken(chainID: string): AuthToken | null { export function removeAllAuthTokens() { localStorage.removeItem(AUTH_TOKEN_KEY_NAME); } + +export function checkAuthzKeyAddress(address: string): boolean { + return localStorage.getItem(AUTHZ_KEY) === address; +} + +export function checkFeegrantKeyAddress(address: string): boolean { + return localStorage.getItem(FEEGRANT_KEY) === address; +} + +export function getAuthzValueAddress(): string { + return localStorage.getItem(AUTHZ_VALUE) || ''; +} + +export function getFeegrantValueAddress(): string { + return localStorage.getItem(FEEGRANT_VALUE) || ''; +} + +export function logoutAuthzMode() { + localStorage.removeItem(AUTHZ_KEY); + localStorage.removeItem(AUTHZ_VALUE); +} + +export function logoutFeegrantMode() { + localStorage.removeItem(FEEGRANT_KEY); + localStorage.removeItem(FEEGRANT_VALUE); +} + +export function getAuthzMode(address: string): { + isAuthzModeOn: boolean; + authzAddress: string; +} { + if (!checkAuthzKeyAddress(address)) { + logoutAuthzMode(); + return { + isAuthzModeOn: false, + authzAddress: '', + }; + } + return { + isAuthzModeOn: true, + authzAddress: getAuthzValueAddress(), + }; +} + +export function getFeegrantMode(address: string): { + isFeegrantModeOn: boolean; + feegrantAddress: string; +} { + if (!checkFeegrantKeyAddress(address)) { + logoutFeegrantMode(); + return { + isFeegrantModeOn: false, + feegrantAddress: '', + }; + } + return { + isFeegrantModeOn: true, + feegrantAddress: getFeegrantValueAddress(), + }; +} + +export function setAuthzMode(address: string, authzAddress: string) { + localStorage.setItem(AUTHZ_KEY, address); + localStorage.setItem(AUTHZ_VALUE, authzAddress); +} + +export function setFeegrantMode(address: string, feegrantAddress: string) { + localStorage.setItem(FEEGRANT_KEY, address); + localStorage.setItem(FEEGRANT_VALUE, feegrantAddress); +} + +export function addIBCTxn(transaction: ParsedTransaction) { + const storedTransactions = localStorage.getItem(IBC_TXNS_KEY); + let parsedTransactions: Record = {}; + if (storedTransactions) { + parsedTransactions = JSON.parse(storedTransactions); + } + parsedTransactions[transaction.txhash] = transaction; + localStorage.setItem(IBC_TXNS_KEY, JSON.stringify(parsedTransactions)); +} + +export function getIBCTxn(txhash: string): ParsedTransaction | null { + const storedTransactions = localStorage.getItem(IBC_TXNS_KEY); + if (storedTransactions) { + const parsedTransactions = JSON.parse(storedTransactions); + return parsedTransactions[txhash]; + } + return null; +} + +export function updateIBCTransactionStatusInLocal(txHash: string) { + const txn = getIBCTxn(txHash); + if (txn) { + const storedTransactions = localStorage.getItem(IBC_TXNS_KEY); + if (storedTransactions) { + const parsedTransactions = JSON.parse(storedTransactions); + parsedTransactions[txHash] = { + ...parsedTransactions[txHash], + isIBCPending: false, + }; + localStorage.setItem(IBC_TXNS_KEY, JSON.stringify(parsedTransactions)); + } + } +} + +export function setAuthzAlertData(display: boolean) { + localStorage.setItem(AUTHZ_ALERT_KEY, JSON.stringify({ display })); +} + +export function getAuthzAlertData() { + const authzAlertData = localStorage.getItem(AUTHZ_ALERT_KEY); + if (authzAlertData) { + return JSON.parse(authzAlertData).display; + } + localStorage.setItem(AUTHZ_ALERT_KEY, JSON.stringify({ display: true })); + return true; +} + +export function isAuthzAlertDataSet() { + const authzAlertData = localStorage.getItem(AUTHZ_ALERT_KEY); + if (authzAlertData) return true; + return false; +} + +export function removeAuthzAlertData() { + localStorage.removeItem(AUTHZ_ALERT_KEY); +} diff --git a/frontend/src/utils/networkConfigSchema.json b/frontend/src/utils/networkConfigSchema.json index 182e798f1..dbf0aec10 100644 --- a/frontend/src/utils/networkConfigSchema.json +++ b/frontend/src/utils/networkConfigSchema.json @@ -48,6 +48,12 @@ }, "required": ["menu", "toolbar"] }, + "supported_wallets": { + "type": "array", + "items": { + "type": "string" + } + }, "keplr_experimental": { "type": "boolean" }, @@ -57,6 +63,9 @@ "is_testnet": { "type": "boolean" }, + "gov_v1": { + "type": "boolean" + }, "explorer_tx_hash_endpoint": { "type": "string", "format": "uri" @@ -90,6 +99,20 @@ "type": "string", "format": "uri" }, + "restURIs": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, + "rpcURIs": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, "currencies": { "type": "array", "items": { @@ -273,11 +296,7 @@ "required": ["low", "average", "high"] } }, - "required": [ - "coin_denom", - "coin_minimal_denom", - "coin_decimals" - ] + "required": ["coin_denom", "coin_minimal_denom", "coin_decimals"] } }, "stake_currency": { @@ -316,11 +335,7 @@ } } }, - "required": [ - "coin_denom", - "coin_minimal_denom", - "coin_decimals" - ] + "required": ["coin_denom", "coin_minimal_denom", "coin_decimals"] }, "image": { "type": "string", @@ -348,6 +363,8 @@ "chain_name", "rest", "rpc", + "restURIs", + "rpcURIs", "currencies", "bip44", "bech32_config", @@ -363,9 +380,11 @@ "amino_config", "show_airdrop", "logos", + "supported_wallets", "keplr_experimental", "leap_experimental", "is_testnet", + "gov_v1", "explorer_tx_hash_endpoint", "config" ] diff --git a/frontend/src/utils/parseMsgs.ts b/frontend/src/utils/parseMsgs.ts index cde876209..5f0abb502 100644 --- a/frontend/src/utils/parseMsgs.ts +++ b/frontend/src/utils/parseMsgs.ts @@ -1,10 +1,18 @@ -import { parseCoins } from "@cosmjs/proto-signing"; - - -export const SEND_TYPE_URL = "/cosmos.bank.v1beta1.MsgSend"; -export const DELEGATE_TYPE_URL = "/cosmos.staking.v1beta1.MsgDelegate"; -export const UNDELEGATE_TYPE_URL = "/cosmos.staking.v1beta1.MsgUndelegate"; -export const REDELEGATE_TYPE_URL = "/cosmos.staking.v1beta1.MsgBeginRedelegate"; +import { parseCoins } from '@cosmjs/proto-signing'; +import { VOTE_TYPE_URL } from './constants'; +import { GovDepositMsg } from '@/txns/gov'; + +export const SEND_TYPE_URL = '/cosmos.bank.v1beta1.MsgSend'; +export const DELEGATE_TYPE_URL = '/cosmos.staking.v1beta1.MsgDelegate'; +export const UNDELEGATE_TYPE_URL = '/cosmos.staking.v1beta1.MsgUndelegate'; +export const REDELEGATE_TYPE_URL = '/cosmos.staking.v1beta1.MsgBeginRedelegate'; + +const voteOptionNumber: Record = { + yes: 1, + no: 3, + abstain: 2, + veto: 4, +}; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -14,10 +22,10 @@ export const parseSendMsgsFromContent = ( from: string, content: string ): [Msg[], string] => { - const messages = content.split("\n"); + const messages = content.split('\n'); if (messages?.length === 0) { - return [[], "no messages or invalid file content"]; + return [[], 'no messages or invalid file content']; } const msgs = []; @@ -30,11 +38,11 @@ export const parseSendMsgsFromContent = ( } } - return [msgs, ""]; + return [msgs, '']; }; const parseSendTx = (from: string, msg: string): Msg | null => { - const values = msg.split(","); + const values = msg.split(','); if (values?.length === 1) return null; if (values?.length !== 2) { throw new Error( @@ -42,11 +50,11 @@ const parseSendTx = (from: string, msg: string): Msg | null => { ); } - const to = values[0]; + const to = values[0].trim(); const amount = parseCoins(values[1]); if (amount.length === 0) { - throw new Error("amount cannot be empty"); + throw new Error('amount cannot be empty'); } return { @@ -65,10 +73,10 @@ export const parseDelegateMsgsFromContent = ( delegator: string, content: string ): [Msg[], string] => { - const messages = content.split("\n"); + const messages = content.split('\n'); if (messages?.length === 0) { - return [[], "no messages or invalid file content"]; + return [[], 'no messages or invalid file content']; } const msgs = []; @@ -81,23 +89,23 @@ export const parseDelegateMsgsFromContent = ( } } - return [msgs, ""]; + return [msgs, '']; }; const parseDelegateMsg = (delegator: string, msg: string): Msg | null => { - const values = msg.split(","); + const values = msg.split(','); if (values?.length === 1) return null; if (values?.length !== 2) { - throw new Error("invalid message"); + throw new Error('invalid message'); } - const validator = values[0]; + const validator = values[0].trim(); const amount = parseCoins(values[1]); if (amount.length === 0) { - throw new Error("amount cannot be empty"); + throw new Error('amount cannot be empty'); } return { @@ -116,10 +124,10 @@ export const parseUnDelegateMsgsFromContent = ( delegator: string, content: string ): [Msg[], string] => { - const messages = content.split("\n"); + const messages = content.split('\n'); if (messages?.length === 0) { - return [[], "no messages or invalid file content"]; + return [[], 'no messages or invalid file content']; } const msgs = []; @@ -132,22 +140,22 @@ export const parseUnDelegateMsgsFromContent = ( } } - return [msgs, ""]; + return [msgs, '']; }; const parseUnDelegateMsg = (delegator: string, msg: string): Msg | null => { - const values = msg.split(","); + const values = msg.split(','); if (values?.length === 1) return null; if (values?.length !== 2) { - throw new Error("invalid message"); + throw new Error('invalid message'); } - const validator = values[0]; + const validator = values[0].trim(); const amount = parseCoins(values[1]); if (amount.length === 0) { - throw new Error("amount cannot be empty"); + throw new Error('amount cannot be empty'); } return { @@ -166,10 +174,10 @@ export const parseReDelegateMsgsFromContent = ( delegator: string, content: string ): [Msg[], string] => { - const messages = content.split("\n"); + const messages = content.split('\n'); if (messages?.length === 0) { - return [[], "no messages or invalid file content"]; + return [[], 'no messages or invalid file content']; } const msgs = []; @@ -177,29 +185,29 @@ export const parseReDelegateMsgsFromContent = ( try { const msg = parseReDelegateMsg(delegator, messages[i]); if (msg && Object.keys(msg)?.length) msgs.push(msg); - } catch (error:any) { + } catch (error: any) { return [[], error?.message || `failed to parse message at ${i}`]; } } - return [msgs, ""]; + return [msgs, '']; }; const parseReDelegateMsg = (delegator: string, msg: string): Msg | null => { - const values = msg.split(","); + const values = msg.split(','); if (values?.length === 1) return null; if (values?.length !== 3) { - throw new Error("invalid message"); + throw new Error('invalid message'); } - const src = values[0]; - const dest = values[1]; + const src = values[0].trim(); + const dest = values[1].trim(); const amount = parseCoins(values[2]); if (amount.length === 0) { - throw new Error("amount cannot be empty"); + throw new Error('amount cannot be empty'); } return { @@ -211,4 +219,114 @@ const parseReDelegateMsg = (delegator: string, msg: string): Msg | null => { amount: amount[0], }, }; -}; \ No newline at end of file +}; + +// parseVoteMsgsFromContent returns list of parsed vote messages. It returns an error +// if provided content is invalid. +export const parseVoteMsgsFromContent = ( + from: string, + content: string +): [Msg[], string] => { + const messages = content.split('\n'); + + if (messages?.length === 0) { + return [[], 'no messages or invalid file content']; + } + + const msgs = []; + for (let i = 0; i < messages.length; i++) { + try { + const tx = parseVoteTx(from, messages[i]); + if (tx && Object.keys(tx)?.length) msgs.push(tx); + } catch (error: any) { + return [[], error?.message || `failed to parse message at ${i}`]; + } + } + + return [msgs, '']; +}; + +const parseVoteTx = (from: string, msg: string): Msg | null => { + const values = msg.split(','); + if (values?.length === 1) return null; + if (values?.length !== 2) { + throw new Error( + `invalid message: expected ${2} values got ${values.length}` + ); + } + const proposalId = values[0].trim(); + const voteOption = voteOptionNumber?.[values[1].trim()]; + + if (isNaN(Number(proposalId))) { + throw new Error(`Invalid proposal id: ${values[0]}`); + } + + if (!voteOption) { + throw new Error(`Invalid vote option: ${values[1]}`); + } + + return { + typeUrl: VOTE_TYPE_URL, + value: { + voter: from, + option: Number(voteOption), + proposalId: Number(proposalId), + }, + }; +}; + +// parseDepositMsgsFromContent returns list of parsed vote messages. It returns an error +// if provided content is invalid. +export const parseDepositMsgsFromContent = ( + from: string, + content: string +): [Msg[], string] => { + const messages = content.split('\n'); + + if (messages?.length === 0) { + return [[], 'no messages or invalid file content']; + } + + const msgs = []; + for (let i = 0; i < messages.length; i++) { + try { + const tx = parseDepositTx(from, messages[i]); + if (tx && Object.keys(tx)?.length) msgs.push(tx); + } catch (error: any) { + return [[], error?.message || `failed to parse message at ${i}`]; + } + } + + return [msgs, '']; +}; + +const parseDepositTx = (from: string, msg: string): Msg | null => { + const values = msg.split(','); + if (values?.length === 1) return null; + if (values?.length !== 2) { + throw new Error( + `invalid message: expected ${2} values got ${values.length}` + ); + } + + const proposalId = values[0].trim(); + const parsedProposalId = Number(proposalId); + if (isNaN(parsedProposalId)) { + throw new Error(`Invalid proposal id: ${values[0]}`); + } + + const amount = parseCoins(values[1]); + + if (amount.length === 0) { + throw new Error('amount cannot be empty'); + } + + const depositMsg = GovDepositMsg( + parsedProposalId, + from, + Number(amount[0].amount), + amount[0].denom + ); + + return depositMsg; +}; diff --git a/frontend/src/utils/signing.ts b/frontend/src/utils/signing.ts index ea76574d2..55a6cdc9f 100644 --- a/frontend/src/utils/signing.ts +++ b/frontend/src/utils/signing.ts @@ -35,6 +35,14 @@ import { ERR_NO_OFFLINE_AMINO_SIGNER, ERR_UNKNOWN } from './errors'; import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'; import { GAS_FEE } from './constants'; import { MsgCancelUnbondingDelegation } from 'cosmjs-types/cosmos/staking/v1beta1/tx'; +import { CosmjsOfflineSigner } from '@leapwallet/cosmos-snap-provider'; +import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'; +import { isMetaMaskWallet } from './localStorage'; +import { get } from 'lodash'; +import { axiosGetRequestWrapper } from './RequestWrapper'; + +const ETH_BASE_ACCOUNT_TYPE = '/ethermint.types.v1.EthAccount'; +const ETH_CHAIN_ACCOUNT_PREFIXES = 'dym'; declare const window: WalletWindow; @@ -53,35 +61,72 @@ const canUseAmino = (aminoConfig: AminoConfig, messages: Msg[]): boolean => { !aminoConfig.group ) { return false; + } else if ( + message.typeUrl.includes( + '/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission' + ) + ) { + return false; } } return true; }; -const getClient = async ( +export const getClient = async ( aminoConfig: AminoConfig, chainId: string, - messages: Msg[] + messages: Msg[], + granter?: string ): Promise => { let signer; - if (!canUseAmino(aminoConfig, messages)) { - try { - await window.wallet.enable(chainId); - signer = window.wallet.getOfflineSigner(chainId); - } catch (error) { - console.log(error); - throw new Error('failed to get wallet'); + if (isMetaMaskWallet()) { + if (!canUseAmino(aminoConfig, messages)) { + try { + await window.wallet.enable(chainId); + + signer = new CosmjsOfflineSigner(chainId); + } catch (error) { + console.log(error); + throw new Error('failed to get wallet'); + } + } else { + try { + await window.wallet.enable(chainId); + signer = new CosmjsOfflineSigner(chainId); + } catch (error) { + console.log(error); + throw new Error('failed to get wallet'); + } } } else { - try { - await window.wallet.enable(chainId); - signer = window.wallet.getOfflineSignerOnlyAmino(chainId); - } catch (error) { - console.log(error); - throw new Error('failed to get wallet'); + if (granter) { + try { + await window.wallet.enable(chainId); + signer = window.wallet.getOfflineSigner(chainId); + } catch (error) { + console.log(error); + throw new Error('failed to get wallet'); + } + } else if (!canUseAmino(aminoConfig, messages)) { + try { + await window.wallet.enable(chainId); + signer = window.wallet.getOfflineSigner(chainId); + } catch (error) { + console.log(error); + throw new Error('failed to get wallet'); + } + } else { + try { + await window.wallet.enable(chainId); + signer = window.wallet.getOfflineSignerOnlyAmino(chainId); + } catch (error) { + console.log(error); + throw new Error('failed to get wallet'); + } } } + return signer; }; @@ -94,12 +139,19 @@ export const signAndBroadcast = async ( memo: string, gasPrice: string, restUrl: string, - granter?: string + granter?: string, + rpc?: string, + restURLs?: Array ): Promise => { let signer: OfflineSigner; + let client: SigningCosmWasmClient; try { - signer = await getClient(aminoConfig, chainId, messages); + if (isMetaMaskWallet()) { + signer = await getClient(aminoConfig, chainId, messages); + } else { + signer = await getClient(aminoConfig, chainId, messages, granter); + } } catch (error) { console.log('error while getting client ', error); throw new Error('failed to get wallet'); @@ -131,25 +183,87 @@ export const signAndBroadcast = async ( memo, 1.2, gasPrice, + chainId, granter ); } const fee = getFee(gas, gasPrice, granter); - const txBody = await sign( - signer, - chainId, - aminoConfig, - aminoTypes, - accounts[0].address, - messages, - memo, - fee, - restUrl, - registry - ); - return await broadcast(txBody, restUrl); + if (isMetaMaskWallet()) { + try { + const offlineSigner = new CosmjsOfflineSigner(chainId); + const rpcEndpoint = rpc || ''; + try { + client = await SigningCosmWasmClient.connectWithSigner( + rpcEndpoint, + offlineSigner + ); + + const result = await client.signAndBroadcast( + accounts[0].address, + messages, + fee, + memo + ); + + const parseResult = parseTxResult({ + code: result?.code, + codespace: '', + data: '', + events: [], + gas_used: String(result?.gasUsed), + gas_wanted: String(result?.gasWanted), + height: String(result?.height), + info: '', + logs: [], + timestamp: '', + raw_log: '', + txhash: result?.transactionHash, + }); + + return Promise.resolve(parseResult); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + console.log('error connect with signer', error); + throw error?.message; + } + } catch (error: any) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + console.log('error in sign and broadcast', error); + throw error?.message; + } + } else { + const txBody = await sign( + signer, + chainId, + aminoConfig, + aminoTypes, + accounts[0].address, + messages, + memo, + fee, + restUrl, + registry + ); + + return await broadcast(txBody, restUrl, chainId, restURLs); + } + + // return Promise.resolve(parseTxResult({ + // code: 0, + // codespace: '', + // data: '', + // events: [], + // gas_used: '', + // gas_wanted: '', + // height: '', + // info: '', + // logs: [], + // timestamp: '', + // raw_log: '', + // txhash: '' + // })) }; function calculateFee( @@ -162,7 +276,17 @@ function calculateFee( amount: decodedGasPrice.amount.toFloatApproximation(), denom: decodedGasPrice.denom, }; - const num1 = multiply(processedGasPrice.amount, gasLimit); + + let num1; + if (isMetaMaskWallet()) { + num1 = multiply(processedGasPrice.amount, 1.2); + if (ceil(num1) <= 1) { + num1 = multiply(processedGasPrice.amount, gasLimit); + } + } else { + num1 = multiply(processedGasPrice.amount, gasLimit); + } + const num2 = bignumber(num1.toString()); const amount = ceil(num2); return { @@ -182,10 +306,14 @@ function getFee(gas: number, gasPrice: string, granter?: string): StdFee { return calculateFee(gas, gasPrice, granter); } -async function getAccount(restUrl: string, address: string): Promise { +async function getAccount( + restUrl: string, + address: string, + chainId: string +): Promise { try { const res: AxiosResponse = await axios.get( - restUrl + '/cosmos/auth/v1beta1/accounts/' + address + restUrl + '/cosmos/auth/v1beta1/accounts/' + address + `?chain=${chainId}` ); return res.data.account; @@ -208,9 +336,10 @@ async function simulate( memo: string, modifier: number, gasPrice: string, + chainId: string, granter?: string ): Promise { - const account = await getAccount(restUrl, address); + const account = await getAccount(restUrl, address, chainId); const fee = getFee(50_000, gasPrice, granter); const amount: Coin[] = fee.amount.map((coin) => { return { amount: coin.amount, denom: coin.denom }; @@ -231,7 +360,7 @@ async function simulate( try { const estimate = await axios - .post(restUrl + '/cosmos/tx/v1beta1/simulate', { + .post(restUrl + `/cosmos/tx/v1beta1/simulate?chain=${chainId}`, { tx_bytes: toBase64(TxRaw.encode(txBody).finish()), }) .then((el) => el.data.gas_info.gas_used); @@ -246,7 +375,9 @@ async function simulate( async function broadcast( txBody: TxRaw, - restUrl: string + restUrl: string, + chainId: string, + restURLs?: Array ): Promise { const timeoutMs = 60_000; const pollIntervalMs = 3_000; @@ -262,14 +393,25 @@ async function broadcast( timeoutMs / 1000 } seconds.` ); + + clearTimeout(txPollTimeout); } await sleep(pollIntervalMs); try { - const response = await axios.get( - restUrl + '/cosmos/tx/v1beta1/txs/' + txId - ); - const result = parseTxResult(response.data.tx_response); - return result; + if (restURLs?.length) { + const response = await axiosGetRequestWrapper( + restURLs, + `/cosmos/tx/v1beta1/txs/${txId}?chain=${chainId}` + ); + const result = parseTxResult(response.data.tx_response); + return result; + } else { + const response = await axios.get( + restUrl + '/cosmos/tx/v1beta1/txs/' + txId + `?chain=${chainId}` + ); + const result = parseTxResult(response.data.tx_response); + return result; + } } catch (error) { // if transaction index is disabled return txhash if (error instanceof AxiosError) { @@ -287,12 +429,22 @@ async function broadcast( } }; - const response = await axios.post(restUrl + '/cosmos/tx/v1beta1/txs', { - tx_bytes: toBase64(TxRaw.encode(txBody).finish()), - mode: 'BROADCAST_MODE_SYNC', - }); - console.log('response of the post txn ', response); - const result = parseTxResult(response.data.tx_response); + const response = await axios.post( + restUrl + '/cosmos/tx/v1beta1/txs' + `?chain=${chainId}`, + { + tx_bytes: toBase64(TxRaw.encode(txBody).finish()), + mode: 'BROADCAST_MODE_SYNC', + } + ); + let parsedData: any; + + if (typeof response.data === 'string') { + parsedData = JSON.parse(response.data); + } else { + parsedData = response.data; + } + + const result = parseTxResult(parsedData.tx_response); if (result.code !== 0) return result; // have ambiguous issues, todo... //assertIsDeliverTxSuccess(result); @@ -363,7 +515,17 @@ async function sign( authInfoBytes: Uint8Array; signatures: [Uint8Array] | [Buffer]; }> { - const account = await getAccount(restUrl, address); + let account = await getAccount(restUrl, address, chainId); + + if (get(account, '@type') === ETH_BASE_ACCOUNT_TYPE) + account = { + '@type': get(account, '@type') || '', + address: get(account, 'base_account.address') || '', + account_number: get(account, 'base_account.account_number') || '', + pub_key: get(account, 'base_account.pub_key'), + sequence: get(account, 'base_account.sequence') || '', + }; + const { account_number, sequence } = account; const txBodyBytes = makeBodyBytes(registry, messages, memo); let aminoMsgs; @@ -376,39 +538,42 @@ async function sign( // if messages are amino and signer is amino signer if (aminoMsgs && !isOfflineDirectSigner(signer)) { // Sign as amino if possible for Ledger and wallet support - const signDoc = makeAminoSignDoc( - aminoMsgs, - fee, - chainId, - memo, - account_number, - sequence - ); - const { signature, signed } = await signer.signAmino(address, signDoc); - const amount: Coin[] = signed.fee.amount.map((coin) => { - return { amount: coin.amount, denom: coin.denom }; - }); - const authInfoBytes = await makeAuthInfoBytes( - signer, - account, - { - amount: amount, - gasLimit: BigInt(signed.fee.gas), - granter: signed.fee.granter, - }, - SignMode.SIGN_MODE_LEGACY_AMINO_JSON - ); - return { - bodyBytes: makeBodyBytes(registry, messages, signed.memo), - authInfoBytes: authInfoBytes, - signatures: [Buffer.from(signature.signature, 'base64')], - }; - } + try { + const signDoc = makeAminoSignDoc( + aminoMsgs, + fee, + chainId, + memo, + account_number, + sequence + ); - // if messages are amino and signer is not amino signer - if (aminoMsgs) { - throw new Error(ERR_NO_OFFLINE_AMINO_SIGNER); + const { signature, signed } = await signer.signAmino(address, signDoc); + const amount: Coin[] = signed.fee.amount.map((coin) => { + return { amount: coin.amount, denom: coin.denom }; + }); + + const authInfoBytes = await makeAuthInfoBytes( + signer, + account, + { + amount: amount, + gasLimit: BigInt(signed.fee.gas), + granter: signed.fee.granter, + }, + SignMode.SIGN_MODE_LEGACY_AMINO_JSON + ); + + return { + bodyBytes: makeBodyBytes(registry, messages, signed.memo), + authInfoBytes: authInfoBytes, + signatures: [Buffer.from(signature.signature, 'base64')], + }; + } catch (error) { + console.log('error while make auth info bytes', error); + throw new Error('Request rejected'); + } } // if the signer is direct signer @@ -423,18 +588,31 @@ async function sign( }, SignMode.SIGN_MODE_DIRECT ); + const signDoc = makeSignDoc( txBodyBytes, authInfoBytes, chainId, +account_number ); - const { signature, signed } = await signer.signDirect(address, signDoc); - return { - bodyBytes: signed.bodyBytes, - authInfoBytes: signed.authInfoBytes, - signatures: [fromBase64(signature.signature)], - }; + + try { + const { signature, signed } = await signer.signDirect(address, signDoc); + + return { + bodyBytes: signed.bodyBytes, + authInfoBytes: signed.authInfoBytes, + signatures: [fromBase64(signature.signature)], + }; + } catch (error) { + console.log('error while sign direct', error); + throw new Error('Request rejected'); + } + } + + // if messages are amino and signer is not amino signer + if (aminoMsgs) { + throw new Error(ERR_NO_OFFLINE_AMINO_SIGNER); } // any other case by default @@ -471,7 +649,12 @@ async function makeAuthInfoBytes( signerInfos: [ { publicKey: { - typeUrl: pubkeyTypeUrl(account.pub_key), + typeUrl: pubkeyTypeUrl( + account?.pub_key, + account.address.includes(ETH_CHAIN_ACCOUNT_PREFIXES) + ? 60 + : undefined + ), value: comsjsPubKey .encode({ key: signerPubkey, @@ -486,7 +669,10 @@ async function makeAuthInfoBytes( }).finish(); } -const pubkeyTypeUrl = (pub_key: PubKey, coinType?: number): string => { +const pubkeyTypeUrl = ( + pub_key?: globalThis.PubKey, + coinType?: number +): string => { if (pub_key && pub_key['@type']) return pub_key['@type']; if (coinType === 60) { diff --git a/frontend/src/utils/transaction.ts b/frontend/src/utils/transaction.ts index 272c19669..fb8523360 100644 --- a/frontend/src/utils/transaction.ts +++ b/frontend/src/utils/transaction.ts @@ -1,28 +1,52 @@ import { msgSendTypeUrl, - serialize as serializeMsgSend, + SendMsg, + formattedSerialize as serializeMsgSend, } from '@/txns/bank/send'; import { msgWithdrawRewards, serialize as serializeMsgClaim, + WithdrawAllRewardsMsg, } from '@/txns/distribution/withDrawRewards'; import { + Delegate, msgDelegate, serialize as serializeMsgDelegate, } from '@/txns/staking/delegate'; import { msgReDelegate, + Redelegate, serialize as serializeMsgRedelegte, } from '@/txns/staking/redelegate'; import { msgUnDelegate, serialize as serializeMsgUndelegte, + UnDelegate, } from '@/txns/staking/undelegate'; import { getTimeDifference } from './dataTime'; import { msgTransfer, serialize as serializeMsgTransfer, } from '@/txns/ibc/transfer'; +import { serialize as serializeMsgExec } from '@/txns/authz/exec'; +import { msgAuthzExecTypeUrl } from '@/txns/authz/exec'; +import { + msgAuthzGrantTypeUrl, + serializeMsgGrantAuthz, +} from '@/txns/authz/grant'; +import { + msgFeegrantGrantTypeUrl, + serializeMsgGrantAllowance, +} from '@/txns/feegrant/grant'; +import { GovVoteMsg, msgVoteTypeUrl, serializeMsgVote } from '@/txns/gov/vote'; +import { + GovDepositMsg, + msgDepositTypeUrl, + serializeMsgDeposit, +} from '@/txns/gov/deposit'; +import { voteOptionNumber, voteOptions } from './constants'; +import { msgAuthzRevokeTypeUrl } from '@/txns/authz/revoke'; +import { revokeTypeUrl } from '@/txns/feegrant/revoke'; export function NewTransaction( txResponse: ParsedTxResponse, @@ -65,39 +89,70 @@ export const MsgType = (msg: string): string => { return 'Claim'; case msgTransfer: return 'IBC'; + case msgAuthzExecTypeUrl: + return 'Authz-permission'; + case msgAuthzGrantTypeUrl: + return 'Grant-Authz'; + case msgFeegrantGrantTypeUrl: + return 'Grant-Allowance'; + case msgVoteTypeUrl: + return 'Vote'; + case msgDepositTypeUrl: + return 'Deposit'; default: - return 'Todo: add type'; + return msg; } }; -export const serializeMsg = (msg: Msg): string => { +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const serializeMsg = ( + msg: any, + decimals: number, + originDenom: string, + txHash: string +): string => { if (!msg) return 'No Message'; - switch (msg.typeUrl) { + switch (msg['@type']) { case msgDelegate: - return serializeMsgDelegate(msg); + return serializeMsgDelegate(msg, decimals, originDenom); case msgUnDelegate: - return serializeMsgUndelegte(msg); + return serializeMsgUndelegte(msg, decimals, originDenom); case msgReDelegate: - return serializeMsgRedelegte(msg); + return serializeMsgRedelegte(msg, decimals, originDenom); case msgSendTypeUrl: - return serializeMsgSend(msg); + return serializeMsgSend(msg, decimals, originDenom, true); case msgWithdrawRewards: return serializeMsgClaim(msg); case msgTransfer: - return serializeMsgTransfer(msg); + return serializeMsgTransfer(msg, decimals, originDenom); + case msgAuthzExecTypeUrl: + return serializeMsgExec(); + case msgAuthzGrantTypeUrl: + return serializeMsgGrantAuthz(msg); + case msgFeegrantGrantTypeUrl: + return serializeMsgGrantAllowance(msg); + case msgVoteTypeUrl: + return serializeMsgVote(msg); + case msgDepositTypeUrl: + return serializeMsgDeposit(msg, decimals, originDenom); default: - return `Todo: serialize message ${msg.typeUrl}`; + return txHash; } }; -export const formatTransaction = (tx: Transaction, msgFilters: string[]) => { - const msgs = tx.msgs; +export const formatTransaction = ( + tx: ParsedTransaction, + msgFilters: string[], + decimals: number, + originDenom: string +) => { + const msgs = tx.messages; const showMsgs: [string, string, boolean] = ['', '', false]; if (msgs[0]) { - showMsgs[0] = MsgType(msgs[0].typeUrl); + showMsgs[0] = MsgType(msgs[0]?.['@type']); } if (msgs[1]) { - showMsgs[1] = MsgType(msgs[1].typeUrl); + showMsgs[1] = MsgType(msgs[1]?.['@type']); } if (msgs.length > 2) { showMsgs[2] = true; @@ -108,14 +163,19 @@ export const formatTransaction = (tx: Transaction, msgFilters: string[]) => { const filterSet = new Set(msgFilters); if (!msgs.length) showTx = true; msgs.forEach((msg) => { - if (filterSet.has(MsgType(msg.typeUrl))) showTx = true; + if (filterSet.has(MsgType(msg['@type']))) showTx = true; }); } const msgCount = msgs.length; const isTxSuccess = tx.code === 0; - const time = getTimeDifference(tx.time); - const firstMessage = serializeMsg(tx.msgs[0]); + const time = getTimeDifference(tx.timestamp); + const firstMessage = serializeMsg( + tx.messages[0], + decimals, + originDenom, + tx.txhash + ); return { showMsgs, isTxSuccess, @@ -123,7 +183,177 @@ export const formatTransaction = (tx: Transaction, msgFilters: string[]) => { firstMessage, msgCount, showTx, - isIBC: tx.isIBC, + isIBC: tx.isIBCTxn, isIBCPending: tx.isIBCPending, }; }; + +export const NewIBCTransaction = ( + txResponse: ParsedTxResponse, + msgs: Msg[], + chainID: string, + address: string, + isIBC?: boolean, + isIBCPending?: boolean +): ParsedTransaction => { + const transaction: ParsedTransaction = { + code: txResponse.code, + txhash: txResponse.transactionHash, + height: txResponse.height || '-', + raw_log: txResponse.rawLog || '-', + gas_used: txResponse.gasUsed || '-', + gas_wanted: txResponse.gasWanted || '-', + fee: txResponse.fee || [], + timestamp: txResponse.time || new Date().toISOString(), + messages: [msgs[0]?.value], + chain_id: chainID, + address, + memo: txResponse.memo || '', + isIBCTxn: !!isIBC, + isIBCPending: !!isIBCPending, + }; + return transaction; +}; + +export const formattedMsgType = (msgType: string) => { + if (!msgType) return 'No Message'; + switch (msgType) { + case msgDelegate: + return 'Delegate'; + case msgUnDelegate: + return 'UnDelegate'; + case msgReDelegate: + return 'ReDelegate'; + case msgSendTypeUrl: + return 'Send'; + case msgWithdrawRewards: + return 'Claim Rewards'; + case msgTransfer: + return 'IBC Send'; + case msgAuthzExecTypeUrl: + return 'Exec Authz'; + case msgAuthzGrantTypeUrl: + return 'Grant Authz'; + case msgAuthzRevokeTypeUrl: + return 'Revoke Authz'; + case msgFeegrantGrantTypeUrl: + return 'Grant Allowance'; + case revokeTypeUrl: + return 'Revoke Allowance'; + case msgVoteTypeUrl: + return 'Vote'; + case msgDepositTypeUrl: + return 'Deposit'; + default: + return msgType.split('.').slice(-1)?.[0] || msgType; + } +}; + +export const buildMessages = (messages: any[]): Msg[] => { + const result: Msg[] = []; + messages.forEach((msg) => { + const formattedMsg = getMessage(msg); + if (formattedMsg) result.push(formattedMsg); + }); + return result; +}; + +const getDelegateMsg = (message: any) => { + const { amount, validator_address, delegator_address } = message; + const { amount: delegationAmount, denom } = amount; + const msg = Delegate( + delegator_address, + validator_address, + delegationAmount, + denom + ); + return msg; +}; + +const getUndelegateMsg = (message: any) => { + const { amount, validator_address, delegator_address } = message; + const { amount: undelegationAmount, denom } = amount; + const msg = UnDelegate( + delegator_address, + validator_address, + undelegationAmount, + denom + ); + return msg; +}; + +const getRedelegateMsg = (message: any) => { + const { + validator_src_address, + validator_dst_address, + delegator_address, + amount, + } = message; + const { amount: delegationAmount, denom } = amount; + const msg = Redelegate( + delegator_address, + validator_src_address, + validator_dst_address, + delegationAmount, + denom + ); + return msg; +}; + +const getSendMsg = (message: any) => { + const { to_address, amount, from_address } = message; + const { amount: sendAmount, denom } = amount[0]; + const msg = SendMsg(from_address, to_address, sendAmount, denom); + return msg; +}; + +const getVoteMsg = (message: any) => { + const { option, proposal_id, voter } = message; + const voteOption = voteOptions?.[option] || ''; + const vote = voteOptionNumber[voteOption] || -1; + const msg = GovVoteMsg(proposal_id, voter, vote); + return msg; +}; + +const getDepositMsg = (message: any) => { + const { proposal_id, depositor, amount } = message; + const { amount: depositAmount, denom } = amount[0]; + const msg = GovDepositMsg(proposal_id, depositor, depositAmount, denom); + return msg; +}; + +const getWithdrawRewardsMsg = (message: any) => { + const { delegator_address, validator_address } = message; + const msg = WithdrawAllRewardsMsg(delegator_address, validator_address); + return msg; +}; + +export const getMessage = (message: any) => { + const msgType = message?.['@type']; + switch (msgType) { + case msgDelegate: + return getDelegateMsg(message); + case msgUnDelegate: + return getUndelegateMsg(message); + case msgReDelegate: + return getRedelegateMsg(message); + case msgSendTypeUrl: + return getSendMsg(message); + case msgWithdrawRewards: + return getWithdrawRewardsMsg(message); + case msgTransfer: + return null; + case msgAuthzExecTypeUrl: + return null; + case msgAuthzGrantTypeUrl: + return null; + case msgFeegrantGrantTypeUrl: + return null; + case msgVoteTypeUrl: + return getVoteMsg(message); + case msgDepositTypeUrl: + return getDepositMsg(message); + default: + return null; + } +}; diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index 6ce54cb01..cebc19449 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -1,9 +1,40 @@ +'use client'; + import { DelegationResponse, Params, Validator } from '@/types/staking'; import { parseBalance } from './denom'; -import { MultisigThresholdPubkey, SinglePubkey } from '@cosmjs/amino'; +import { + MultisigThresholdPubkey, + SinglePubkey, + pubkeyToAddress, +} from '@cosmjs/amino'; import { Options } from '@/custom-hooks/useSortedAssets'; -import { getAuthToken, removeAllAuthTokens } from './localStorage'; +import { + getAuthToken, + getWalletName, + removeAllAuthTokens, +} from './localStorage'; import { MultisigAddressPubkey } from '@/types/multisig'; +import { fromBech32 } from '@cosmjs/encoding'; +import { SUPPORTED_WALLETS } from './constants'; +import { FAILED_TO_FETCH } from './errors'; +import { FastAverageColor } from 'fast-average-color'; +import ReactGA from 'react-ga'; +import { getLocalTime } from '@/utils/dataTime'; +import { GOV_VOTE_OPTIONS } from '@/constants/gov-constants'; + +export function initializeGA () { + ReactGA.initialize('G-RTXGXXDNNS'); + console.log("GA INITIALIZED"); + trackEvent('TRANSFER', 'MULTI_SEND', 'SUCCESS') +}; + +export const trackEvent = (category: string, action: string, label: string) => { + ReactGA.event({ + category: category, + action: action, + label: label, + }); +}; export const convertPaginationToParams = ( pagination?: KeyLimitPagination @@ -56,20 +87,24 @@ export const getSelectedPartFromURL = (urlParts: string[]): string => { switch (urlParts[1]) { case 'staking': return 'Staking'; - case 'feegrant': - return 'Feegrant'; case 'governance': return 'Governance'; case 'groups': return 'Groups'; - case 'authz': - return 'Authz'; case 'multisig': return 'Multisig'; case 'transfers': return 'Transfers'; case 'history': return 'History'; + case 'validator': + return 'Staking'; + case 'cosmwasm': + return 'Cosmwasm'; + case 'multiops': + return 'Multiops'; + case 'settings': + return 'Settings'; default: return 'Overview'; } @@ -98,6 +133,15 @@ export const formatDollarAmount = (amount: number): string => { ); }; +export const formatAmountToString = (amount: number): string => { + if (amount === 0) return '0'; + if (amount < 0.1) return '< 0.1'; + return amount.toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }); +}; + export const formatAmount = (amount: number): string => { return amount.toLocaleString('en-US', { minimumFractionDigits: 0, @@ -118,20 +162,28 @@ export const formatCoin = (amount: number, denom: string): string => { }; export function formatNumber(number: number): string { - if (number <= 999) return number + ''; - const suffixes = ['', 'K', 'M', 'B', 'T']; - const tier = (Math.log10(Math.abs(number)) / 3) | 0; - - if (tier === 0) return number.toString(); - - const suffix = suffixes[tier]; - const scale = Math.pow(10, tier * 3); - - const scaledNumber = number / scale; - - const formattedNumber = parseFloat(scaledNumber.toFixed(2)); - - return formattedNumber.toString() + suffix; + // Check for trillions + if (Math.abs(number) >= 1.0e12) { + return (number / 1.0e12).toFixed(2) + 'T'; // Trillions + } + // Check for billions + else if (Math.abs(number) >= 1.0e9) { + return (number / 1.0e9).toFixed(2) + 'B'; // Billions + } + // Check for more than millions + else if (Math.abs(number) > 1.0e6) { + return (number / 1.0e6).toFixed(2) + 'M'; // Millions + } else if (Math.abs(number) <= 1.0e6 && Math.abs(number) > 1.0e3) { + return Math.trunc(number).toLocaleString('en-US'); + // No decimals, formatted with commas + } + // Less than a thousand + else { + return number.toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }); + } } export const getDaysLeftString = (daysLeft: number): string => { @@ -150,6 +202,34 @@ export function shortenMsg(Msg: string, maxCharacters: number) { return Msg.slice(0, maxCharacters) + '...'; } +export function shortenString(str: string, maxCharacters: number): string { + if (!str.length) { + return ''; + } + if (maxCharacters >= str.length) { + return str; + } + + const middle = Math.floor(str.length / 2); + let firstPart = str.slice(0, middle); + let lastPart = str.slice(middle); + + maxCharacters -= 3; + + while (maxCharacters < firstPart.length + lastPart.length) { + if ( + (firstPart.length + lastPart.length) % 2 === 1 && + firstPart.length > 0 + ) { + firstPart = firstPart.slice(0, firstPart.length - 1); + } else { + lastPart = lastPart.slice(1); + } + } + + return `${firstPart}...${lastPart}`; +} + export function shortenAddress(bech32: string, maxCharacters: number) { if (maxCharacters >= bech32?.length) { return bech32; @@ -193,7 +273,7 @@ export const getValidatorRank = ( return '#-'; }; -function convertSnakeToCamelCase(key: string): string { +export function convertSnakeToCamelCase(key: string): string { return key.replace(/_([a-z])/g, (match, group1) => group1.toUpperCase()); } @@ -210,7 +290,6 @@ export function convertKeysToCamelCase(data: any): any { const cleanedKey = removeQuotesFromKey(key); const camelCaseKey = convertSnakeToCamelCase(cleanedKey); convertedData[camelCaseKey] = convertKeysToCamelCase(value); - console.log(camelCaseKey); } return convertedData; } @@ -229,15 +308,58 @@ export const tabLink = (link: string, chainName: string): string => { }; export const allNetworksLink = (pathParts: string[]): string => { + if (pathParts.includes('settings')) { + if (pathParts.includes('feegrant')) { + if (pathParts.includes('new-feegrant')) { + return '/settings/feegrant/new-feegrant'; + } + return '/settings/feegrant'; + } else if (pathParts.includes('authz')) { + if (pathParts.includes('new-authz')) { + return '/settings/authz/new-authz'; + } + return '/settings/authz'; + } else { + return '/settings'; + } + } + if (pathParts.includes('transactions')) { + if (pathParts.includes('builder')) { + return '/transactions/builder'; + } + if (pathParts.includes('history')) { + return '/transactions/history'; + } + } return pathParts[1] === 'overview' || pathParts[1] === '' ? '/' - : '/' + pathParts[1]; + : pathParts[1] === 'validator' + ? '/staking' + : '/' + pathParts[1]; }; export const changeNetworkRoute = ( pathName: string, chainName: string ): string => { + if (pathName.includes('feegrant')) { + if (pathName.includes('new-feegrant')) { + return '/settings/feegrant/new-feegrant'; + } + return '/settings/feegrant/' + chainName.toLowerCase(); + } + if (pathName.includes('authz')) { + if (pathName.includes('new-authz')) { + return '/settings/authz/new-authz'; + } + return '/settings/authz/' + chainName.toLowerCase(); + } + if (pathName.includes('builder')) { + return '/transactions/builder/' + chainName.toLowerCase(); + } + if (pathName.includes('history')) { + return '/transactions/history/' + chainName.toLowerCase(); + } const route = pathName === '/' ? '/overview' : '/' + pathName.split('/')?.[1]; return `${route}/${chainName.toLowerCase()}`; }; @@ -372,6 +494,13 @@ export const getTxnURL = ( return cleanURL(explorerTxHashEndpoint) + '/' + hash; }; +export const getTxnURLOnResolute = ( + chainName: string, + hash: string +): string => { + return `/transactions/history/${chainName.toLowerCase()}/${hash}`; +}; + export const parseAmount = (amount: Coin[], currency: Currency) => { return formatCoin( parseBalance(amount, currency.coinDecimals, currency.coinMinimalDenom), @@ -383,3 +512,184 @@ export function getRandomNumber(min: number, max: number): number { const randomNumber = Math.random() * (max - min) + min; return Math.floor(randomNumber); } + +export const shortenName = (name: string, maxLength: number): string => { + if (name?.length) + return name?.length > maxLength + ? `${name.substring(0, maxLength)}...` + : name; + return ''; +}; + +export const convertToSnakeCase = (name: string) => { + return name.toLowerCase().replace(/ /g, '_') || ''; +}; + +export function amountToMinimalValue(amount: number, coinDecimals: number) { + return Number(amount) * 10 ** coinDecimals; +} + +export function isMultisigAccountMember( + walletAddress: string, + pubKeys: PubKey[], + prefix: string +): boolean { + const result = pubKeys?.filter((pubKey) => { + const address = + pubkeyToAddress( + { + type: 'tendermint/PubKeySecp256k1', + value: pubKey.key, + }, + prefix + ) || ''; + return walletAddress === address; + }); + return result?.length !== 0; +} + +export const validateAddress = (address: string) => { + if (address?.length) { + try { + fromBech32(address); + return true; + } catch (error) { + return false; + } + } + return false; +}; + +export function getTypeURLName(url: string) { + if (!url) { + return '-'; + } + const temp = url.split('.'); + if (temp?.length > 0) { + const msg = temp[temp?.length - 1]; + return msg.slice(3, msg.length); + } + return '-'; +} + +/** + * @example + * Input: WithdrawDelegatorRewards + * Output: Withdraw Delegator Rewards + */ +export function convertToSpacedName(camelCaseName: string): string { + const spacedName = camelCaseName.replace(/([a-z])([A-Z])/g, '$1 $2'); + + return spacedName.charAt(0).toUpperCase() + spacedName.slice(1); +} + +export function formatValidatorStatsValue( + value: number | string, + precision: number +) { + const numValue = Number(value); + return isNaN(numValue) + ? '-' + : Number(numValue.toFixed(precision)).toLocaleString(); +} + +export function extractContractMessages(inputString: string): string[] { + let errMsg = ''; + if (inputString.includes('expected')) { + errMsg = inputString.split('expected')[1]; + } else if (inputString.includes('missing')) { + errMsg = inputString.split('missing')[1]; + } else { + errMsg = inputString; + } + const pattern: RegExp = /`(\w+)`/g; + + const matches: string[] = []; + let match: RegExpExecArray | null; + while ((match = pattern.exec(errMsg)) !== null) { + matches.push(match[1]); + } + + return matches; +} + +export const getFormattedFundsList = ( + funds: FundInfo[], + fundsInput: string, + attachFundType: string +) => { + if (attachFundType === 'select') { + const result: { + denom: string; + amount: string; + }[] = []; + funds.forEach((fund) => { + if (fund.amount.length) { + result.push({ + denom: fund.denom, + amount: (Number(fund.amount) * 10 ** fund.decimals).toString(), + }); + } + }); + return result; + } else if (attachFundType === 'json') { + try { + const parsedFunds = JSON.parse(fundsInput); + return parsedFunds; + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (error: any) { + console.log(error); + } + } +}; + +export const getConnectWalletLogo = () => { + const wallets = SUPPORTED_WALLETS; + const walletName = getWalletName(); + const wallet = wallets.find( + (wallet) => wallet.name.toLowerCase() === walletName.toLowerCase() + ); + + return wallet ? wallet.logo : ''; +}; + +export const isNetworkError = (errMsg: string) => { + if (errMsg?.toLowerCase()?.includes(FAILED_TO_FETCH.toLowerCase())) + return true; + return false; +}; + +export const addChainIDParam = (uri: string, chainID: string) => { + let updatedURI: string; + if (uri.includes('?')) { + updatedURI = `${uri}&chain=${chainID.toLowerCase()}`; + } else { + updatedURI = `${uri}?chain=${chainID.toLowerCase()}`; + } + return updatedURI; +}; + +export const getFAC = () => { + const fac = new FastAverageColor(); + return fac; +}; + +export const parseTxnData = (txn: ParsedTransaction) => { + const success = txn.code === 0 ? true : false; + const messages = txn.messages; + const txHash = txn.txhash; + const timeStamp = getLocalTime(txn.timestamp); + return { + success, + messages, + txHash, + timeStamp, + }; +}; + +export const getColorForVoteOption = (optionLabel: string) => { + const matchedOption = GOV_VOTE_OPTIONS.find( + (option) => option.label.toLowerCase() === optionLabel + ); + return matchedOption ? matchedOption.selectedColor : '#ffffff'; +}; \ No newline at end of file diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 72beaba32..6e0c4510b 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -8,6 +8,9 @@ const config: Config = { ], theme: { extend: { + colors: { + customTextColor: '#fffffff0', + }, backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': @@ -17,7 +20,9 @@ const config: Config = { screens: { lg: '1600px', md: '1350px', - sm: '1150px' + sm: '1150px', + max: '1512px', + desktop: '1480px', }, }, plugins: [], diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9b0c48abd..64560247b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,6 +2,19 @@ # yarn lockfile v1 +"@0xsquid/sdk@^1.14.15": + version "1.14.15" + resolved "https://registry.yarnpkg.com/@0xsquid/sdk/-/sdk-1.14.15.tgz#ce74445b838a8f1321a96c796940a2106a58ada6" + integrity sha512-MJUoKYnlqqrkUb1h+6tePPL1luf/ppG9fxD2IrTcTuN1Z/2NYfAlF2jd2J+0MpMVKXpSZishzbW3sBFPjPej8Q== + dependencies: + "@cosmjs/cosmwasm-stargate" "^0.32.2" + "@cosmjs/encoding" "^0.32.2" + "@cosmjs/stargate" "^0.32.2" + axios "^0.27.2" + cosmjs-types "^0.9.0" + ethereum-multicall "2.23.0" + ethers "^5.7.1" + "@aashutoshrathi/word-wrap@^1.2.3": version "1.2.6" resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" @@ -18,9 +31,9 @@ integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== "@apollo/client@^3.5.8": - version "3.8.8" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.8.8.tgz#1a004b2e6de4af38668249a7d7790f6a3431e475" - integrity sha512-omjd9ryGDkadZrKW6l5ktUAdS4SNaFOccYQ4ZST0HLW83y8kQaSZOCTNlpkoBUK8cv6qP8+AxOKwLm2ho8qQ+Q== + version "3.8.10" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.8.10.tgz#db6ee4378212d93c1f22b90a2aa474f6e9664b68" + integrity sha512-p/22RZ8ehHyvySnC20EHPPe0gdu8Xp6ZCiXOfdEe1ZORw5cUteD/TLc66tfKv8qu8NLIfbiWoa+6s70XnKvxqg== dependencies: "@graphql-typed-document-node/core" "^3.1.1" "@wry/equality" "^0.5.6" @@ -35,46 +48,10 @@ tslib "^2.3.0" zen-observable-ts "^1.2.5" -"@axelar-network/axelar-cgp-solidity@^4.5.0": - version "4.5.0" - resolved "https://registry.yarnpkg.com/@axelar-network/axelar-cgp-solidity/-/axelar-cgp-solidity-4.5.0.tgz#f0456a2a6665302613a4d5243282ce1b18842348" - integrity sha512-4F4rmHei0cmzeUR7/mW4Bap5rc/KlPV2crD9HA7HTRfl15mVcN6/3z8p+pAm9We6bOrQplNW9KBZ3HJFP3C1Gw== - -"@axelar-network/axelarjs-sdk@^0.13.6": - version "0.13.7" - resolved "https://registry.yarnpkg.com/@axelar-network/axelarjs-sdk/-/axelarjs-sdk-0.13.7.tgz#953b1866ae0c1efdf8e3abd0adbda71bcf3543f3" - integrity sha512-fqb31pXE/NoAmVUWIz+Dd5b+YYA893OD0ghZqlPicXagaozDnZZZVgPv0deythh4DWAqWl9D0dz0exbbkrL83A== - dependencies: - "@axelar-network/axelar-cgp-solidity" "^4.5.0" - "@axelar-network/axelarjs-types" "^0.33.0" - "@cosmjs/json-rpc" "^0.30.1" - "@cosmjs/stargate" "0.31.0-alpha.1" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/networks" "^5.7.1" - "@ethersproject/providers" "^5.7.2" - "@types/uuid" "^8.3.1" - bech32 "^2.0.0" - clone-deep "^4.0.1" - cross-fetch "^3.1.5" - ethers "^5.7.2" - socket.io-client "^4.6.1" - standard-http-error "^2.0.1" - string-similarity-js "^2.1.4" - uuid "^8.3.2" - ws "^8.13.0" - -"@axelar-network/axelarjs-types@^0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@axelar-network/axelarjs-types/-/axelarjs-types-0.33.0.tgz#070ffbaec6be57259b64a41ee14f98804907732e" - integrity sha512-aCbX/5G+tgWPjr9tl3dQfJftWwRMkILz61ytach7dKqxtO9G9jlxpNvELJQ6gKVOodUtSY8qBCO/fWU19v4hdQ== - dependencies: - long "^5.2.0" - protobufjs "^7.0.0" - "@babel/code-frame@^7.0.0": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.4.tgz#03ae5af150be94392cb5c7ccd97db5a19a5da6aa" - integrity sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA== + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== dependencies: "@babel/highlight" "^7.23.4" chalk "^2.4.2" @@ -105,33 +82,26 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.4.tgz#36fa1d2b36db873d25ec631dcc4923fdc1cf2e2e" - integrity sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/runtime@^7.21.0": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d" - integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ== +"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.23.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" + integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== dependencies: regenerator-runtime "^0.14.0" "@babel/types@^7.22.15": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.4.tgz#7206a1810fc512a7f7f7d4dace4cb4c1c9dbfb8e" - integrity sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ== + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" + integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== dependencies: "@babel/helper-string-parser" "^7.23.4" "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@chain-registry/types@^0.17.0": - version "0.17.0" - resolved "https://registry.yarnpkg.com/@chain-registry/types/-/types-0.17.0.tgz#bbe9176a6d30a491259fab1fcdcee2b7edf6141f" - integrity sha512-lavACU4oDxioUy8lZOFZN0Vrr2qR+Dg2yEh/mkrPfOldcioavREXJou0elDyyXwq4pGLC5YQ+IISCtQ4Du0bdw== +"@chain-registry/types@^0.18.0", "@chain-registry/types@^0.18.1": + version "0.18.1" + resolved "https://registry.yarnpkg.com/@chain-registry/types/-/types-0.18.1.tgz#bd926ddf204f2ae986a79b42f0cfe85764e2977a" + integrity sha512-H/UnOyd7WdcWHa2FGKxy4MclDDFrbE2rFwaUh9oubNJOey7UBI4dNF10oZIWM/1by15LUgDz45fVbh6uA6W5Tg== dependencies: "@babel/runtime" "^7.21.0" @@ -143,17 +113,17 @@ "@noble/hashes" "^1.0.0" protobufjs "^6.8.8" -"@cosmjs/amino@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.30.1.tgz#7c18c14627361ba6c88e3495700ceea1f76baace" - integrity sha512-yNHnzmvAlkETDYIpeCTdVqgvrdt1qgkOXwuRVi8s27UKI5hfqyE9fJ/fuunXE6ZZPnKkjIecDznmuUOMrMvw4w== +"@cosmjs/amino@0.27.1": + version "0.27.1" + resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.27.1.tgz#0910256b5aecd794420bb5f7319d98fc63252fa1" + integrity sha512-w56ar/nK9+qlvWDpBPRmD0Blk2wfkkLqRi1COs1x7Ll1LF0AtkIBUjbRKplENLbNovK0T3h+w8bHiFm+GBGQOA== dependencies: - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" + "@cosmjs/crypto" "0.27.1" + "@cosmjs/encoding" "0.27.1" + "@cosmjs/math" "0.27.1" + "@cosmjs/utils" "0.27.1" -"@cosmjs/amino@^0.31.0-alpha.1", "@cosmjs/amino@^0.31.1", "@cosmjs/amino@^0.31.3": +"@cosmjs/amino@0.31.x", "@cosmjs/amino@^0.31.0", "@cosmjs/amino@^0.31.3": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.31.3.tgz#0f4aa6bd68331c71bd51b187fa64f00eb075db0a" integrity sha512-36emtUq895sPRX8PTSOnG+lhJDCVyIcE0Tr5ct59sUbgQiI14y43vj/4WAlJ/utSOxy+Zhj9wxcs4AZfu0BHsw== @@ -163,17 +133,27 @@ "@cosmjs/math" "^0.31.3" "@cosmjs/utils" "^0.31.3" -"@cosmjs/amino@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.32.1.tgz#412ea151ee064757d8c8746f8a8975dc73ee175f" - integrity sha512-5l2xQ2XuAhV/B3kTIMPBcVZ/OQ+9Yyddzw/lIVs4qE5e/oBI0PVNWXw1oyR0wgfGHrMUxgKjsoOOqE2IbXVyCw== +"@cosmjs/amino@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.32.2.tgz#ba3cf255e4e6b1ba67461f1ef7b0b8ad3f895da7" + integrity sha512-lcK5RCVm4OfdAooxKcF2+NwaDVVpghOq6o/A40c2mHXDUzUoRZ33VAHjVJ9Me6vOFxshrw/XEFn1f4KObntjYA== dependencies: - "@cosmjs/crypto" "^0.32.1" - "@cosmjs/encoding" "^0.32.1" - "@cosmjs/math" "^0.32.1" - "@cosmjs/utils" "^0.32.1" + "@cosmjs/crypto" "^0.32.2" + "@cosmjs/encoding" "^0.32.2" + "@cosmjs/math" "^0.32.2" + "@cosmjs/utils" "^0.32.2" -"@cosmjs/cosmwasm-stargate@^0.31.1": +"@cosmjs/amino@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.32.3.tgz#b81d4a2b8d61568431a1afcd871e1344a19d97ff" + integrity sha512-G4zXl+dJbqrz1sSJ56H/25l5NJEk/pAPIr8piAHgbXYw88OdAOlpA26PQvk2IbSN/rRgVbvlLTNgX2tzz1dyUA== + dependencies: + "@cosmjs/crypto" "^0.32.3" + "@cosmjs/encoding" "^0.32.3" + "@cosmjs/math" "^0.32.3" + "@cosmjs/utils" "^0.32.3" + +"@cosmjs/cosmwasm-stargate@0.31.x", "@cosmjs/cosmwasm-stargate@^0.31.1": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.31.3.tgz#13066822f111832d57c2c5acc9e697ed389713f8" integrity sha512-Uv9TmCn3650gdFeZm7SEfUZF3uX3lfJfFhXOk6I2ZLr/FrKximnlb+vwAfZaZnWYvlA7qrKtHIjeRNHvT23zcw== @@ -190,18 +170,53 @@ long "^4.0.0" pako "^2.0.2" -"@cosmjs/crypto@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.30.1.tgz#21e94d5ca8f8ded16eee1389d2639cb5c43c3eb5" - integrity sha512-rAljUlake3MSXs9xAm87mu34GfBLN0h/1uPPV6jEwClWjNkAMotzjC0ab9MARy5FFAvYHL3lWb57bhkbt2GtzQ== +"@cosmjs/cosmwasm-stargate@0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.32.2.tgz#32aca8b4c2043cd1bc91cf4d0225b268c166e421" + integrity sha512-OwJHzIx2CoJS6AULxOpNR6m+CI0GXxy8z9svHA1ZawzNM3ZGlL0GvHdhmF0WkpX4E7UdrYlJSLpKcgg5Fo6i7Q== + dependencies: + "@cosmjs/amino" "^0.32.2" + "@cosmjs/crypto" "^0.32.2" + "@cosmjs/encoding" "^0.32.2" + "@cosmjs/math" "^0.32.2" + "@cosmjs/proto-signing" "^0.32.2" + "@cosmjs/stargate" "^0.32.2" + "@cosmjs/tendermint-rpc" "^0.32.2" + "@cosmjs/utils" "^0.32.2" + cosmjs-types "^0.9.0" + pako "^2.0.2" + +"@cosmjs/cosmwasm-stargate@^0.32.2": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.32.3.tgz#26a110a6bb0c15fdeef647e3433bd9553a1acd5f" + integrity sha512-pqkt+QsLIPNMTRh9m+igJgIpzXXgn1BxmxfAb9zlC23kvsuzY/12un9M7iAdim1NwKXDFeYw46xC2YkprwQp+g== + dependencies: + "@cosmjs/amino" "^0.32.3" + "@cosmjs/crypto" "^0.32.3" + "@cosmjs/encoding" "^0.32.3" + "@cosmjs/math" "^0.32.3" + "@cosmjs/proto-signing" "^0.32.3" + "@cosmjs/stargate" "^0.32.3" + "@cosmjs/tendermint-rpc" "^0.32.3" + "@cosmjs/utils" "^0.32.3" + cosmjs-types "^0.9.0" + pako "^2.0.2" + +"@cosmjs/crypto@0.27.1": + version "0.27.1" + resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.27.1.tgz#271c853089a3baf3acd6cf0b2122fd49f8815743" + integrity sha512-vbcxwSt99tIYJg8Spp00wc3zx72qx+pY3ozGuBN8gAvySnagK9dQ/jHwtWQWdammmdD6oW+75WfIHZ+gNa+Ybg== dependencies: - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - "@noble/hashes" "^1" + "@cosmjs/encoding" "0.27.1" + "@cosmjs/math" "0.27.1" + "@cosmjs/utils" "0.27.1" + bip39 "^3.0.2" bn.js "^5.2.0" - elliptic "^6.5.4" + elliptic "^6.5.3" + js-sha3 "^0.8.0" libsodium-wrappers "^0.7.6" + ripemd160 "^2.0.2" + sha.js "^2.4.11" "@cosmjs/crypto@^0.31.3": version "0.31.3" @@ -216,29 +231,42 @@ elliptic "^6.5.4" libsodium-wrappers-sumo "^0.7.11" -"@cosmjs/crypto@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.32.1.tgz#81202a10cbd36394a390454d954d782482537a5b" - integrity sha512-AsKucEg5o8evU0wXF/lDwX+ZSwCKF4bbc57nFzraHywlp3sNu4dfPPURoMrT0r7kT7wQZAy4Pdnvmm9nnCCm/Q== +"@cosmjs/crypto@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.32.2.tgz#8ed255d3d1c1c4d916a1586f8cbc33eaab82f511" + integrity sha512-RuxrYKzhrPF9g6NmU7VEq++Hn1vZJjqqJpZ9Tmw9lOYOV8BUsv+j/0BE86kmWi7xVJ7EwxiuxYsKuM8IR18CIA== + dependencies: + "@cosmjs/encoding" "^0.32.2" + "@cosmjs/math" "^0.32.2" + "@cosmjs/utils" "^0.32.2" + "@noble/hashes" "^1" + bn.js "^5.2.0" + elliptic "^6.5.4" + libsodium-wrappers-sumo "^0.7.11" + +"@cosmjs/crypto@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.32.3.tgz#787f8e659709678722068ee1ddf379f65051a25e" + integrity sha512-niQOWJHUtlJm2GG4F00yGT7sGPKxfUwz+2qQ30uO/E3p58gOusTcH2qjiJNVxb8vScYJhFYFqpm/OA/mVqoUGQ== dependencies: - "@cosmjs/encoding" "^0.32.1" - "@cosmjs/math" "^0.32.1" - "@cosmjs/utils" "^0.32.1" + "@cosmjs/encoding" "^0.32.3" + "@cosmjs/math" "^0.32.3" + "@cosmjs/utils" "^0.32.3" "@noble/hashes" "^1" bn.js "^5.2.0" elliptic "^6.5.4" libsodium-wrappers-sumo "^0.7.11" -"@cosmjs/encoding@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.30.1.tgz#b5c4e0ef7ceb1f2753688eb96400ed70f35c6058" - integrity sha512-rXmrTbgqwihORwJ3xYhIgQFfMSrwLu1s43RIK9I8EBudPx3KmnmyAKzMOVsRDo9edLFNuZ9GIvysUCwQfq3WlQ== +"@cosmjs/encoding@0.27.1": + version "0.27.1" + resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.27.1.tgz#3cd5bc0af743485eb2578cdb08cfa84c86d610e1" + integrity sha512-rayLsA0ojHeniaRfWWcqSsrE/T1rl1gl0OXVNtXlPwLJifKBeLEefGbOUiAQaT0wgJ8VNGBazVtAZBpJidfDhw== dependencies: base64-js "^1.3.0" bech32 "^1.1.4" readonly-date "^1.0.0" -"@cosmjs/encoding@^0.31.0-alpha.1", "@cosmjs/encoding@^0.31.1", "@cosmjs/encoding@^0.31.3": +"@cosmjs/encoding@0.31.x", "@cosmjs/encoding@^0.31.3": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.31.3.tgz#2519d9c9ae48368424971f253775c4580b54c5aa" integrity sha512-6IRtG0fiVYwyP7n+8e54uTx2pLYijO48V3t9TLiROERm5aUAIzIlz6Wp0NYaI5he9nh1lcEGJ1lkquVKFw3sUg== @@ -247,22 +275,23 @@ bech32 "^1.1.4" readonly-date "^1.0.0" -"@cosmjs/encoding@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.32.1.tgz#1755c96e063bebef07a3f2d32971e90fb9ea4e3a" - integrity sha512-x60Lfds+Eq42rVV29NaoIAson3kBhATBI3zPp7X3GJTryBc5HFHQ6L/976tE1WB2DrvkfUdWS3ayCMVOY/qm1g== +"@cosmjs/encoding@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.32.2.tgz#8c5c64481a85cd570740c34dccce69d024a29805" + integrity sha512-WX7m1wLpA9V/zH0zRcz4EmgZdAv1F44g4dbXOgNj1eXZw1PIGR12p58OEkLN51Ha3S4DKRtCv5CkhK1KHEvQtg== dependencies: base64-js "^1.3.0" bech32 "^1.1.4" readonly-date "^1.0.0" -"@cosmjs/json-rpc@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.30.1.tgz#16f21305fc167598c8a23a45549b85106b2372bc" - integrity sha512-pitfC/2YN9t+kXZCbNuyrZ6M8abnCC2n62m+JtU9vQUfaEtVsgy+1Fk4TRQ175+pIWSdBMFi2wT8FWVEE4RhxQ== +"@cosmjs/encoding@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.32.3.tgz#e245ff511fe4a0df7ba427b5187aab69e3468e5b" + integrity sha512-p4KF7hhv8jBQX3MkB3Defuhz/W0l3PwWVYU2vkVuBJ13bJcXyhU9nJjiMkaIv+XP+W2QgRceqNNgFUC5chNR7w== dependencies: - "@cosmjs/stream" "^0.30.1" - xstream "^11.14.0" + base64-js "^1.3.0" + bech32 "^1.1.4" + readonly-date "^1.0.0" "@cosmjs/json-rpc@^0.31.3": version "0.31.3" @@ -272,49 +301,64 @@ "@cosmjs/stream" "^0.31.3" xstream "^11.14.0" -"@cosmjs/json-rpc@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.32.1.tgz#0f816943e36a8e8079587180ed099cacb361fd90" - integrity sha512-Hsj3Sg+m/JF8qfISp/G4TXQ0FAO01mzDKtNcgKufIHCrvJNDiE69xGyGgSm/qKwsXLBmzRTSxHWK0+yZef3LNQ== +"@cosmjs/json-rpc@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.32.2.tgz#f87fab0d6975ed1d1c7daafcf6f1f81e5e296912" + integrity sha512-lan2lOgmz4yVE/HR8eCOSiII/1OudIulk8836koyIDCsPEpt6eKBuctnAD168vABGArKccLAo7Mr2gy9nrKrOQ== dependencies: - "@cosmjs/stream" "^0.32.1" + "@cosmjs/stream" "^0.32.2" xstream "^11.14.0" -"@cosmjs/math@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.30.1.tgz#8b816ef4de5d3afa66cb9fdfb5df2357a7845b8a" - integrity sha512-yaoeI23pin9ZiPHIisa6qqLngfnBR/25tSaWpkTm8Cy10MX70UF5oN4+/t1heLaM6SSmRrhk3psRkV4+7mH51Q== +"@cosmjs/json-rpc@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.32.3.tgz#ccffdd7f722cecfab6daaa7463843b92f5d25355" + integrity sha512-JwFRWZa+Y95KrAG8CuEbPVOSnXO2uMSEBcaAB/FBU3Mo4jQnDoUjXvt3vwtFWxfAytrWCn1I4YDFaOAinnEG/Q== + dependencies: + "@cosmjs/stream" "^0.32.3" + xstream "^11.14.0" + +"@cosmjs/launchpad@^0.27.1": + version "0.27.1" + resolved "https://registry.yarnpkg.com/@cosmjs/launchpad/-/launchpad-0.27.1.tgz#b6f1995748be96560f5f01e84d3ff907477dda77" + integrity sha512-DcFwGD/z5PK8CzO2sojDxa+Be9EIEtRZb2YawgVnw2Ht/p5FlNv+OVo8qlishpBdalXEN7FvQ1dVeDFEe9TuJw== + dependencies: + "@cosmjs/amino" "0.27.1" + "@cosmjs/crypto" "0.27.1" + "@cosmjs/encoding" "0.27.1" + "@cosmjs/math" "0.27.1" + "@cosmjs/utils" "0.27.1" + axios "^0.21.2" + fast-deep-equal "^3.1.3" + +"@cosmjs/math@0.27.1": + version "0.27.1" + resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.27.1.tgz#be78857b008ffc6b1ed6fecaa1c4cd5bc38c07d7" + integrity sha512-cHWVjmfIjtRc7f80n7x+J5k8pe+vTVTQ0lA82tIxUgqUvgS6rogPP/TmGtTiZ4+NxWxd11DUISY6gVpr18/VNQ== dependencies: bn.js "^5.2.0" -"@cosmjs/math@^0.31.0-alpha.1", "@cosmjs/math@^0.31.1", "@cosmjs/math@^0.31.3": +"@cosmjs/math@0.31.x", "@cosmjs/math@^0.31.3": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.31.3.tgz#767f7263d12ba1b9ed2f01f68d857597839fd957" integrity sha512-kZ2C6glA5HDb9hLz1WrftAjqdTBb3fWQsRR+Us2HsjAYdeE6M3VdXMsYCP5M3yiihal1WDwAY2U7HmfJw7Uh4A== dependencies: bn.js "^5.2.0" -"@cosmjs/math@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.32.1.tgz#e748b1f8bb20a927f5fe8311615911ed63c7334e" - integrity sha512-sqJgDjPh49rxe06apzwKYLxAw4LLFKmEd4yQtHqH16BxVVUrvK5UH9TEBpUrRErdjqENowekecDCDBZspGXHNA== +"@cosmjs/math@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.32.2.tgz#4522312769197e132679e4960862bcec0eed4cb8" + integrity sha512-b8+ruAAY8aKtVKWSft2IvtCVCUH1LigIlf9ALIiY8n9jtM4kMASiaRbQ/27etnSAInV88IaezKK9rQZrtxTjcw== dependencies: bn.js "^5.2.0" -"@cosmjs/proto-signing@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.30.1.tgz#f0dda372488df9cd2677150b89b3e9c72b3cb713" - integrity sha512-tXh8pPYXV4aiJVhTKHGyeZekjj+K9s2KKojMB93Gcob2DxUjfKapFYBMJSgfKPuWUPEmyr8Q9km2hplI38ILgQ== - dependencies: - "@cosmjs/amino" "^0.30.1" - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - cosmjs-types "^0.7.1" - long "^4.0.0" +"@cosmjs/math@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.32.3.tgz#16e4256f4da507b9352327da12ae64056a2ba6c9" + integrity sha512-amumUtZs8hCCnV+lSBaJIiZkGabQm22QGg/IotYrhcmoOEOjt82n7hMNlNXRs7V6WLMidGrGYcswB5zcmp0Meg== + dependencies: + bn.js "^5.2.0" -"@cosmjs/proto-signing@^0.31.0-alpha.1", "@cosmjs/proto-signing@^0.31.1", "@cosmjs/proto-signing@^0.31.3": +"@cosmjs/proto-signing@0.31.x", "@cosmjs/proto-signing@^0.31.0", "@cosmjs/proto-signing@^0.31.3": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.31.3.tgz#20440b7b96fb2cd924256a10e656fd8d4481cdcd" integrity sha512-24+10/cGl6lLS4VCrGTCJeDRPQTn1K5JfknzXzDIHOx8THR31JxA7/HV5eWGHqWgAbudA7ccdSvEK08lEHHtLA== @@ -327,27 +371,29 @@ cosmjs-types "^0.8.0" long "^4.0.0" -"@cosmjs/proto-signing@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.32.1.tgz#39de3c1758b2e3ae862d77fe4cb80b1dd6bc229f" - integrity sha512-IHJMXQ8XnfzR5K1hWb8VV/jEfJof6BL2mgGIA7X4hSPegwoVfb9hnFKPEPgFjGCTTvGZ8SfnCdXxpsOjianVIA== +"@cosmjs/proto-signing@^0.32.1", "@cosmjs/proto-signing@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.32.2.tgz#26ed2675978ce24078981f4c15a06c5d6b808f44" + integrity sha512-UV4WwkE3W3G3s7wwU9rizNcUEz2g0W8jQZS5J6/3fiN0mRPwtPKQ6EinPN9ASqcAJ7/VQH4/9EPOw7d6XQGnqw== dependencies: - "@cosmjs/amino" "^0.32.1" - "@cosmjs/crypto" "^0.32.1" - "@cosmjs/encoding" "^0.32.1" - "@cosmjs/math" "^0.32.1" - "@cosmjs/utils" "^0.32.1" + "@cosmjs/amino" "^0.32.2" + "@cosmjs/crypto" "^0.32.2" + "@cosmjs/encoding" "^0.32.2" + "@cosmjs/math" "^0.32.2" + "@cosmjs/utils" "^0.32.2" cosmjs-types "^0.9.0" -"@cosmjs/socket@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.30.1.tgz#00b22f4b5e2ab01f4d82ccdb7b2e59536bfe5ce0" - integrity sha512-r6MpDL+9N+qOS/D5VaxnPaMJ3flwQ36G+vPvYJsXArj93BjgyFB7BwWwXCQDzZ+23cfChPUfhbINOenr8N2Kow== +"@cosmjs/proto-signing@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.32.3.tgz#91ae149b747d18666a6ccc924165b306431f7c0d" + integrity sha512-kSZ0ZUY0DwcRT0NcIn2HkadH4NKlwjfZgbLj1ABwh/4l0RgeT84QCscZCu63tJYq3K6auwqTiZSZERwlO4/nbg== dependencies: - "@cosmjs/stream" "^0.30.1" - isomorphic-ws "^4.0.1" - ws "^7" - xstream "^11.14.0" + "@cosmjs/amino" "^0.32.3" + "@cosmjs/crypto" "^0.32.3" + "@cosmjs/encoding" "^0.32.3" + "@cosmjs/math" "^0.32.3" + "@cosmjs/utils" "^0.32.3" + cosmjs-types "^0.9.0" "@cosmjs/socket@^0.31.3": version "0.31.3" @@ -359,53 +405,27 @@ ws "^7" xstream "^11.14.0" -"@cosmjs/socket@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.32.1.tgz#a8d45cde9944646f2da930d55e4269bc411b694e" - integrity sha512-thPCLCmnCuZvrsDW4YmsADI/MliOXWuMnflbzX+3OhoTuEav2I4/1aOXY0jdy0bbqL0l1opx+JfmwdWptMgKzg== +"@cosmjs/socket@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.32.2.tgz#a66be3863d03bf2d8df0433af476df010ff10e8c" + integrity sha512-Qc8jaw4uSBJm09UwPgkqe3g9TBFx4ZR9HkXpwT6Z9I+6kbLerXPR0Gy3NSJFSUgxIfTpO8O1yqoWAyf0Ay17Mw== dependencies: - "@cosmjs/stream" "^0.32.1" + "@cosmjs/stream" "^0.32.2" isomorphic-ws "^4.0.1" ws "^7" xstream "^11.14.0" -"@cosmjs/stargate@0.31.0-alpha.1": - version "0.31.0-alpha.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.31.0-alpha.1.tgz#85b9d41cd5e73c3b8db73115aa1c9cd6eb5914fd" - integrity sha512-kCTUT3niB2hvcHjhlxpM8cNw1KOVmgZROdJUQaO8Ts4j22OyRZRFdwRPrOIuAKpqhVW2I1vI2HQL9Bg7pk9Glw== - dependencies: - "@confio/ics23" "^0.6.8" - "@cosmjs/amino" "^0.31.0-alpha.1" - "@cosmjs/encoding" "^0.31.0-alpha.1" - "@cosmjs/math" "^0.31.0-alpha.1" - "@cosmjs/proto-signing" "^0.31.0-alpha.1" - "@cosmjs/stream" "^0.31.0-alpha.1" - "@cosmjs/tendermint-rpc" "^0.31.0-alpha.1" - "@cosmjs/utils" "^0.31.0-alpha.1" - cosmjs-types "^0.8.0" - long "^4.0.0" - protobufjs "~6.11.3" - xstream "^11.14.0" - -"@cosmjs/stargate@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.30.1.tgz#e1b22e1226cffc6e93914a410755f1f61057ba04" - integrity sha512-RdbYKZCGOH8gWebO7r6WvNnQMxHrNXInY/gPHPzMjbQF6UatA6fNM2G2tdgS5j5u7FTqlCI10stNXrknaNdzog== +"@cosmjs/socket@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.32.3.tgz#fa5c36bf58e87c0ad865d6318ecb0f8d9c89a28a" + integrity sha512-F2WwNmaUPdZ4SsH6Uyreq3wQk7jpaEkb3wfOP951f5Jt6HCW/PxbxhKzHkAAf6+Sqks6SPhkbWoE8XaZpjL2KA== dependencies: - "@confio/ics23" "^0.6.8" - "@cosmjs/amino" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/proto-signing" "^0.30.1" - "@cosmjs/stream" "^0.30.1" - "@cosmjs/tendermint-rpc" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - cosmjs-types "^0.7.1" - long "^4.0.0" - protobufjs "~6.11.3" + "@cosmjs/stream" "^0.32.3" + isomorphic-ws "^4.0.1" + ws "^7" xstream "^11.14.0" -"@cosmjs/stargate@^0.31.1", "@cosmjs/stargate@^0.31.3": +"@cosmjs/stargate@0.31.x", "@cosmjs/stargate@^0.31.1", "@cosmjs/stargate@^0.31.3": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.31.3.tgz#a2b38e398097a00f897dbd8f02d4d347d8fed818" integrity sha512-53NxnzmB9FfXpG4KjOUAYAvWLYKdEmZKsutcat/u2BrDXNZ7BN8jim/ENcpwXfs9/Og0K24lEIdvA4gsq3JDQw== @@ -423,60 +443,60 @@ protobufjs "~6.11.3" xstream "^11.14.0" -"@cosmjs/stargate@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.32.1.tgz#c4e3a4b6847ef45c26275e64f4668274cae01f9c" - integrity sha512-S0E1qKQ2CMJU79G8bQTquTyrbU03gFsvCkbo3RvK8v2OltVCByjFNh+0nGN5do+uDOzwwmDvnNLhR+SaIyNQoQ== +"@cosmjs/stargate@^0.32.1", "@cosmjs/stargate@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.32.2.tgz#73718c5c6a3ae138682ee9987333d911eca22a13" + integrity sha512-AsJa29fT7Jd4xt9Ai+HMqhyj7UQu7fyYKdXj/8+/9PD74xe6lZSYhQPcitUmMLJ1ckKPgXSk5Dd2LbsQT0IhZg== dependencies: "@confio/ics23" "^0.6.8" - "@cosmjs/amino" "^0.32.1" - "@cosmjs/encoding" "^0.32.1" - "@cosmjs/math" "^0.32.1" - "@cosmjs/proto-signing" "^0.32.1" - "@cosmjs/stream" "^0.32.1" - "@cosmjs/tendermint-rpc" "^0.32.1" - "@cosmjs/utils" "^0.32.1" + "@cosmjs/amino" "^0.32.2" + "@cosmjs/encoding" "^0.32.2" + "@cosmjs/math" "^0.32.2" + "@cosmjs/proto-signing" "^0.32.2" + "@cosmjs/stream" "^0.32.2" + "@cosmjs/tendermint-rpc" "^0.32.2" + "@cosmjs/utils" "^0.32.2" cosmjs-types "^0.9.0" xstream "^11.14.0" -"@cosmjs/stream@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.30.1.tgz#ba038a2aaf41343696b1e6e759d8e03a9516ec1a" - integrity sha512-Fg0pWz1zXQdoxQZpdHRMGvUH5RqS6tPv+j9Eh7Q953UjMlrwZVo0YFLC8OTf/HKVf10E4i0u6aM8D69Q6cNkgQ== +"@cosmjs/stargate@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.32.3.tgz#5a92b222ada960ebecea72cc9f366370763f4b66" + integrity sha512-OQWzO9YWKerUinPIxrO1MARbe84XkeXJAW0lyMIjXIEikajuXZ+PwftiKA5yA+8OyditVmHVLtPud6Pjna2s5w== dependencies: + "@confio/ics23" "^0.6.8" + "@cosmjs/amino" "^0.32.3" + "@cosmjs/encoding" "^0.32.3" + "@cosmjs/math" "^0.32.3" + "@cosmjs/proto-signing" "^0.32.3" + "@cosmjs/stream" "^0.32.3" + "@cosmjs/tendermint-rpc" "^0.32.3" + "@cosmjs/utils" "^0.32.3" + cosmjs-types "^0.9.0" xstream "^11.14.0" -"@cosmjs/stream@^0.31.0-alpha.1", "@cosmjs/stream@^0.31.3": +"@cosmjs/stream@^0.31.3": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.31.3.tgz#53428fd62487ec08fc3886a50a3feeb8b2af2e66" integrity sha512-8keYyI7X0RjsLyVcZuBeNjSv5FA4IHwbFKx7H60NHFXszN8/MvXL6aZbNIvxtcIHHsW7K9QSQos26eoEWlAd+w== dependencies: xstream "^11.14.0" -"@cosmjs/stream@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.32.1.tgz#bab72498a0a146ba172fb155fb7c38fb9bc16c6f" - integrity sha512-6RwHaGxWbIG0y++aCYP/doa4ex/Up8Q8G+ehwDzAq3aKl3zbDe9L0FmycclnMuwPm/baPIkEZ6+IVmJoNLX79Q== +"@cosmjs/stream@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.32.2.tgz#b1e8f977d25313d659f1aa89ad21614b5391cd93" + integrity sha512-gpCufLfHAD8Zp1ZKge7AHbDf4RA0TZp66wZY6JaQR5bSiEF2Drjtp4mwXZPGejtaUMnaAgff3LrUzPJfKYdQwg== dependencies: xstream "^11.14.0" -"@cosmjs/tendermint-rpc@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.30.1.tgz#c16378892ba1ac63f72803fdf7567eab9d4f0aa0" - integrity sha512-Z3nCwhXSbPZJ++v85zHObeUggrEHVfm1u18ZRwXxFE9ZMl5mXTybnwYhczuYOl7KRskgwlB+rID0WYACxj4wdQ== - dependencies: - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/json-rpc" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/socket" "^0.30.1" - "@cosmjs/stream" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - axios "^0.21.2" - readonly-date "^1.0.0" +"@cosmjs/stream@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.32.3.tgz#7522579aaf18025d322c2f33d6fb7573220395d6" + integrity sha512-J2zVWDojkynYifAUcRmVczzmp6STEpyiAARq0rSsviqjreGIfspfuws/8rmkPa6qQBZvpQOBQCm2HyZZwYplIw== + dependencies: xstream "^11.14.0" -"@cosmjs/tendermint-rpc@^0.31.0-alpha.1", "@cosmjs/tendermint-rpc@^0.31.1", "@cosmjs/tendermint-rpc@^0.31.3": +"@cosmjs/tendermint-rpc@0.31.x", "@cosmjs/tendermint-rpc@^0.31.3": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.31.3.tgz#d1a2bc5b3c98743631c9b55888589d352403c9b3" integrity sha512-s3TiWkPCW4QceTQjpYqn4xttUJH36mTPqplMl+qyocdqk5+X5mergzExU/pHZRWQ4pbby8bnR7kMvG4OC1aZ8g== @@ -492,41 +512,100 @@ readonly-date "^1.0.0" xstream "^11.14.0" -"@cosmjs/tendermint-rpc@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.32.1.tgz#f7f8929619648fb0520047c6d930dc65588345d2" - integrity sha512-4uGSxB2JejWhwBUgxca4GqcK/BGnCFMIP7ptwEledrC3AY/shPeIYcPXWEBwO7sfwCta8DhAOCLrc9zhVC+VAQ== - dependencies: - "@cosmjs/crypto" "^0.32.1" - "@cosmjs/encoding" "^0.32.1" - "@cosmjs/json-rpc" "^0.32.1" - "@cosmjs/math" "^0.32.1" - "@cosmjs/socket" "^0.32.1" - "@cosmjs/stream" "^0.32.1" - "@cosmjs/utils" "^0.32.1" +"@cosmjs/tendermint-rpc@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.32.2.tgz#c5607b8d472e5bf9fd58d5453db7194f500ccc62" + integrity sha512-DXyJHDmcAfCix4H/7/dKR0UMdshP01KxJOXHdHxBCbLIpck94BsWD3B2ZTXwfA6sv98so9wOzhp7qGQa5malxg== + dependencies: + "@cosmjs/crypto" "^0.32.2" + "@cosmjs/encoding" "^0.32.2" + "@cosmjs/json-rpc" "^0.32.2" + "@cosmjs/math" "^0.32.2" + "@cosmjs/socket" "^0.32.2" + "@cosmjs/stream" "^0.32.2" + "@cosmjs/utils" "^0.32.2" axios "^1.6.0" readonly-date "^1.0.0" xstream "^11.14.0" -"@cosmjs/utils@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.30.1.tgz#6d92582341be3c2ec8d82090253cfa4b7f959edb" - integrity sha512-KvvX58MGMWh7xA+N+deCfunkA/ZNDvFLw4YbOmX3f/XBIkqrVY7qlotfy2aNb1kgp6h4B6Yc8YawJPDTfvWX7g== +"@cosmjs/tendermint-rpc@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.32.3.tgz#f0406b9f0233e588fb924dca8c614972f9038aff" + integrity sha512-xeprW+VR9xKGstqZg0H/KBZoUp8/FfFyS9ljIUTLM/UINjP2MhiwncANPS2KScfJVepGufUKk0/phHUeIBSEkw== + dependencies: + "@cosmjs/crypto" "^0.32.3" + "@cosmjs/encoding" "^0.32.3" + "@cosmjs/json-rpc" "^0.32.3" + "@cosmjs/math" "^0.32.3" + "@cosmjs/socket" "^0.32.3" + "@cosmjs/stream" "^0.32.3" + "@cosmjs/utils" "^0.32.3" + axios "^1.6.0" + readonly-date "^1.0.0" + xstream "^11.14.0" + +"@cosmjs/utils@0.27.1": + version "0.27.1" + resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.27.1.tgz#1c8efde17256346ef142a3bd15158ee4055470e2" + integrity sha512-VG7QPDiMUzVPxRdJahDV8PXxVdnuAHiIuG56hldV4yPnOz/si/DLNd7VAUUA5923b6jS1Hhev0Hr6AhEkcxBMg== -"@cosmjs/utils@^0.31.0-alpha.1", "@cosmjs/utils@^0.31.3": +"@cosmjs/utils@^0.31.3": version "0.31.3" resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.31.3.tgz#f97bbfda35ad69e80cd5c7fe0a270cbda16db1ed" integrity sha512-VBhAgzrrYdIe0O5IbKRqwszbQa7ZyQLx9nEQuHQ3HUplQW7P44COG/ye2n6AzCudtqxmwdX7nyX8ta1J07GoqA== -"@cosmjs/utils@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.32.1.tgz#0f7f7cbbe38c4a7fd852e698bad4d811fba5f80a" - integrity sha512-PV9pa0cVPFCNgfQKEOc6RcNFHr5wMQLcDqWoo/ekIoj1AfzAaqnojdnL80u1C9Qf+vOfRGIXubqiU7Tl7QZuig== +"@cosmjs/utils@^0.32.2": + version "0.32.2" + resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.32.2.tgz#324304aa85bfa6f10561cc17781d824d02130897" + integrity sha512-Gg5t+eR7vPJMAmhkFt6CZrzPd0EKpAslWwk5rFVYZpJsM8JG5KT9XQ99hgNM3Ov6ScNoIWbXkpX27F6A9cXR4Q== + +"@cosmjs/utils@^0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.32.3.tgz#5dcaee6dd7cc846cdc073e9a7a7f63242f5f7e31" + integrity sha512-WCZK4yksj2hBDz4w7xFZQTRZQ/RJhBX26uFHmmQFIcNUUVAihrLO+RerqJgk0dZqC42wstM9pEUQGtPmLcIYvg== + +"@date-io/core@^2.15.0", "@date-io/core@^2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.17.0.tgz#360a4d0641f069776ed22e457876e8a8a58c205e" + integrity sha512-+EQE8xZhRM/hsY0CDTVyayMDDY5ihc4MqXCrPxooKw19yAzUIC6uUqsZeaOFNL9YKTNxYKrJP5DFgE8o5xRCOw== + +"@date-io/date-fns@^2.15.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.17.0.tgz#1d9d0a02e0137524331819c9576a4e8e19a6142b" + integrity sha512-L0hWZ/mTpy3Gx/xXJ5tq5CzHo0L7ry6KEO9/w/JWiFWFLZgiNVo3ex92gOl3zmzjHqY/3Ev+5sehAr8UnGLEng== + dependencies: + "@date-io/core" "^2.17.0" + +"@date-io/dayjs@^2.15.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.17.0.tgz#ec3e2384136c028971ca2f78800a6877b9fdbe62" + integrity sha512-Iq1wjY5XzBh0lheFA0it6Dsyv94e8mTiNR8vuTai+KopxDkreL3YjwTmZHxkgB7/vd0RMIACStzVgWvPATnDCA== + dependencies: + "@date-io/core" "^2.17.0" + +"@date-io/luxon@^2.15.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.17.0.tgz#76e1f001aaa38fe7f0049f010fe356db1bb517d2" + integrity sha512-l712Vdm/uTddD2XWt9TlQloZUiTiRQtY5TCOG45MQ/8u0tu8M17BD6QYHar/3OrnkGybALAMPzCy1r5D7+0HBg== + dependencies: + "@date-io/core" "^2.17.0" -"@emnapi/runtime@^0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-0.44.0.tgz#1ef702f846cfcd559d28eb7673919087ba5b63e3" - integrity sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw== +"@date-io/moment@^2.15.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.17.0.tgz#04d2487d9d15d468b2e7903b87268fa1c89b56cb" + integrity sha512-e4nb4CDZU4k0WRVhz1Wvl7d+hFsedObSauDHKtZwU9kt7gdYEAzKgnrSCTHsEaXrDumdrkCYTeZ0Tmyk7uV4tw== + dependencies: + "@date-io/core" "^2.17.0" + +"@discoveryjs/json-ext@0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@emnapi/runtime@^0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-0.45.0.tgz#e754de04c683263f34fd0c7f32adfe718bbe4ddd" + integrity sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w== dependencies: tslib "^2.4.0" @@ -576,23 +655,23 @@ integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== "@emotion/react@^11.11.1": - version "11.11.1" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.1.tgz#b2c36afac95b184f73b08da8c214fdf861fa4157" - integrity sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA== + version "11.11.3" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.3.tgz#96b855dc40a2a55f52a72f518a41db4f69c31a25" + integrity sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA== dependencies: "@babel/runtime" "^7.18.3" "@emotion/babel-plugin" "^11.11.0" "@emotion/cache" "^11.11.0" - "@emotion/serialize" "^1.1.2" + "@emotion/serialize" "^1.1.3" "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" "@emotion/utils" "^1.2.1" "@emotion/weak-memoize" "^0.3.1" hoist-non-react-statics "^3.3.1" -"@emotion/serialize@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.2.tgz#017a6e4c9b8a803bd576ff3d52a0ea6fa5a62b51" - integrity sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA== +"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.3.tgz#84b77bfcfe3b7bb47d326602f640ccfcacd5ffb0" + integrity sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA== dependencies: "@emotion/hash" "^0.9.1" "@emotion/memoize" "^0.8.1" @@ -670,9 +749,9 @@ integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== "@eslint/eslintrc@^2.1.2": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.3.tgz#797470a75fe0fbd5a53350ee715e85e87baff22d" - integrity sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA== + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -858,7 +937,7 @@ resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== -"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0", "@ethersproject/networks@^5.7.1": +"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": version "5.7.1" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== @@ -880,7 +959,7 @@ dependencies: "@ethersproject/logger" "^5.7.0" -"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.2": +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.0.10": version "5.7.2" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== @@ -1031,32 +1110,32 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@floating-ui/core@^1.4.2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c" - integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg== +"@floating-ui/core@^1.5.3": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.3.tgz#b6aa0827708d70971c8679a16cf680a515b8a52a" + integrity sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q== dependencies: - "@floating-ui/utils" "^0.1.3" + "@floating-ui/utils" "^0.2.0" -"@floating-ui/dom@^1.5.1": - version "1.5.3" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa" - integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA== +"@floating-ui/dom@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.4.tgz#28df1e1cb373884224a463235c218dcbd81a16bb" + integrity sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ== dependencies: - "@floating-ui/core" "^1.4.2" - "@floating-ui/utils" "^0.1.3" + "@floating-ui/core" "^1.5.3" + "@floating-ui/utils" "^0.2.0" -"@floating-ui/react-dom@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.4.tgz#b076fafbdfeb881e1d86ae748b7ff95150e9f3ec" - integrity sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ== +"@floating-ui/react-dom@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.6.tgz#5ffcf40b6550817a973b54cdd443374f51ca7a5c" + integrity sha512-IB8aCRFxr8nFkdYZgH+Otd9EVQPJoynxeFRGTB8voPoZMRWo8XjYuCRgpI1btvuKY69XMiLnW+ym7zoBHM90Rw== dependencies: - "@floating-ui/dom" "^1.5.1" + "@floating-ui/dom" "^1.5.4" -"@floating-ui/utils@^0.1.3": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" - integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== +"@floating-ui/utils@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== "@graphql-typed-document-node/core@^3.1.1": version "3.2.0" @@ -1064,12 +1143,12 @@ integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== "@humanwhocodes/config-array@^0.11.11": - version "0.11.13" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" - integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^2.0.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" "@humanwhocodes/module-importer@^1.0.1": @@ -1077,128 +1156,128 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" - integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== -"@img/sharp-darwin-arm64@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.1.tgz#9d3cb0e4899b10b003608a018877b45b6db02861" - integrity sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw== +"@img/sharp-darwin-arm64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz#0a52a82c2169112794dac2c71bfba9e90f7c5bd1" + integrity sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w== optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.0.0" + "@img/sharp-libvips-darwin-arm64" "1.0.1" -"@img/sharp-darwin-x64@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.1.tgz#3ad6b275dba0ed9995ce4403fc2c59b0475a0162" - integrity sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw== +"@img/sharp-darwin-x64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz#982e26bb9d38a81f75915c4032539aed621d1c21" + integrity sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg== optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.0.0" + "@img/sharp-libvips-darwin-x64" "1.0.1" -"@img/sharp-libvips-darwin-arm64@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz#8baf27b01dabba524e885c30287e1916ab978de7" - integrity sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw== +"@img/sharp-libvips-darwin-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz#81e83ffc2c497b3100e2f253766490f8fad479cd" + integrity sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw== -"@img/sharp-libvips-darwin-x64@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz#509113f649d3ea0e99b76d41437fc0049d8ba2f9" - integrity sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA== +"@img/sharp-libvips-darwin-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz#fc1fcd9d78a178819eefe2c1a1662067a83ab1d6" + integrity sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog== -"@img/sharp-libvips-linux-arm64@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz#9e131cc95f9f8aa36da9addee89efead21ad9993" - integrity sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA== +"@img/sharp-libvips-linux-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz#26eb8c556a9b0db95f343fc444abc3effb67ebcf" + integrity sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA== -"@img/sharp-libvips-linux-arm@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz#a529f68a28ab1d219907071c41ace029121d1c4f" - integrity sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw== +"@img/sharp-libvips-linux-arm@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz#2a377b959ff7dd6528deee486c25461296a4fa8b" + integrity sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ== -"@img/sharp-libvips-linux-s390x@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz#c26aaa9ee58b47ff901bb5f93f29c4ab3f03caf7" - integrity sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw== +"@img/sharp-libvips-linux-s390x@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz#af28ac9ba929204467ecdf843330d791e9421e10" + integrity sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ== -"@img/sharp-libvips-linux-x64@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz#7a04795fbf2668b9dd7c74bf8326cb779131d809" - integrity sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q== +"@img/sharp-libvips-linux-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz#4273d182aa51912e655e1214ea47983d7c1f7f8d" + integrity sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw== -"@img/sharp-libvips-linuxmusl-arm64@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz#600e7322faa5ce124d3ae2cbf63808ea47678591" - integrity sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ== +"@img/sharp-libvips-linuxmusl-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz#d150c92151cea2e8d120ad168b9c358d09c77ce8" + integrity sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg== -"@img/sharp-libvips-linuxmusl-x64@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz#af33a04e75f192c5396c6a41df8b7b7bf15a8006" - integrity sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg== +"@img/sharp-libvips-linuxmusl-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz#e297c1a4252c670d93b0f9e51fca40a7a5b6acfd" + integrity sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw== -"@img/sharp-linux-arm64@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.1.tgz#3f419eb6f9ace924c511903a4d9034fb847de06b" - integrity sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw== +"@img/sharp-linux-arm64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz#af3409f801a9bee1d11d0c7e971dcd6180f80022" + integrity sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew== optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.0.0" + "@img/sharp-libvips-linux-arm64" "1.0.1" -"@img/sharp-linux-arm@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.1.tgz#87e157716c55fa274dd652e71bf9a5b7c260f8ab" - integrity sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA== +"@img/sharp-linux-arm@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz#181f7466e6ac074042a38bfb679eb82505e17083" + integrity sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA== optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.0.0" + "@img/sharp-libvips-linux-arm" "1.0.1" -"@img/sharp-linux-s390x@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.1.tgz#c554567bb211cb1e916562752e70ef65df0cdcb7" - integrity sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A== +"@img/sharp-linux-s390x@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz#9c171f49211f96fba84410b3e237b301286fa00f" + integrity sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA== optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.0.0" + "@img/sharp-libvips-linux-s390x" "1.0.1" -"@img/sharp-linux-x64@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz#c8623c743e2a68b460b63bd2e225b1b5a485d144" - integrity sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg== +"@img/sharp-linux-x64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz#b956dfc092adc58c2bf0fae2077e6f01a8b2d5d7" + integrity sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A== optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.0.0" + "@img/sharp-libvips-linux-x64" "1.0.1" -"@img/sharp-linuxmusl-arm64@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.1.tgz#1b17f9950246108cb22cf98f1822fbe4a5b0dc9e" - integrity sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA== +"@img/sharp-linuxmusl-arm64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz#10e0ec5a79d1234c6a71df44c9f3b0bef0bc0f15" + integrity sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA== optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.0.0" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" -"@img/sharp-linuxmusl-x64@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz#69e2ab197c6d6e7a09748e0f2d03244d2c2afed7" - integrity sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ== +"@img/sharp-linuxmusl-x64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz#29e0030c24aa27c38201b1fc84e3d172899fcbe0" + integrity sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A== optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.0.0" + "@img/sharp-libvips-linuxmusl-x64" "1.0.1" -"@img/sharp-wasm32@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.1.tgz#aa6f33a8535e6bd4a66c59aeb569499db9d30043" - integrity sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA== +"@img/sharp-wasm32@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz#38d7c740a22de83a60ad1e6bcfce17462b0d4230" + integrity sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ== dependencies: - "@emnapi/runtime" "^0.44.0" + "@emnapi/runtime" "^0.45.0" -"@img/sharp-win32-ia32@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.1.tgz#8df522d79b3c08f59e65fbe773849daa353c4f12" - integrity sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ== +"@img/sharp-win32-ia32@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz#09456314e223f68e5417c283b45c399635c16202" + integrity sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g== -"@img/sharp-win32-x64@0.33.1": - version "0.33.1" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.1.tgz#8b56e97dc9d987b070d7530a357161d0f057c5af" - integrity sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg== +"@img/sharp-win32-x64@0.33.2": + version "0.33.2" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz#148e96dfd6e68747da41a311b9ee4559bb1b1471" + integrity sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg== -"@injectivelabs/core-proto-ts@^0.0.18": - version "0.0.18" - resolved "https://registry.yarnpkg.com/@injectivelabs/core-proto-ts/-/core-proto-ts-0.0.18.tgz#aa60ffde2b52cbbf80a18ba77833ecad605d8fd6" - integrity sha512-WSZS7SQ+I/m8jdc7fhzkMTUhA7i5nVTeKbN6QGqKmOwQ/F+PqM75vDHD9Y9NbLPx9P+m7hyUzSHz4zmajth5jw== +"@injectivelabs/core-proto-ts@0.0.x", "@injectivelabs/core-proto-ts@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@injectivelabs/core-proto-ts/-/core-proto-ts-0.0.21.tgz#b52d4bee7556ce57c7c7e2c1ec7f6b920b4c2ffd" + integrity sha512-RBxSkRBCty60R/l55/D1jsSW0Aof5dyGFhCFdN3A010KjMv/SzZGGr+6DZPY/hflyFeaJzDv/VTopCymKNRBvQ== dependencies: "@injectivelabs/grpc-web" "^0.0.1" google-protobuf "^3.14.0" @@ -1215,13 +1294,13 @@ protobufjs "^7.0.0" rxjs "^7.4.0" -"@injectivelabs/exceptions@^1.14.4": - version "1.14.4" - resolved "https://registry.yarnpkg.com/@injectivelabs/exceptions/-/exceptions-1.14.4.tgz#cb882530e8618d627cbc8fbb4c21d521bc73d00a" - integrity sha512-mJVzDsw+anL85NzXl0l1Ortk7MEgHdD5EQwFp/XaI1U3cupsHKUfHpAuXjggD+XQVFeWoYOFzk4CJPCBgz6u+w== +"@injectivelabs/exceptions@^1.14.6": + version "1.14.6" + resolved "https://registry.yarnpkg.com/@injectivelabs/exceptions/-/exceptions-1.14.6.tgz#86f93815eb6bc60902c43072b8e212146d78614e" + integrity sha512-A+URJwygeDjFPhulGMNVw70z738NtpIiCr0W8q4Kr4Ggg30i+AaVAjViYLm56pSMXXpomu9CYJ/sY6ijQn48IQ== dependencies: "@injectivelabs/grpc-web" "^0.0.1" - "@injectivelabs/ts-types" "^1.14.4" + "@injectivelabs/ts-types" "^1.14.6" http-status-codes "^2.2.0" link-module-alias "^1.2.0" shx "^0.3.2" @@ -1243,67 +1322,67 @@ dependencies: browser-headers "^0.4.1" -"@injectivelabs/indexer-proto-ts@1.11.22": - version "1.11.22" - resolved "https://registry.yarnpkg.com/@injectivelabs/indexer-proto-ts/-/indexer-proto-ts-1.11.22.tgz#e00f625d47e972ab2adb871466ff072b28560a36" - integrity sha512-Mx90Q6F2lW03FlqXqw6MKCtKl3Rxuhla2CBlhhXe6XrmZWt+LpkiYinlZCyQDnmwjuqzCqurlMx4/sEjuy5WhA== +"@injectivelabs/indexer-proto-ts@1.11.36": + version "1.11.36" + resolved "https://registry.yarnpkg.com/@injectivelabs/indexer-proto-ts/-/indexer-proto-ts-1.11.36.tgz#4a1476e52a5003a077b30a22679272eb9dd18d7a" + integrity sha512-s7E3Y28JrkylDwRVfF/AvcPy/zPgz52W+XbQ0FOcsqPof78xp8FvnM3ubVZi0Dad39LgDB5eeiMFPmeuLp8Uew== dependencies: "@injectivelabs/grpc-web" "^0.0.1" google-protobuf "^3.14.0" protobufjs "^7.0.0" rxjs "^7.4.0" -"@injectivelabs/mito-proto-ts@1.0.52": - version "1.0.52" - resolved "https://registry.yarnpkg.com/@injectivelabs/mito-proto-ts/-/mito-proto-ts-1.0.52.tgz#2073d52a087795e0365a40c0a11638543c3cfd53" - integrity sha512-esqrLXy9GheL5DAGTSxhUYOJZXE8g/3NnYVqz/x9MxZHQwYhj3LoV2G8CWB0Ly2i7VEOc0osesgJk08hrgLa+w== +"@injectivelabs/mito-proto-ts@1.0.62": + version "1.0.62" + resolved "https://registry.yarnpkg.com/@injectivelabs/mito-proto-ts/-/mito-proto-ts-1.0.62.tgz#45fc0746af7d1b283625816caeb9fff9887e050f" + integrity sha512-WtoO80Y597nZiAuE4H+L208I0i3ByWytR+HqABdCaA26uJ7F1LhXw8YXxh3pP9z0LAeW31T+N7bwtOMlVR4riA== dependencies: "@injectivelabs/grpc-web" "^0.0.1" google-protobuf "^3.14.0" protobufjs "^7.0.0" rxjs "^7.4.0" -"@injectivelabs/networks@^1.14.4": - version "1.14.4" - resolved "https://registry.yarnpkg.com/@injectivelabs/networks/-/networks-1.14.4.tgz#1603a2050c82ee4be5984d9a9978214b03d87fa8" - integrity sha512-2DbbaiI/v5PY+X90zhFMstVcVokpxEytMWZZvRKls6YqSBERhE3lxUF696lk0TEecfZ37CikuxZ7E9hx+eXg2A== +"@injectivelabs/networks@^1.14.6": + version "1.14.6" + resolved "https://registry.yarnpkg.com/@injectivelabs/networks/-/networks-1.14.6.tgz#29be5e81e60d725a0eef2db2c9b6ba0b4588141b" + integrity sha512-O1IkPFJl8ThNL6N+k/9OimrgCYsSWQ8A1FtVMXSQge+0QRZsDKSpRmQRwE601otXXauO31nOUct5AaiWPffXVQ== dependencies: - "@injectivelabs/exceptions" "^1.14.4" - "@injectivelabs/ts-types" "^1.14.4" - "@injectivelabs/utils" "^1.14.4" + "@injectivelabs/exceptions" "^1.14.6" + "@injectivelabs/ts-types" "^1.14.6" + "@injectivelabs/utils" "^1.14.6" link-module-alias "^1.2.0" shx "^0.3.2" -"@injectivelabs/sdk-ts@^1.12.1": - version "1.14.4" - resolved "https://registry.yarnpkg.com/@injectivelabs/sdk-ts/-/sdk-ts-1.14.4.tgz#0ec1669c707aadc07f272b1622c5f75baaed20de" - integrity sha512-Iw4y2YRxZ//p8lumqnSTUpXdoQGpt91wofm8R+eM65KOXX/T0p+6rpPYITWPxjT6E+koH4HXh9InnzzN4UzJQw== +"@injectivelabs/sdk-ts@1.x": + version "1.14.7" + resolved "https://registry.yarnpkg.com/@injectivelabs/sdk-ts/-/sdk-ts-1.14.7.tgz#16e84cecb14fe796314f8f616567511e1030f043" + integrity sha512-Qm8y8jKCMyNfYZGZVI+p0SIGJPtP5M9/DPFyPK+JSR2OOU0J4MX2yS/tQB5ViC/3Bt7yQhw/l3Rop93e7pTZEg== dependencies: "@apollo/client" "^3.5.8" - "@cosmjs/amino" "^0.30.1" - "@cosmjs/proto-signing" "^0.30.1" - "@cosmjs/stargate" "^0.30.1" + "@cosmjs/amino" "^0.32.2" + "@cosmjs/proto-signing" "^0.32.2" + "@cosmjs/stargate" "^0.32.2" "@ensdomains/ens-validation" "^0.1.0" "@ensdomains/eth-ens-namehash" "^2.0.15" "@ethersproject/bytes" "^5.7.0" - "@injectivelabs/core-proto-ts" "^0.0.18" + "@injectivelabs/core-proto-ts" "^0.0.21" "@injectivelabs/dmm-proto-ts" "1.0.19" - "@injectivelabs/exceptions" "^1.14.4" + "@injectivelabs/exceptions" "^1.14.6" "@injectivelabs/grpc-web" "^0.0.1" "@injectivelabs/grpc-web-node-http-transport" "^0.0.2" "@injectivelabs/grpc-web-react-native-transport" "^0.0.2" - "@injectivelabs/indexer-proto-ts" "1.11.22" - "@injectivelabs/mito-proto-ts" "1.0.52" - "@injectivelabs/networks" "^1.14.4" + "@injectivelabs/indexer-proto-ts" "1.11.36" + "@injectivelabs/mito-proto-ts" "1.0.62" + "@injectivelabs/networks" "^1.14.6" "@injectivelabs/test-utils" "^1.14.3" - "@injectivelabs/token-metadata" "^1.14.4" - "@injectivelabs/ts-types" "^1.14.4" - "@injectivelabs/utils" "^1.14.4" + "@injectivelabs/token-metadata" "^1.14.7" + "@injectivelabs/ts-types" "^1.14.6" + "@injectivelabs/utils" "^1.14.6" "@metamask/eth-sig-util" "^4.0.0" axios "^0.27.2" bech32 "^2.0.0" bip39 "^3.0.4" - cosmjs-types "^0.7.1" + cosmjs-types "^0.9.0" ethereumjs-util "^7.1.4" ethers "^5.7.2" google-protobuf "^3.21.0" @@ -1313,7 +1392,6 @@ jscrypto "^1.0.3" keccak256 "^1.0.6" link-module-alias "^1.2.0" - rxjs "^7.8.0" secp256k1 "^4.0.3" shx "^0.3.2" snakecase-keys "^5.4.1" @@ -1330,15 +1408,15 @@ snakecase-keys "^5.1.2" store2 "^2.12.0" -"@injectivelabs/token-metadata@^1.14.4": - version "1.14.4" - resolved "https://registry.yarnpkg.com/@injectivelabs/token-metadata/-/token-metadata-1.14.4.tgz#42642172c45be8607df15a99840c178f145abfcb" - integrity sha512-g0jrIFpEdQ4kjUUaMcXWmXWu5owpIGE6GQPj7Gx07kkPTDNDiIfcaHUQ0nzTmcG02h0AhFx1eSpkHZFLSwyG/Q== +"@injectivelabs/token-metadata@^1.14.7": + version "1.14.7" + resolved "https://registry.yarnpkg.com/@injectivelabs/token-metadata/-/token-metadata-1.14.7.tgz#0adba14e76e8882dc13a6a488ced0762fd888ae3" + integrity sha512-RRRuyirzoThwQ5P8D3STH2YOavGsdnetQy6ZPQ8yA7VUavt00KBB26M92zSLbiUz5VrxhPHDCEEkuJVWx+xtmw== dependencies: - "@injectivelabs/exceptions" "^1.14.4" - "@injectivelabs/networks" "^1.14.4" - "@injectivelabs/ts-types" "^1.14.4" - "@injectivelabs/utils" "^1.14.4" + "@injectivelabs/exceptions" "^1.14.6" + "@injectivelabs/networks" "^1.14.6" + "@injectivelabs/ts-types" "^1.14.6" + "@injectivelabs/utils" "^1.14.6" "@types/lodash.values" "^4.3.6" copyfiles "^2.4.1" jsonschema "^1.4.0" @@ -1347,21 +1425,21 @@ lodash.values "^4.3.0" shx "^0.3.2" -"@injectivelabs/ts-types@^1.14.4": - version "1.14.4" - resolved "https://registry.yarnpkg.com/@injectivelabs/ts-types/-/ts-types-1.14.4.tgz#d0c29289e167de2018a90b521bfa3a96f69a735c" - integrity sha512-c8g81bdrOQoi6S1CbeERRifkwNhjyLWB8lmF7liwRnPjyDciVHfHjikZjVWFLS9tJegNm44N9PWsM4RN9g0rXQ== +"@injectivelabs/ts-types@^1.14.6": + version "1.14.6" + resolved "https://registry.yarnpkg.com/@injectivelabs/ts-types/-/ts-types-1.14.6.tgz#917a14c8fed81c683bc7dece3ec254388123a10e" + integrity sha512-/Ax5eCSfE9OhcyUc9wZk/LFKTYhIY9RJIaNT/n92rbHjXSfXRLSX+Bvk62vC9Ej+SEBPp77WYngtrePPA1HEgw== dependencies: link-module-alias "^1.2.0" shx "^0.3.2" -"@injectivelabs/utils@^1.14.4": - version "1.14.4" - resolved "https://registry.yarnpkg.com/@injectivelabs/utils/-/utils-1.14.4.tgz#81e45e3504dc305d5b74ad60e26b62596f54cb72" - integrity sha512-eyx6XpgqdmEhEhdwNT4zEDYx+wXOhW01foCGC6e0Dmmrcxv5Bjd+R2BuLopKJ9h22OYNJm6dAX3ZtcoFwpFYbQ== +"@injectivelabs/utils@^1.14.6": + version "1.14.6" + resolved "https://registry.yarnpkg.com/@injectivelabs/utils/-/utils-1.14.6.tgz#1e5c60973f9b2bb1a0334a0cd5b3f56377904472" + integrity sha512-5I0h4GiLB5PPTl+g2lpevRP+WScvEbntdkoUQVtAdHewl4kutd5p1Kcnoi1Nvpq+sz5N/e9qtBIRuyxG38akOg== dependencies: - "@injectivelabs/exceptions" "^1.14.4" - "@injectivelabs/ts-types" "^1.14.4" + "@injectivelabs/exceptions" "^1.14.6" + "@injectivelabs/ts-types" "^1.14.6" axios "^0.21.1" bignumber.js "^9.0.1" http-status-codes "^2.2.0" @@ -1370,6 +1448,18 @@ snakecase-keys "^5.1.2" store2 "^2.12.0" +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" @@ -1395,25 +1485,59 @@ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== "@jridgewell/trace-mapping@^0.3.9": - version "0.3.20" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" - integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + version "0.3.22" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" + integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@keplr-wallet/types@0.12.72": + version "0.12.72" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.72.tgz#0e7d35a0c4f6758aa4c89d8b2044f388031692e3" + integrity sha512-gDP+NCPa4seTT1xU9bkIKbMw7N/LPla9/4/amDLIGyFb6OEjfnFkRuReI/cZe/8aEvlkYAKnfB0UMipHobsn5g== + dependencies: + long "^4.0.0" + +"@keplr-wallet/types@^0.11.12": + version "0.11.64" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.11.64.tgz#5a308c8c019b4e18f894e0f35f0904b60134d605" + integrity sha512-GgzeLDHHfZFyne3O7UIfFHj/uYqVbxAZI31RbBwt460OBbvwQzjrlZwvJW3vieWRAgxKSITjzEDBl2WneFTQdQ== + dependencies: + axios "^0.27.2" + long "^4.0.0" + "@keplr-wallet/types@^0.12.39": - version "0.12.44" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.44.tgz#0e6615b1aa441ee589f78c89bf59830936c4683f" - integrity sha512-eyJMmEsYcKN/5iTIWKPmpLVeG4QRMJUlPnFxGd48FTq5/1ZwtXv1AcjN9BB13BSbp/S6vo9it2pSG+swPrno0Q== + version "0.12.63" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.63.tgz#684b018d0b9fd9f9eee79eb530f2306dfc9dedfc" + integrity sha512-pN1+cVi5tjIjFjRS2jqUS2eoAHIApACGpJlXkrrzfQyxa6qO5W7B2sqQ9XGzpCoWl70VzpGm0Xlsj3lbBcaTKw== dependencies: long "^4.0.0" +"@keplr-wallet/unit@^0.12.67": + version "0.12.72" + resolved "https://registry.yarnpkg.com/@keplr-wallet/unit/-/unit-0.12.72.tgz#8a818be8d271acfe127dc029ef9c85b3be4bdb9c" + integrity sha512-egrh0L/uo6MQrAfVi1V8GQB1eml1ZbfWDiv5gpU6AkeAte4Q1DIe8qkYqeJOJ+KW1TurnxDw9L+9/T/i4Lu8Jg== + dependencies: + "@keplr-wallet/types" "0.12.72" + big-integer "^1.6.48" + utility-types "^3.10.0" + "@kurkle/color@^0.3.0": version "0.3.2" resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== +"@leapwallet/cosmos-snap-provider@^0.1.25": + version "0.1.25" + resolved "https://registry.yarnpkg.com/@leapwallet/cosmos-snap-provider/-/cosmos-snap-provider-0.1.25.tgz#f256cd4c7ef89aa9209ed8dbaf16487db24bde10" + integrity sha512-oov2jgISkvoAYvGsWkPscFt/XEEv1McTehTqJRfIJPO8uebIE6goJi4wjaaMW3wDIDsMMK54uGSdqbL6C8ciYQ== + dependencies: + "@cosmjs/amino" "^0.31.0" + "@cosmjs/proto-signing" "^0.31.0" + bignumber.js "^9.1.2" + long "^5.2.3" + "@mapbox/hast-util-table-cell-style@^0.2.0": version "0.2.1" resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz#b8e92afdd38b668cf0762400de980073d2ade101" @@ -1432,101 +1556,126 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" -"@mui/base@5.0.0-beta.24": - version "5.0.0-beta.24" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.24.tgz#1a0638388291828dacf9547b466bc21fbaad3a2a" - integrity sha512-bKt2pUADHGQtqWDZ8nvL2Lvg2GNJyd/ZUgZAJoYzRgmnxBL9j36MSlS3+exEdYkikcnvVafcBtD904RypFKb0w== +"@mui/base@5.0.0-beta.33": + version "5.0.0-beta.33" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.33.tgz#fbb844e2d840d47dd7a48850a03152aed2381d10" + integrity sha512-WcSpoJUw/UYHXpvgtl4HyMar2Ar97illUpqiS/X1gtSBp6sdDW6kB2BJ9OlVQ+Kk/RL2GDp/WHA9sbjAYV35ow== dependencies: - "@babel/runtime" "^7.23.2" - "@floating-ui/react-dom" "^2.0.4" - "@mui/types" "^7.2.9" - "@mui/utils" "^5.14.18" + "@babel/runtime" "^7.23.8" + "@floating-ui/react-dom" "^2.0.6" + "@mui/types" "^7.2.13" + "@mui/utils" "^5.15.6" "@popperjs/core" "^2.11.8" - clsx "^2.0.0" + clsx "^2.1.0" prop-types "^15.8.1" -"@mui/core-downloads-tracker@^5.14.18": - version "5.14.18" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.18.tgz#f8b187dc89756fa5c0b7d15aea537a6f73f0c2d8" - integrity sha512-yFpF35fEVDV81nVktu0BE9qn2dD/chs7PsQhlyaV3EnTeZi9RZBuvoEfRym1/jmhJ2tcfeWXiRuHG942mQXJJQ== +"@mui/core-downloads-tracker@^5.15.6": + version "5.15.6" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.6.tgz#9b82ba86d5a0fe55e9479b68dd5068943cc3835b" + integrity sha512-0aoWS4qvk1uzm9JBs83oQmIMIQeTBUeqqu8u+3uo2tMznrB5fIKqQVCbCgq+4Tm4jG+5F7dIvnjvQ2aV7UKtdw== "@mui/icons-material@^5.14.11": - version "5.14.18" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.14.18.tgz#9e92964cde8c7ba32cf50438a83403dc283f2328" - integrity sha512-o2z49R1G4SdBaxZjbMmkn+2OdT1bKymLvAYaB6pH59obM1CYv/0vAVm6zO31IqhwtYwXv6A7sLIwCGYTaVkcdg== + version "5.15.6" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.15.6.tgz#6958232bef48972fcbafd5f69e6079a9be5951f1" + integrity sha512-GnkxMtlhs+8ieHLmCytg00ew0vMOiXGFCw8Ra9nxMsBjBqnrOI5gmXqUm+sGggeEU/HG8HyeqC1MX/IxOBJHzA== dependencies: - "@babel/runtime" "^7.23.2" + "@babel/runtime" "^7.23.8" "@mui/material@^5.14.10": - version "5.14.18" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.14.18.tgz#d0a89be3e27afe90135d542ddbf160b3f34e869c" - integrity sha512-y3UiR/JqrkF5xZR0sIKj6y7xwuEiweh9peiN3Zfjy1gXWXhz5wjlaLdoxFfKIEBUFfeQALxr/Y8avlHH+B9lpQ== - dependencies: - "@babel/runtime" "^7.23.2" - "@mui/base" "5.0.0-beta.24" - "@mui/core-downloads-tracker" "^5.14.18" - "@mui/system" "^5.14.18" - "@mui/types" "^7.2.9" - "@mui/utils" "^5.14.18" - "@types/react-transition-group" "^4.4.8" - clsx "^2.0.0" + version "5.15.6" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.15.6.tgz#e32944ae4e01f85b314bc26e4cbbb700d598f30c" + integrity sha512-rw7bDdpi2kzfmcDN78lHp8swArJ5sBCKsn+4G3IpGfu44ycyWAWX0VdlvkjcR9Yrws2KIm7c+8niXpWHUDbWoA== + dependencies: + "@babel/runtime" "^7.23.8" + "@mui/base" "5.0.0-beta.33" + "@mui/core-downloads-tracker" "^5.15.6" + "@mui/system" "^5.15.6" + "@mui/types" "^7.2.13" + "@mui/utils" "^5.15.6" + "@types/react-transition-group" "^4.4.10" + clsx "^2.1.0" csstype "^3.1.2" prop-types "^15.8.1" react-is "^18.2.0" react-transition-group "^4.4.5" -"@mui/private-theming@^5.14.18": - version "5.14.18" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.14.18.tgz#98f97139ea21570493391ab377c1deb47fc6d1a2" - integrity sha512-WSgjqRlzfHU+2Rou3HlR2Gqfr4rZRsvFgataYO3qQ0/m6gShJN+lhVEvwEiJ9QYyVzMDvNpXZAcqp8Y2Vl+PAw== +"@mui/private-theming@^5.15.6": + version "5.15.6" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.15.6.tgz#224819694ed76df041b1257256152a45d1fd733d" + integrity sha512-ZBX9E6VNUSscUOtU8uU462VvpvBS7eFl5VfxAzTRVQBHflzL+5KtnGrebgf6Nd6cdvxa1o0OomiaxSKoN2XDmg== dependencies: - "@babel/runtime" "^7.23.2" - "@mui/utils" "^5.14.18" + "@babel/runtime" "^7.23.8" + "@mui/utils" "^5.15.6" prop-types "^15.8.1" -"@mui/styled-engine@^5.14.18": - version "5.14.18" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.14.18.tgz#82d427bc975b85cecdbab2fd9353ed6c2df7eae1" - integrity sha512-pW8bpmF9uCB5FV2IPk6mfbQCjPI5vGI09NOLhtGXPeph/4xIfC3JdIX0TILU0WcTs3aFQqo6s2+1SFgIB9rCXA== +"@mui/styled-engine@^5.15.6": + version "5.15.6" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.15.6.tgz#3f4a8804de6ddeee17cb52ec92225686f423398a" + integrity sha512-KAn8P8xP/WigFKMlEYUpU9z2o7jJnv0BG28Qu1dhNQVutsLVIFdRf5Nb+0ijp2qgtcmygQ0FtfRuXv5LYetZTg== dependencies: - "@babel/runtime" "^7.23.2" + "@babel/runtime" "^7.23.8" "@emotion/cache" "^11.11.0" csstype "^3.1.2" prop-types "^15.8.1" -"@mui/system@^5.14.18": - version "5.14.18" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.14.18.tgz#0f671e8f0a5e8e965b79235d77c50098f54195b5" - integrity sha512-hSQQdb3KF72X4EN2hMEiv8EYJZSflfdd1TRaGPoR7CIAG347OxCslpBUwWngYobaxgKvq6xTrlIl+diaactVww== - dependencies: - "@babel/runtime" "^7.23.2" - "@mui/private-theming" "^5.14.18" - "@mui/styled-engine" "^5.14.18" - "@mui/types" "^7.2.9" - "@mui/utils" "^5.14.18" - clsx "^2.0.0" +"@mui/system@^5.15.6": + version "5.15.6" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.6.tgz#d278adb09d57ee21f4eef2f6bc335bf9bd062fca" + integrity sha512-J01D//u8IfXvaEHMBQX5aO2l7Q+P15nt96c4NskX7yp5/+UuZP8XCQJhtBtLuj+M2LLyXHYGmCPeblsmmscP2Q== + dependencies: + "@babel/runtime" "^7.23.8" + "@mui/private-theming" "^5.15.6" + "@mui/styled-engine" "^5.15.6" + "@mui/types" "^7.2.13" + "@mui/utils" "^5.15.6" + clsx "^2.1.0" csstype "^3.1.2" prop-types "^15.8.1" -"@mui/types@^7.2.9": - version "7.2.9" - resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.9.tgz#730ee83a37af292a5973962f78ce5c95f31213a7" - integrity sha512-k1lN/PolaRZfNsRdAqXtcR71sTnv3z/VCCGPxU8HfdftDkzi335MdJ6scZxvofMAd/K/9EbzCZTFBmlNpQVdCg== +"@mui/types@^7.2.13": + version "7.2.13" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.13.tgz#d1584912942f9dc042441ecc2d1452be39c666b8" + integrity sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g== -"@mui/utils@^5.14.18": - version "5.14.18" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.14.18.tgz#d2a46df9b06230423ba6b6317748b27185a56ac3" - integrity sha512-HZDRsJtEZ7WMSnrHV9uwScGze4wM/Y+u6pDVo+grUjt5yXzn+wI8QX/JwTHh9YSw/WpnUL80mJJjgCnWj2VrzQ== +"@mui/utils@^5.10.3", "@mui/utils@^5.15.6": + version "5.15.6" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.6.tgz#bbcc302b8e83e360a87230afe3ed8fc99e29fae9" + integrity sha512-qfEhf+zfU9aQdbzo1qrSWlbPQhH1nCgeYgwhOVnj9Bn39shJQitEnXpSQpSNag8+uty5Od6PxmlNKPTnPySRKA== dependencies: - "@babel/runtime" "^7.23.2" - "@types/prop-types" "^15.7.10" + "@babel/runtime" "^7.23.8" + "@types/prop-types" "^15.7.11" prop-types "^15.8.1" react-is "^18.2.0" -"@next/env@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.3.tgz#9a58b296e7ae04ffebce8a4e5bd0f87f71de86bd" - integrity sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA== +"@mui/x-date-pickers@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-5.0.4.tgz#79a509354eea4bedaa955ee52f37d80256d7e415" + integrity sha512-Co4tbwqXSdHfR8UoZSHQpDZqnFdikzQr0lQPG2AjGh9BdB4EdY3YE2+sZyAltjk/AXxp5JzIWDZ2Kj83ClzjwA== + dependencies: + "@babel/runtime" "^7.18.9" + "@date-io/core" "^2.15.0" + "@date-io/date-fns" "^2.15.0" + "@date-io/dayjs" "^2.15.0" + "@date-io/luxon" "^2.15.0" + "@date-io/moment" "^2.15.0" + "@mui/utils" "^5.10.3" + "@types/react-transition-group" "^4.4.5" + clsx "^1.2.1" + prop-types "^15.7.2" + react-transition-group "^4.4.5" + rifm "^0.12.1" + +"@next/bundle-analyzer@^14.2.3": + version "14.2.3" + resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-14.2.3.tgz#dfa43586983d3fffdeb5f3c50f2c65ab1c51f184" + integrity sha512-Z88hbbngMs7njZKI8kTJIlpdLKYfMSLwnsqYe54AP4aLmgL70/Ynx/J201DQ+q2Lr6FxFw1uCeLGImDrHOl2ZA== + dependencies: + webpack-bundle-analyzer "4.10.1" + +"@next/env@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.0.tgz#43d92ebb53bc0ae43dcc64fb4d418f8f17d7a341" + integrity sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw== "@next/eslint-plugin-next@13.5.2": version "13.5.2" @@ -1535,50 +1684,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz#b1a0440ffbf69056451947c4aea5b6d887e9fbbc" - integrity sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw== - -"@next/swc-darwin-x64@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz#48b527ef7eb5dbdcaf62fd107bc3a78371f36f09" - integrity sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ== - -"@next/swc-linux-arm64-gnu@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz#0a36475a38b2855ab8ea0fe8b56899bc90184c0f" - integrity sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg== - -"@next/swc-linux-arm64-musl@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz#25328a9f55baa09fde6364e7e47ade65c655034f" - integrity sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA== - -"@next/swc-linux-x64-gnu@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.3.tgz#594b747e3c8896b2da67bba54fcf8a6b5a410e5e" - integrity sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg== - -"@next/swc-linux-x64-musl@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.3.tgz#a02da58fc6ecad8cf5c5a2a96a7f6030ec7f6215" - integrity sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg== - -"@next/swc-win32-arm64-msvc@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz#bf2be23d3ba2ebd0d4a9376a31f783efdb677b48" - integrity sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog== - -"@next/swc-win32-ia32-msvc@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz#839f8de85a4bf2c3c69242483ab87cb916427551" - integrity sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg== - -"@next/swc-win32-x64-msvc@14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz#27b623612b1d0cea6efe0a0d31aa1a335fc99647" - integrity sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ== +"@next/swc-darwin-arm64@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz#70a57c87ab1ae5aa963a3ba0f4e59e18f4ecea39" + integrity sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ== + +"@next/swc-darwin-x64@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz#0863a22feae1540e83c249384b539069fef054e9" + integrity sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g== + +"@next/swc-linux-arm64-gnu@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz#893da533d3fce4aec7116fe772d4f9b95232423c" + integrity sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ== + +"@next/swc-linux-arm64-musl@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz#d81ddcf95916310b8b0e4ad32b637406564244c0" + integrity sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g== + +"@next/swc-linux-x64-gnu@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz#18967f100ec19938354332dcb0268393cbacf581" + integrity sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ== + +"@next/swc-linux-x64-musl@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz#77077cd4ba8dda8f349dc7ceb6230e68ee3293cf" + integrity sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg== + +"@next/swc-win32-arm64-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz#5f0b8cf955644104621e6d7cc923cad3a4c5365a" + integrity sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ== + +"@next/swc-win32-ia32-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz#21f4de1293ac5e5a168a412b139db5d3420a89d0" + integrity sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw== + +"@next/swc-win32-x64-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz#e561fb330466d41807123d932b365cf3d33ceba2" + integrity sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg== "@noble/curves@1.2.0", "@noble/curves@~1.2.0": version "1.2.0" @@ -1587,12 +1736,12 @@ dependencies: "@noble/hashes" "1.3.2" -"@noble/hashes@1.3.2", "@noble/hashes@^1", "@noble/hashes@^1.0.0": +"@noble/hashes@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== -"@noble/hashes@^1.2.0", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": +"@noble/hashes@^1", "@noble/hashes@^1.0.0", "@noble/hashes@^1.2.0", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== @@ -1618,6 +1767,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@polka/url@^1.0.0-next.24": + version "1.0.0-next.25" + resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" + integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== + "@popperjs/core@^2.11.8": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" @@ -1686,15 +1845,15 @@ redux-thunk "^2.4.2" reselect "^4.1.8" -"@remix-run/router@1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.0.tgz#7e29c4ee85176d9c08cb0f4456bff74d092c5065" - integrity sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA== +"@remix-run/router@1.14.2": + version "1.14.2" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.14.2.tgz#4d58f59908d9197ba3179310077f25c88e49ed17" + integrity sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg== "@rushstack/eslint-patch@^1.3.3": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz#1898e7a7b943680d757417a47fb10f5fcc230b39" - integrity sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA== + version "1.7.0" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.0.tgz#b5bc1e081428794f6a4d239707b359404be35ce2" + integrity sha512-Jh4t/593gxs0lJZ/z3NnasKlplXT2f+4y/LZYuaKZW5KAaiVFL/fThhs+17EbUd53jUVJ0QudYCBGbN/psvaqg== "@scure/base@~1.1.0", "@scure/base@~1.1.2": version "1.1.5" @@ -1718,32 +1877,26 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" -"@skip-router/core@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@skip-router/core/-/core-1.1.1.tgz#29e48905a8d75df15fe73f751fb9c91fb96731ae" - integrity sha512-b5TL08cKJkxSyJFLRHlCiDbTITzsq+48x+X3dkRpb3RXhpLOv0r0inq9j0aM/1HoaYs/awm/na7+VrImMeY4Hw== - dependencies: - "@axelar-network/axelarjs-sdk" "^0.13.6" - "@cosmjs/amino" "^0.31.1" - "@cosmjs/cosmwasm-stargate" "^0.31.1" - "@cosmjs/encoding" "^0.31.1" - "@cosmjs/math" "^0.31.1" - "@cosmjs/proto-signing" "^0.31.1" - "@cosmjs/stargate" "^0.31.1" - "@cosmjs/tendermint-rpc" "^0.31.1" - "@injectivelabs/core-proto-ts" "^0.0.18" - "@injectivelabs/sdk-ts" "^1.12.1" - axios "^1.4.0" - chain-registry "^1.19.0" - cosmjs-types "^0.8.0" - faker "^6.6.6" - keccak256 "^1.0.6" - viem "^1.12.2" - -"@socket.io/component-emitter@~3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" - integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== +"@skip-router/core@^1.3.11": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@skip-router/core/-/core-1.3.11.tgz#f59ecfae353ecb5cc74165c1b9f543e4001c9f33" + integrity sha512-lStTsB1F/ODhyrkh6kpPT+FrnbjgCmBhm0aKv7F8zI2Y8KHNdNtsB8dQlPHs4tidjHr1e2QkHRsaiwkLRyt6Hw== + dependencies: + "@cosmjs/amino" "0.31.x" + "@cosmjs/cosmwasm-stargate" "0.31.x" + "@cosmjs/encoding" "0.31.x" + "@cosmjs/math" "0.31.x" + "@cosmjs/proto-signing" "0.31.x" + "@cosmjs/stargate" "0.31.x" + "@cosmjs/tendermint-rpc" "0.31.x" + "@injectivelabs/core-proto-ts" "0.0.x" + "@injectivelabs/sdk-ts" "1.x" + "@keplr-wallet/unit" "^0.12.67" + axios "1.x" + cosmjs-types "0.8.x" + keccak256 "1.x" + kujira.js "0.9.x" + viem "1.x" "@swc/helpers@0.5.2": version "0.5.2" @@ -1766,6 +1919,11 @@ dependencies: "@types/node" "*" +"@types/google-protobuf@^3.15.6": + version "3.15.12" + resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.12.tgz#eb2ba0eddd65712211a2b455dc6071d665ccf49b" + integrity sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ== + "@types/hoist-non-react-statics@^3.3.1": version "3.3.5" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" @@ -1808,10 +1966,17 @@ dependencies: "@types/unist" "^2" -"@types/node@*": - version "20.10.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.4.tgz#b246fd84d55d5b1b71bf51f964bd514409347198" - integrity sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg== +"@types/node-gzip@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/node-gzip/-/node-gzip-1.1.0.tgz#99a7dfab7c0eec545658f3d736e8d6939ed7161e" + integrity sha512-j7cGb6HIOZbDx3sqe9/9VAPeSvyt143yu5k35gzRXE3mxEgK6BOZ6BAiJ3ToXBcJqLzL9Cr53dav21jlp3f9gw== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>=13.7.0": + version "20.11.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.5.tgz#be10c622ca7fcaa3cf226cf80166abc31389d86e" + integrity sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w== dependencies: undici-types "~5.26.4" @@ -1820,13 +1985,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.5.tgz#4c6a79adf59a8e8193ac87a0e522605b16587258" integrity sha512-2qGq5LAOTh9izcc0+F+dToFigBWiK1phKPt7rNhOqJSr35y8rlIBjDwGtFSgAI6MGIhjwOVNSQZVdJsZJ2uR1w== -"@types/node@>=13.7.0": - version "20.10.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.0.tgz#16ddf9c0a72b832ec4fcce35b8249cf149214617" - integrity sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ== - dependencies: - undici-types "~5.26.4" - "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -1839,7 +1997,7 @@ dependencies: "@types/node" "*" -"@types/prop-types@*", "@types/prop-types@^15.7.10": +"@types/prop-types@*", "@types/prop-types@^15.7.11": version "15.7.11" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== @@ -1851,17 +2009,17 @@ dependencies: "@types/react" "*" -"@types/react-transition-group@^4.4.8": - version "4.4.9" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.9.tgz#12a1a1b5b8791067198149867b0823fbace31579" - integrity sha512-ZVNmWumUIh5NhH8aMD9CR2hdW0fNuYInlocZHaZ+dgk/1K49j1w/HoAuK1ki+pgscQrOFRTlXeoURtuzEkV3dg== +"@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.5": + version "4.4.10" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" + integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q== dependencies: "@types/react" "*" "@types/react@*": - version "18.2.39" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.39.tgz#744bee99e053ad61fe74eb8b897f3ab5b19a7e25" - integrity sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA== + version "18.2.48" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1" + integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -1903,21 +2061,16 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== -"@types/uuid@^8.3.1": - version "8.3.4" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" - integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== - "@typescript-eslint/eslint-plugin@^6.9.1": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.0.tgz#c239f9b3800ab14b5479a93812ddbe4a8fc64411" - integrity sha512-HTvbSd0JceI2GW5DHS3R9zbarOqjkM9XDR7zL8eCsBUO/eSiHcoNE7kSL5sjGXmVa9fjH5LCfHDXNnH4QLp7tQ== + version "6.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz#bb0676af940bc23bf299ca58dbdc6589c2548c2e" + integrity sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.13.0" - "@typescript-eslint/type-utils" "6.13.0" - "@typescript-eslint/utils" "6.13.0" - "@typescript-eslint/visitor-keys" "6.13.0" + "@typescript-eslint/scope-manager" "6.19.1" + "@typescript-eslint/type-utils" "6.19.1" + "@typescript-eslint/utils" "6.19.1" + "@typescript-eslint/visitor-keys" "6.19.1" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -1926,71 +2079,72 @@ ts-api-utils "^1.0.1" "@typescript-eslint/parser@^5.4.2 || ^6.0.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.13.0.tgz#ddb2be591c347ff292165ecffbd0b6d508d7463a" - integrity sha512-VpG+M7GNhHLI/aTDctqAV0XbzB16vf+qDX9DXuMZSe/0bahzDA9AKZB15NDbd+D9M4cDsJvfkbGOA7qiZ/bWJw== - dependencies: - "@typescript-eslint/scope-manager" "6.13.0" - "@typescript-eslint/types" "6.13.0" - "@typescript-eslint/typescript-estree" "6.13.0" - "@typescript-eslint/visitor-keys" "6.13.0" + version "6.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.19.1.tgz#68a87bb21afaf0b1689e9cdce0e6e75bc91ada78" + integrity sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ== + dependencies: + "@typescript-eslint/scope-manager" "6.19.1" + "@typescript-eslint/types" "6.19.1" + "@typescript-eslint/typescript-estree" "6.19.1" + "@typescript-eslint/visitor-keys" "6.19.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.13.0.tgz#343665d5c87c78ebea38ab4577ad3ece0751f331" - integrity sha512-2x0K2/CujsokIv+LN2T0l5FVDMtsCjkUyYtlcY4xxnxLAW+x41LXr16duoicHpGtLhmtN7kqvuFJ3zbz00Ikhw== +"@typescript-eslint/scope-manager@6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz#2f527ee30703a6169a52b31d42a1103d80acd51b" + integrity sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w== dependencies: - "@typescript-eslint/types" "6.13.0" - "@typescript-eslint/visitor-keys" "6.13.0" + "@typescript-eslint/types" "6.19.1" + "@typescript-eslint/visitor-keys" "6.19.1" -"@typescript-eslint/type-utils@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.13.0.tgz#71e96a5f718b6857eba499136d109239c8f87f55" - integrity sha512-YHufAmZd/yP2XdoD3YeFEjq+/Tl+myhzv+GJHSOz+ro/NFGS84mIIuLU3pVwUcauSmwlCrVXbBclkn1HfjY0qQ== +"@typescript-eslint/type-utils@6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz#6a130e3afe605a4898e043fa9f72e96309b54935" + integrity sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg== dependencies: - "@typescript-eslint/typescript-estree" "6.13.0" - "@typescript-eslint/utils" "6.13.0" + "@typescript-eslint/typescript-estree" "6.19.1" + "@typescript-eslint/utils" "6.19.1" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.13.0.tgz#45147f658ae0aa33a3999cdf1727613d6467c271" - integrity sha512-oXg7DFxx/GmTrKXKKLSoR2rwiutOC7jCQ5nDH5p5VS6cmHE1TcPTaYQ0VPSSUvj7BnNqCgQ/NXcTBxn59pfPTQ== +"@typescript-eslint/types@6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.19.1.tgz#2d4c9d492a63ede15e7ba7d129bdf7714b77f771" + integrity sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg== -"@typescript-eslint/typescript-estree@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.0.tgz#4975f49c1a7a035720bc2b1f7862a61d30d52943" - integrity sha512-IT4O/YKJDoiy/mPEDsfOfp+473A9GVqXlBKckfrAOuVbTqM8xbc0LuqyFCcgeFWpqu3WjQexolgqN2CuWBYbog== +"@typescript-eslint/typescript-estree@6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz#796d88d88882f12e85bb33d6d82d39e1aea54ed1" + integrity sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA== dependencies: - "@typescript-eslint/types" "6.13.0" - "@typescript-eslint/visitor-keys" "6.13.0" + "@typescript-eslint/types" "6.19.1" + "@typescript-eslint/visitor-keys" "6.19.1" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" + minimatch "9.0.3" semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.13.0.tgz#f2ee6ba06cf76c1879fd7dfceb6a5f886d70985c" - integrity sha512-V+txaxARI8yznDkcQ6FNRXxG+T37qT3+2NsDTZ/nKLxv6VfGrRhTnuvxPUxpVuWWr+eVeIxU53PioOXbz8ratQ== +"@typescript-eslint/utils@6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.19.1.tgz#df93497f9cfddde2bcc2a591da80536e68acd151" + integrity sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.13.0" - "@typescript-eslint/types" "6.13.0" - "@typescript-eslint/typescript-estree" "6.13.0" + "@typescript-eslint/scope-manager" "6.19.1" + "@typescript-eslint/types" "6.19.1" + "@typescript-eslint/typescript-estree" "6.19.1" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.13.0": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.0.tgz#1b57d83fb74e2d7a02382e1ee6abda92ca5973f4" - integrity sha512-UQklteCEMCRoq/1UhKFZsHv5E4dN1wQSzJoxTfABasWk1HgJRdg1xNUve/Kv/Sdymt4x+iEzpESOqRFlQr/9Aw== +"@typescript-eslint/visitor-keys@6.19.1": + version "6.19.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz#2164073ed4fc34a5ff3b5e25bb5a442100454c4c" + integrity sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ== dependencies: - "@typescript-eslint/types" "6.13.0" + "@typescript-eslint/types" "6.19.1" eslint-visitor-keys "^3.4.1" "@wry/caches@^1.0.0": @@ -2038,10 +2192,20 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: - version "8.11.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" - integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== +acorn-walk@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + +acorn@^8.0.4, acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +adler-32@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2" + integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== aes-js@3.0.0: version "3.0.0" @@ -2063,6 +2227,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2077,6 +2246,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -2225,6 +2399,15 @@ axe-core@=4.7.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axios@1.x: + version "1.6.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" + integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axios@^0.21.1, axios@^0.21.2: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -2240,12 +2423,12 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" -axios@^1.4.0, axios@^1.5.1, axios@^1.6.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" - integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== +axios@^1.5.1, axios@^1.6.0: + version "1.6.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8" + integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg== dependencies: - follow-redirects "^1.15.0" + follow-redirects "^1.15.4" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -2297,7 +2480,12 @@ bech32@^2.0.0: resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== -bignumber.js@^9.0.1: +big-integer@^1.6.48: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + +bignumber.js@^9.0.1, bignumber.js@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== @@ -2307,7 +2495,7 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bip39@^3.0.4: +bip39@^3.0.2, bip39@^3.0.4: version "3.1.0" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== @@ -2337,6 +2525,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -2367,13 +2562,13 @@ browserify-aes@^1.2.0: safe-buffer "^5.0.1" browserslist@^4.21.10: - version "4.22.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" - integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== + version "4.22.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b" + integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== dependencies: - caniuse-lite "^1.0.30001541" - electron-to-chromium "^1.4.535" - node-releases "^2.0.13" + caniuse-lite "^1.0.30001565" + electron-to-chromium "^1.4.601" + node-releases "^2.0.14" update-browserslist-db "^1.0.13" bs58@^4.0.0: @@ -2436,18 +2631,34 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: - version "1.0.30001565" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz#a528b253c8a2d95d2b415e11d8b9942acc100c4f" - integrity sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w== +caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001579: + version "1.0.30001579" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz#45c065216110f46d6274311a4b3fcf6278e0852a" + integrity sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA== -chain-registry@^1.19.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/chain-registry/-/chain-registry-1.21.0.tgz#4e781d464035d64996aa80e01fe80ddb8ba3f3fa" - integrity sha512-DkhulMvok8wRClECDHOPLylVfmE4yl/ilkVO+Ht6XLz34oq5bJbleblSRhvYc1pjFg8AJ7FmXGvxkrWA6LdTHg== +cfb@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" + integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== + dependencies: + adler-32 "~1.3.0" + crc-32 "~1.2.0" + +chain-registry@1.28.1: + version "1.28.1" + resolved "https://registry.yarnpkg.com/chain-registry/-/chain-registry-1.28.1.tgz#3aa6fd1199e06e1e95fe6a53b55076962effb980" + integrity sha512-WYFhulujAss5llO75CAoLVbMs+EmiYH6N/+XAtV2xBCI0V8eI2yBxHI93w9YsagizE8Ew9wCuNZk8QdAXPNHtg== + dependencies: + "@babel/runtime" "^7.21.0" + "@chain-registry/types" "^0.18.0" + +chain-registry@^1.27.0: + version "1.28.7" + resolved "https://registry.yarnpkg.com/chain-registry/-/chain-registry-1.28.7.tgz#c4efa2a3a10e71c45166a6f03d58593683d08d02" + integrity sha512-XsLGriPOonROPOD069KkZEuORw4ngJf2ntME4dbuavh4B3asYwXWK4oak0kGR2fQC2GI0vnBuWNs4dEGuH5njA== dependencies: "@babel/runtime" "^7.21.0" - "@chain-registry/types" "^0.17.0" + "@chain-registry/types" "^0.18.1" chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" @@ -2525,19 +2736,20 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== -clsx@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" - integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== +clsx@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" + integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== + +codepage@~1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab" + integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== color-convert@^1.9.0: version "1.9.3" @@ -2596,6 +2808,11 @@ commander@^4.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + complex.js@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.1.1.tgz#0675dac8e464ec431fb2ab7d30f41d889fb25c31" @@ -2640,15 +2857,7 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cosmjs-types@^0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.7.2.tgz#a757371abd340949c5bd5d49c6f8379ae1ffd7e2" - integrity sha512-vf2uLyktjr/XVAgEq0DjMxeAWh1yYREe7AMHDKd7EiHVqxBPCaBS+qEEQUkXbR9ndnckqr1sUG8BQhazh4X5lA== - dependencies: - long "^4.0.0" - protobufjs "~6.11.2" - -cosmjs-types@^0.8.0: +cosmjs-types@0.8.x, cosmjs-types@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.8.0.tgz#2ed78f3e990f770229726f95f3ef5bf9e2b6859b" integrity sha512-Q2Mj95Fl0PYMWEhA2LuGEIhipF7mQwd9gTQ85DdP9jjjopeoGaDxvmPa5nakNzsq7FnO1DMTatXTAx6bxMH7Lg== @@ -2661,6 +2870,11 @@ cosmjs-types@^0.9.0: resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.9.0.tgz#c3bc482d28c7dfa25d1445093fdb2d9da1f6cfcc" integrity sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ== +crc-32@~1.2.0, crc-32@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -2684,14 +2898,7 @@ create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-fetch@^3.1.5: - version "3.1.8" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" - integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== - dependencies: - node-fetch "^2.6.12" - -cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2706,15 +2913,27 @@ cssesc@^3.0.0: integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== csstype@^3.0.2, csstype@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +date-fns@2.30.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2722,7 +2941,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@^4.0.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2748,7 +2967,7 @@ define-data-property@^1.0.1, define-data-property@^1.1.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" -define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -2826,10 +3045,20 @@ duplexer2@^0.1.2: dependencies: readable-stream "^2.0.2" -electron-to-chromium@^1.4.535: - version "1.4.595" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.595.tgz#fa33309eb9aabb7426915f8e166ec60f664e9ad4" - integrity sha512-+ozvXuamBhDOKvMNUQvecxfbyICmIAwS4GpLmR0bsiSBlGnLaOcs2Cj7J8XSbW+YEaN3Xl3ffgpm+srTUWFwFQ== +duplexer@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +electron-to-chromium@^1.4.601: + version "1.4.643" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.643.tgz#081a20c5534db91e66ef094f68624960f674768f" + integrity sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg== elliptic@6.5.4, elliptic@^6.5.2, elliptic@^6.5.4: version "6.5.4" @@ -2844,6 +3073,32 @@ elliptic@6.5.4, elliptic@^6.5.2, elliptic@^6.5.4: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +elliptic@^6.5.3: + version "6.5.5" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" + integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +elliptic@^6.5.7: + version "6.5.7" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" + integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -2854,22 +3109,6 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -engine.io-client@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.3.tgz#4cf6fa24845029b238f83c628916d9149c399bc5" - integrity sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - engine.io-parser "~5.2.1" - ws "~8.11.0" - xmlhttprequest-ssl "~2.0.0" - -engine.io-parser@~5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" - integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== - enhanced-resolve@^5.12.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -3011,9 +3250,9 @@ eslint-config-next@13.5.2: eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" eslint-config-prettier@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f" - integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw== + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -3045,9 +3284,9 @@ eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: debug "^3.2.7" eslint-plugin-import@^2.28.1: - version "2.29.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz#8133232e4329ee344f2f612885ac3073b0b7e155" - integrity sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg== + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== dependencies: array-includes "^3.1.7" array.prototype.findlastindex "^1.2.3" @@ -3065,7 +3304,7 @@ eslint-plugin-import@^2.28.1: object.groupby "^1.0.1" object.values "^1.1.7" semver "^6.3.1" - tsconfig-paths "^3.14.2" + tsconfig-paths "^3.15.0" eslint-plugin-jsx-a11y@^6.7.1: version "6.8.0" @@ -3226,6 +3465,14 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" +ethereum-multicall@2.23.0: + version "2.23.0" + resolved "https://registry.yarnpkg.com/ethereum-multicall/-/ethereum-multicall-2.23.0.tgz#9f39e80cae6d6d587b5f64d1e78152add32bb5b3" + integrity sha512-KVboRQSXzJ/czaD9UXIuYFKF9YwDsWORGDDRNyOtkBRYg7TUk2RkmqLcu/+uXUemdb3+XRcAU7c8bfdfOGfoiQ== + dependencies: + "@ethersproject/providers" "^5.0.10" + ethers "^5.0.15" + ethereumjs-abi@^0.6.8: version "0.6.8" resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz#71bc152db099f70e62f108b7cdfca1b362c6fcae" @@ -3258,7 +3505,7 @@ ethereumjs-util@^7.1.4: ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethers@^5.7.2: +ethers@^5.0.15, ethers@^5.7.1, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -3315,17 +3562,17 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -faker@^6.6.6: - version "6.6.6" - resolved "https://registry.yarnpkg.com/faker/-/faker-6.6.6.tgz#e9529da0109dca4c7c5dbfeaadbd9234af943033" - integrity sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg== +fast-average-color@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/fast-average-color/-/fast-average-color-9.4.0.tgz#eea0182fa8818ea0c70dcc7a85b945f3c716b11b" + integrity sha512-bvM8vV6YwK07dPbzFz77zJaBcfF6ABVfgNwaxVgXc2G+o0e/tzLCF9WU8Ryp1r0Nkk6JuJNsWCzbb4cLOMlB+Q== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.1: +fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -3347,9 +3594,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + version "1.16.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.16.0.tgz#83b9a9375692db77a822df081edb6a9cf6839320" + integrity sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA== dependencies: reusify "^1.0.4" @@ -3394,10 +3641,10 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== -follow-redirects@^1.14.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== +follow-redirects@^1.14.0, follow-redirects@^1.14.9, follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== for-each@^0.3.3: version "0.3.3" @@ -3406,6 +3653,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3415,6 +3670,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + fraction.js@4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.4.tgz#b2bac8249a610c3396106da97c5a71da75b94b1c" @@ -3499,23 +3759,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@7.1.7: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" @@ -3528,6 +3771,17 @@ glob@7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^10.3.10: + version "10.3.10" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + glob@^7.0.0, glob@^7.0.5, glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3541,9 +3795,9 @@ glob@^7.0.0, glob@^7.0.5, glob@^7.1.3: path-is-absolute "^1.0.0" globals@^13.19.0: - version "13.23.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" - integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -3578,7 +3832,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.2.4: +graceful-fs@^4.2.11, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3600,6 +3854,13 @@ graphql@^16.3.0: resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3615,7 +3876,7 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0: +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== @@ -3692,6 +3953,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react- dependencies: react-is "^16.7.0" +html-escaper@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + html-tokenize@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/html-tokenize/-/html-tokenize-2.0.1.tgz#c3b2ea6e2837d4f8c06693393e9d2a12c960be5f" @@ -3931,12 +4197,10 @@ is-plain-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== is-regex@^1.1.4: version "1.1.4" @@ -4019,11 +4283,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - isomorphic-ws@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" @@ -4045,12 +4304,21 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + javascript-natural-sort@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw== -jiti@^1.18.2: +jiti@^1.19.1: version "1.21.0" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== @@ -4119,7 +4387,7 @@ jsonschema@^1.4.0, jsonschema@^1.4.1: object.assign "^4.1.4" object.values "^1.1.6" -keccak256@^1.0.6: +keccak256@1.x, keccak256@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/keccak256/-/keccak256-1.0.6.tgz#dd32fb771558fed51ce4e45a035ae7515573da58" integrity sha512-8GLiM01PkdJVGUhR1e6M/AvWnSqYS0HaERI+K/QtStGDGlSTx2B1zTqZk4Zlqu5TxHJNTxWAdP9Y+WI50OApUw== @@ -4144,10 +4412,22 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" -kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +kujira.js@0.9.x: + version "0.9.150" + resolved "https://registry.yarnpkg.com/kujira.js/-/kujira.js-0.9.150.tgz#4b4e003d07518aa097e5a813767443abd0eb2f61" + integrity sha512-F/xEFgHAh9KG+yw3FhasTADN1l8BnojJTjk6bDm0NFNqHyDYWuamu/zUuDeYT3RietDBdHKI1qEfmbw+svzb+g== + dependencies: + "@cosmjs/cosmwasm-stargate" "^0.31.1" + "@cosmjs/launchpad" "^0.27.1" + "@cosmjs/stargate" "^0.31.1" + "@ethersproject/bignumber" "^5.7.0" + "@keplr-wallet/types" "^0.11.12" + "@types/google-protobuf" "^3.15.6" + chain-registry "^1.27.0" + cosmjs-types "^0.8.0" + long "^4.0.0" + text-encoding "^0.7.0" + yarn "^1.22.19" language-subtag-registry@^0.3.20: version "0.3.22" @@ -4242,7 +4522,7 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== -long@^5.0.0, long@^5.2.0: +long@^5.0.0, long@^5.2.3: version "5.2.3" resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== @@ -4268,17 +4548,22 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +"lru-cache@^9.1.1 || ^10.0.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" + integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== + map-obj@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== mathjs@^12.0.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-12.1.0.tgz#0b8af34aba7acb4ed9555ac5ec8703211c2fae76" - integrity sha512-x5wUkWo3CfXMl4mf/jHsbVNuMcUwN2m4qbj+tR/jYr9fdiIrlsexQbKYav8Uwfe+E5he30CyVaABNLmAlTFO3w== + version "12.3.0" + resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-12.3.0.tgz#83fea1983325a47d711cca7c13317381597f4c8e" + integrity sha512-Mik+O8gbH14/1V2D/vdJNgu+qGXpF+2oeBJVBqN8nbOdZNuu4Nxw6aDbJ0QOkDSq/9bQ+AZpXoIxBuErRODS8w== dependencies: - "@babel/runtime" "^7.23.2" + "@babel/runtime" "^7.23.8" complex.js "^2.1.1" decimal.js "^10.4.3" escape-latex "^1.2.0" @@ -4382,6 +4667,13 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== +minimatch@9.0.3, minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -4394,15 +4686,25 @@ minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6, minimist@~1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== moment@^2.29.4: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +mrmime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== ms@2.1.2: version "2.1.2" @@ -4431,7 +4733,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.3.6: +nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== @@ -4442,27 +4744,27 @@ natural-compare@^1.4.0: integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== next@^14.0.1: - version "14.0.3" - resolved "https://registry.yarnpkg.com/next/-/next-14.0.3.tgz#8d801a08eaefe5974203d71092fccc463103a03f" - integrity sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw== + version "14.1.0" + resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69" + integrity sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q== dependencies: - "@next/env" "14.0.3" + "@next/env" "14.1.0" "@swc/helpers" "0.5.2" busboy "1.6.0" - caniuse-lite "^1.0.30001406" + caniuse-lite "^1.0.30001579" + graceful-fs "^4.2.11" postcss "8.4.31" styled-jsx "5.1.1" - watchpack "2.4.0" optionalDependencies: - "@next/swc-darwin-arm64" "14.0.3" - "@next/swc-darwin-x64" "14.0.3" - "@next/swc-linux-arm64-gnu" "14.0.3" - "@next/swc-linux-arm64-musl" "14.0.3" - "@next/swc-linux-x64-gnu" "14.0.3" - "@next/swc-linux-x64-musl" "14.0.3" - "@next/swc-win32-arm64-msvc" "14.0.3" - "@next/swc-win32-ia32-msvc" "14.0.3" - "@next/swc-win32-x64-msvc" "14.0.3" + "@next/swc-darwin-arm64" "14.1.0" + "@next/swc-darwin-x64" "14.1.0" + "@next/swc-linux-arm64-gnu" "14.1.0" + "@next/swc-linux-arm64-musl" "14.1.0" + "@next/swc-linux-x64-gnu" "14.1.0" + "@next/swc-linux-x64-musl" "14.1.0" + "@next/swc-win32-arm64-msvc" "14.1.0" + "@next/swc-win32-ia32-msvc" "14.1.0" + "@next/swc-win32-x64-msvc" "14.1.0" no-case@^3.0.4: version "3.0.4" @@ -4477,22 +4779,25 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== -node-fetch@^2.6.12: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== node-gyp-build@^4.2.0: - version "4.7.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.7.1.tgz#cd7d2eb48e594874053150a9418ac85af83ca8f7" - integrity sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg== + version "4.8.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" + integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== -node-releases@^2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" - integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== +node-gzip@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/node-gzip/-/node-gzip-1.1.2.tgz#245bd171b31ce7c7f50fc4cd0ca7195534359afb" + integrity sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw== + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== noms@0.0.0: version "0.0.0" @@ -4538,12 +4843,12 @@ object-keys@~0.4.0: integrity sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw== object.assign@^4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" + call-bind "^1.0.5" + define-properties "^1.2.1" has-symbols "^1.0.3" object-keys "^1.1.1" @@ -4599,6 +4904,11 @@ once@^1.3.0: dependencies: wrappy "1" +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optimism@^0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.0.tgz#e7bb38b24715f3fdad8a9a7fc18e999144bbfa63" @@ -4689,6 +4999,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -4757,9 +5075,9 @@ postcss-nested@^6.0.1: postcss-selector-parser "^6.0.11" postcss-selector-parser@^6.0.11: - version "6.0.13" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" - integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + version "6.0.15" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535" + integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -4778,7 +5096,7 @@ postcss@8.4.30: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@8.4.31, postcss@^8.4.23: +postcss@8.4.31: version "8.4.31" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== @@ -4787,15 +5105,24 @@ postcss@8.4.31, postcss@^8.4.23: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.23: + version "8.4.33" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prettier@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e" - integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw== + version "3.2.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.4.tgz#4723cadeac2ce7c9227de758e5ff9b14e075f283" + integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ== process-nextick-args@~2.0.0: version "2.0.1" @@ -4838,9 +5165,9 @@ protobufjs@^6.8.8, protobufjs@~6.11.2, protobufjs@~6.11.3: long "^4.0.0" protobufjs@^7.0.0: - version "7.2.5" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" - integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== + version "7.2.6" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.6.tgz#4a0ccd79eb292717aacf07530a07e0ed20278215" + integrity sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -4877,11 +5204,6 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -react-chartjs-2@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d" - integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== - react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -4890,10 +5212,15 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-ga@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-3.3.1.tgz#d8e1f4e05ec55ed6ff944dcb14b99011dfaf9504" + integrity sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ== + react-hook-form@^7.47.0: - version "7.48.2" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.48.2.tgz#01150354d2be61412ff56a030b62a119283b9935" - integrity sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A== + version "7.49.3" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.49.3.tgz#576a4567f8a774830812f4855e89f5da5830435c" + integrity sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ== react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" @@ -4905,13 +5232,6 @@ react-is@^18.0.0, react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react-particles@^2.12.2: - version "2.12.2" - resolved "https://registry.yarnpkg.com/react-particles/-/react-particles-2.12.2.tgz#0ab12b72428d3b89537d3422fa8bf725175cfd4d" - integrity sha512-Bo9DdrBRPFy8uiT7BA8P36Rdmz6GhB/RG9kkWUyZGIsS8AxWyuUjpVxw9Lr23K0LVE2aenAJ0vnqEbbLDpBgQw== - dependencies: - tsparticles-engine "^2.12.0" - react-redux@^8.1.3: version "8.1.3" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" @@ -4935,19 +5255,19 @@ react-remark@^2.1.0: unified "^9.0.0" react-router-dom@^6.18.0: - version "6.20.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.20.0.tgz#7b9527a1e29c7fb90736a5f89d54ca01f40e264b" - integrity sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ== + version "6.21.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.21.3.tgz#ef3a7956a3699c7b82c21fcb3dbc63c313ed8c5d" + integrity sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g== dependencies: - "@remix-run/router" "1.13.0" - react-router "6.20.0" + "@remix-run/router" "1.14.2" + react-router "6.21.3" -react-router@6.20.0: - version "6.20.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.20.0.tgz#4275a3567ecc55f7703073158048db10096bb539" - integrity sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w== +react-router@6.21.3: + version "6.21.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.21.3.tgz#8086cea922c2bfebbb49c6594967418f1f167d70" + integrity sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg== dependencies: - "@remix-run/router" "1.13.0" + "@remix-run/router" "1.14.2" react-transition-group@^4.4.5: version "4.4.5" @@ -5049,9 +5369,9 @@ reflect.getprototypeof@^1.0.4: which-builtin-type "^1.1.3" regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: version "1.5.1" @@ -5132,6 +5452,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rifm@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4" + integrity sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -5139,7 +5464,7 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: +ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== @@ -5161,7 +5486,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.4.0, rxjs@^7.8.0: +rxjs@^7.4.0: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== @@ -5169,12 +5494,12 @@ rxjs@^7.4.0, rxjs@^7.8.0: tslib "^2.1.0" safe-array-concat@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" - integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.0.tgz#8d0cae9cb806d6d1c06e08ab13d847293ebe0692" + integrity sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" + call-bind "^1.0.5" + get-intrinsic "^1.2.2" has-symbols "^1.0.3" isarray "^2.0.5" @@ -5189,12 +5514,12 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-regex-test@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" - integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + version "1.0.2" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.2.tgz#3ba32bdb3ea35f940ee87e5087c60ee786c3f6c5" + integrity sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" + call-bind "^1.0.5" + get-intrinsic "^1.2.2" is-regex "^1.1.4" scheduler@^0.23.0: @@ -5210,12 +5535,12 @@ scrypt-js@3.0.1, scrypt-js@^3.0.0: integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== secp256k1@^4.0.1, secp256k1@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.3.tgz#c4559ecd1b8d3c1827ed2d1b94190d69ce267303" - integrity sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA== + version "4.0.4" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.4.tgz#58f0bfe1830fe777d9ca1ffc7574962a8189f8ab" + integrity sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw== dependencies: - elliptic "^6.5.4" - node-addon-api "^2.0.0" + elliptic "^6.5.7" + node-addon-api "^5.0.0" node-gyp-build "^4.2.0" seedrandom@^3.0.5: @@ -5236,14 +5561,15 @@ semver@^7.5.4: lru-cache "^6.0.0" set-function-length@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" - integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + version "1.2.0" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1" + integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w== dependencies: define-data-property "^1.1.1" - get-intrinsic "^1.2.1" + function-bind "^1.1.2" + get-intrinsic "^1.2.2" gopd "^1.0.1" - has-property-descriptors "^1.0.0" + has-property-descriptors "^1.0.1" set-function-name@^2.0.0, set-function-name@^2.0.1: version "2.0.1" @@ -5259,7 +5585,7 @@ setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== -sha.js@^2.4.0, sha.js@^2.4.8: +sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -5267,41 +5593,34 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - sharp@^0.33.1: - version "0.33.1" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.1.tgz#81e8778b9f5e2b195666cf56b2e5a110c2399d70" - integrity sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ== + version "0.33.2" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.2.tgz#fcd52f2c70effa8a02160b1bfd989a3de55f2dfb" + integrity sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ== dependencies: color "^4.2.3" detect-libc "^2.0.2" semver "^7.5.4" optionalDependencies: - "@img/sharp-darwin-arm64" "0.33.1" - "@img/sharp-darwin-x64" "0.33.1" - "@img/sharp-libvips-darwin-arm64" "1.0.0" - "@img/sharp-libvips-darwin-x64" "1.0.0" - "@img/sharp-libvips-linux-arm" "1.0.0" - "@img/sharp-libvips-linux-arm64" "1.0.0" - "@img/sharp-libvips-linux-s390x" "1.0.0" - "@img/sharp-libvips-linux-x64" "1.0.0" - "@img/sharp-libvips-linuxmusl-arm64" "1.0.0" - "@img/sharp-libvips-linuxmusl-x64" "1.0.0" - "@img/sharp-linux-arm" "0.33.1" - "@img/sharp-linux-arm64" "0.33.1" - "@img/sharp-linux-s390x" "0.33.1" - "@img/sharp-linux-x64" "0.33.1" - "@img/sharp-linuxmusl-arm64" "0.33.1" - "@img/sharp-linuxmusl-x64" "0.33.1" - "@img/sharp-wasm32" "0.33.1" - "@img/sharp-win32-ia32" "0.33.1" - "@img/sharp-win32-x64" "0.33.1" + "@img/sharp-darwin-arm64" "0.33.2" + "@img/sharp-darwin-x64" "0.33.2" + "@img/sharp-libvips-darwin-arm64" "1.0.1" + "@img/sharp-libvips-darwin-x64" "1.0.1" + "@img/sharp-libvips-linux-arm" "1.0.1" + "@img/sharp-libvips-linux-arm64" "1.0.1" + "@img/sharp-libvips-linux-s390x" "1.0.1" + "@img/sharp-libvips-linux-x64" "1.0.1" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.1" + "@img/sharp-libvips-linuxmusl-x64" "1.0.1" + "@img/sharp-linux-arm" "0.33.2" + "@img/sharp-linux-arm64" "0.33.2" + "@img/sharp-linux-s390x" "0.33.2" + "@img/sharp-linux-x64" "0.33.2" + "@img/sharp-linuxmusl-arm64" "0.33.2" + "@img/sharp-linuxmusl-x64" "0.33.2" + "@img/sharp-wasm32" "0.33.2" + "@img/sharp-win32-ia32" "0.33.2" + "@img/sharp-win32-x64" "0.33.2" shebang-command@^2.0.0: version "2.0.0" @@ -5341,6 +5660,11 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -5348,6 +5672,15 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sirv@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" + integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -5370,24 +5703,6 @@ snakecase-keys@^5.1.2, snakecase-keys@^5.4.1: snake-case "^3.0.4" type-fest "^3.12.0" -socket.io-client@^4.6.1: - version "4.7.2" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" - integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.2" - engine.io-client "~6.5.2" - socket.io-parser "~4.2.4" - -socket.io-parser@~4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" - integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -5403,17 +5718,12 @@ space-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== -"standard-error@>= 1.1.0 < 2": - version "1.1.0" - resolved "https://registry.yarnpkg.com/standard-error/-/standard-error-1.1.0.tgz#23e5168fa1c0820189e5812701a79058510d0d34" - integrity sha512-4v7qzU7oLJfMI5EltUSHCaaOd65J6S4BqKRWgzMi4EYaE5fvNabPxmAPGdxpGXqrcWjhDGI/H09CIdEuUOUeXg== - -standard-http-error@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/standard-http-error/-/standard-http-error-2.0.1.tgz#f8ae9172e3cef9cb38d2e7084a1925f57a7c34bd" - integrity sha512-DX/xPIoyXQTuY6BMZK4Utyi4l3A4vFoafsfqrU6/dO4Oe/59c7PyqPd2IQj9m+ZieDg2K3RL9xOYJsabcD9IUA== +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== dependencies: - standard-error ">= 1.1.0 < 2" + frac "~1.1.2" store2@^2.12.0: version "2.14.2" @@ -5425,10 +5735,14 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -string-similarity-js@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/string-similarity-js/-/string-similarity-js-2.1.4.tgz#73716330691946f2ebc435859aba8327afd31307" - integrity sha512-uApODZNjCHGYROzDSAdCmAHf60L/pMDHnP/yk6TAbvGg7JSPZlSto/ceCI7hZEqzc53/juU2aOJFkM2yUVTMTA== +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" @@ -5439,6 +5753,15 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.matchall@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" @@ -5500,6 +5823,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -5507,6 +5837,13 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -5544,13 +5881,13 @@ stylis@4.2.0: integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== sucrase@^3.32.0: - version "3.34.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f" - integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== dependencies: "@jridgewell/gen-mapping" "^0.3.2" commander "^4.0.0" - glob "7.1.6" + glob "^10.3.10" lines-and-columns "^1.1.6" mz "^2.7.0" pirates "^4.0.1" @@ -5585,20 +5922,20 @@ symbol-observable@^4.0.0: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== -tailwindcss@3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf" - integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w== +tailwindcss@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.0.tgz#045a9c474e6885ebd0436354e611a76af1c76839" + integrity sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA== dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" chokidar "^3.5.3" didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.2.12" + fast-glob "^3.3.0" glob-parent "^6.0.2" is-glob "^4.0.3" - jiti "^1.18.2" + jiti "^1.19.1" lilconfig "^2.1.0" micromatch "^4.0.5" normalize-path "^3.0.0" @@ -5618,6 +5955,11 @@ tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +text-encoding@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.7.0.tgz#f895e836e45990624086601798ea98e8f36ee643" + integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -5675,10 +6017,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== trough@^1.0.0: version "1.0.5" @@ -5702,10 +6044,10 @@ ts-invariant@^0.10.3: dependencies: tslib "^2.1.0" -tsconfig-paths@^3.14.2: - version "3.14.2" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" - integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.2" @@ -5717,280 +6059,6 @@ tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tsparticles-basic@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-basic/-/tsparticles-basic-2.12.0.tgz#39fd8cc31cf625b8c7c9dc517c4cb9943cafa5f4" - integrity sha512-pN6FBpL0UsIUXjYbiui5+IVsbIItbQGOlwyGV55g6IYJBgdTNXgFX0HRYZGE9ZZ9psEXqzqwLM37zvWnb5AG9g== - dependencies: - tsparticles-engine "^2.12.0" - tsparticles-move-base "^2.12.0" - tsparticles-shape-circle "^2.12.0" - tsparticles-updater-color "^2.12.0" - tsparticles-updater-opacity "^2.12.0" - tsparticles-updater-out-modes "^2.12.0" - tsparticles-updater-size "^2.12.0" - -tsparticles-engine@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-engine/-/tsparticles-engine-2.12.0.tgz#4a52a8de4ab6085180abf27f4720f47caa1455fc" - integrity sha512-ZjDIYex6jBJ4iMc9+z0uPe7SgBnmb6l+EJm83MPIsOny9lPpetMsnw/8YJ3xdxn8hV+S3myTpTN1CkOVmFv0QQ== - -tsparticles-interaction-external-attract@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-attract/-/tsparticles-interaction-external-attract-2.12.0.tgz#a223178431a76366dd25a2b25cde269585b716c7" - integrity sha512-0roC6D1QkFqMVomcMlTaBrNVjVOpyNzxIUsjMfshk2wUZDAvTNTuWQdUpmsLS4EeSTDN3rzlGNnIuuUQqyBU5w== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-bounce@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-bounce/-/tsparticles-interaction-external-bounce-2.12.0.tgz#171931e4fd98e0655dc031c7dfda973b90e821a9" - integrity sha512-MMcqKLnQMJ30hubORtdq+4QMldQ3+gJu0bBYsQr9BsThsh8/V0xHc1iokZobqHYVP5tV77mbFBD8Z7iSCf0TMQ== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-bubble@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-bubble/-/tsparticles-interaction-external-bubble-2.12.0.tgz#a9854fc92ad63e9f4b6c3704866095303b07c4c9" - integrity sha512-5kImCSCZlLNccXOHPIi2Yn+rQWTX3sEa/xCHwXW19uHxtILVJlnAweayc8+Zgmb7mo0DscBtWVFXHPxrVPFDUA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-connect@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-connect/-/tsparticles-interaction-external-connect-2.12.0.tgz#15beef5215bd084daa24b0000387de3937d000a2" - integrity sha512-ymzmFPXz6AaA1LAOL5Ihuy7YSQEW8MzuSJzbd0ES13U8XjiU3HlFqlH6WGT1KvXNw6WYoqrZt0T3fKxBW3/C3A== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-grab@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-grab/-/tsparticles-interaction-external-grab-2.12.0.tgz#f338a831f05ff275683939cec32efe790aff69e2" - integrity sha512-iQF/A947hSfDNqAjr49PRjyQaeRkYgTYpfNmAf+EfME8RsbapeP/BSyF6mTy0UAFC0hK2A2Hwgw72eT78yhXeQ== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-pause@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-pause/-/tsparticles-interaction-external-pause-2.12.0.tgz#8984dfc20d895ae30b8163cfc1901fff6e8e27d7" - integrity sha512-4SUikNpsFROHnRqniL+uX2E388YTtfRWqqqZxRhY0BrijH4z04Aii3YqaGhJxfrwDKkTQlIoM2GbFT552QZWjw== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-push@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-push/-/tsparticles-interaction-external-push-2.12.0.tgz#f3c4a18cebcfdce5cfa4febef3f7adbea458f41e" - integrity sha512-kqs3V0dgDKgMoeqbdg+cKH2F+DTrvfCMrPF1MCCUpBCqBiH+TRQpJNNC86EZYHfNUeeLuIM3ttWwIkk2hllR/Q== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-remove@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-remove/-/tsparticles-interaction-external-remove-2.12.0.tgz#1820f7e420bc0fe6c14d95b72c8122e838c1017c" - integrity sha512-2eNIrv4m1WB2VfSVj46V2L/J9hNEZnMgFc+A+qmy66C8KzDN1G8aJUAf1inW8JVc0lmo5+WKhzex4X0ZSMghBg== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-repulse@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-repulse/-/tsparticles-interaction-external-repulse-2.12.0.tgz#373a839fe517465d2b308baabf0324fce6e5eaff" - integrity sha512-rSzdnmgljeBCj5FPp4AtGxOG9TmTsK3AjQW0vlyd1aG2O5kSqFjR+FuT7rfdSk9LEJGH5SjPFE6cwbuy51uEWA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-external-slow@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-external-slow/-/tsparticles-interaction-external-slow-2.12.0.tgz#e9b50b276b848b9b59bf14ac675a519afa5fe81e" - integrity sha512-2IKdMC3om7DttqyroMtO//xNdF0NvJL/Lx7LDo08VpfTgJJozxU+JAUT8XVT7urxhaDzbxSSIROc79epESROtA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-particles-attract@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-particles-attract/-/tsparticles-interaction-particles-attract-2.12.0.tgz#ab784d2de5f1bad7c6ebf859a8ccf962b8233fd4" - integrity sha512-Hl8qwuwF9aLq3FOkAW+Zomu7Gb8IKs6Y3tFQUQScDmrrSCaeRt2EGklAiwgxwgntmqzL7hbMWNx06CHHcUQKdQ== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-particles-collisions@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-particles-collisions/-/tsparticles-interaction-particles-collisions-2.12.0.tgz#4261133737279ca46e682a270d6213e3cd97d8be" - integrity sha512-Se9nPWlyPxdsnHgR6ap4YUImAu3W5MeGKJaQMiQpm1vW8lSMOUejI1n1ioIaQth9weKGKnD9rvcNn76sFlzGBA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-interaction-particles-links@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-interaction-particles-links/-/tsparticles-interaction-particles-links-2.12.0.tgz#2716106bf9f29ef3bb64600c6b56ec895553d2b1" - integrity sha512-e7I8gRs4rmKfcsHONXMkJnymRWpxHmeaJIo4g2NaDRjIgeb2AcJSWKWZvrsoLnm7zvaf/cMQlbN6vQwCixYq3A== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-move-base@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-move-base/-/tsparticles-move-base-2.12.0.tgz#22a0bf5c5a6a21db868c31d3172c1434e3e9e45a" - integrity sha512-oSogCDougIImq+iRtIFJD0YFArlorSi8IW3HD2gO3USkH+aNn3ZqZNTqp321uB08K34HpS263DTbhLHa/D6BWw== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-move-parallax@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-move-parallax/-/tsparticles-move-parallax-2.12.0.tgz#67f6bbb00c61a003056acc77c0c4d0245a519763" - integrity sha512-58CYXaX8Ih5rNtYhpnH0YwU4Ks7gVZMREGUJtmjhuYN+OFr9FVdF3oDIJ9N6gY5a5AnAKz8f5j5qpucoPRcYrQ== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-particles.js@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-particles.js/-/tsparticles-particles.js-2.12.0.tgz#f9cb0326e083c8abe19e9d0a8ea6bcd0ce728447" - integrity sha512-LyOuvYdhbUScmA4iDgV3LxA0HzY1DnOwQUy3NrPYO393S2YwdDjdwMod6Btq7EBUjg9FVIh+sZRizgV5elV2dg== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-plugin-easing-quad@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-plugin-easing-quad/-/tsparticles-plugin-easing-quad-2.12.0.tgz#8dccb2a7abb0cc4d5ab76c6bdecc267d77bb2dab" - integrity sha512-2mNqez5pydDewMIUWaUhY5cNQ80IUOYiujwG6qx9spTq1D6EEPLbRNAEL8/ecPdn2j1Um3iWSx6lo340rPkv4Q== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-shape-circle@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-shape-circle/-/tsparticles-shape-circle-2.12.0.tgz#52757d222145c3f80b02d3c51e6158aee0af024c" - integrity sha512-L6OngbAlbadG7b783x16ns3+SZ7i0SSB66M8xGa5/k+YcY7zm8zG0uPt1Hd+xQDR2aNA3RngVM10O23/Lwk65Q== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-shape-image@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-shape-image/-/tsparticles-shape-image-2.12.0.tgz#02273740d754a872f77434af8d57b5486b920c91" - integrity sha512-iCkSdUVa40DxhkkYjYuYHr9MJGVw+QnQuN5UC+e/yBgJQY+1tQL8UH0+YU/h0GHTzh5Sm+y+g51gOFxHt1dj7Q== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-shape-line@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-shape-line/-/tsparticles-shape-line-2.12.0.tgz#f4af1820eade84021f33d1c949a26dc791424d8b" - integrity sha512-RcpKmmpKlk+R8mM5wA2v64Lv1jvXtU4SrBDv3vbdRodKbKaWGGzymzav1Q0hYyDyUZgplEK/a5ZwrfrOwmgYGA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-shape-polygon@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-shape-polygon/-/tsparticles-shape-polygon-2.12.0.tgz#ab01b27f7c97f5fc1843ac0eef91e8dfa79bb3a2" - integrity sha512-5YEy7HVMt1Obxd/jnlsjajchAlYMr9eRZWN+lSjcFSH6Ibra7h59YuJVnwxOxAobpijGxsNiBX0PuGQnB47pmA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-shape-square@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-shape-square/-/tsparticles-shape-square-2.12.0.tgz#53916baea090a6af1863cb8ec5f77bf0e993ef2a" - integrity sha512-33vfajHqmlODKaUzyPI/aVhnAOT09V7nfEPNl8DD0cfiNikEuPkbFqgJezJuE55ebtVo7BZPDA9o7GYbWxQNuw== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-shape-star@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-shape-star/-/tsparticles-shape-star-2.12.0.tgz#609f9eb7025ad30931ee115b17cee4cf5b91a13e" - integrity sha512-4sfG/BBqm2qBnPLASl2L5aBfCx86cmZLXeh49Un+TIR1F5Qh4XUFsahgVOG0vkZQa+rOsZPEH04xY5feWmj90g== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-shape-text@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-shape-text/-/tsparticles-shape-text-2.12.0.tgz#1edc78c174210be4152a095a21136fc420d125d5" - integrity sha512-v2/FCA+hyTbDqp2ymFOe97h/NFb2eezECMrdirHWew3E3qlvj9S/xBibjbpZva2gnXcasBwxn0+LxKbgGdP0rA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-slim@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-slim/-/tsparticles-slim-2.12.0.tgz#21b9474e358830d9eb8ec38a47c32d71b7ec7f02" - integrity sha512-27w9aGAAAPKHvP4LHzWFpyqu7wKyulayyaZ/L6Tuuejy4KP4BBEB4rY5GG91yvAPsLtr6rwWAn3yS+uxnBDpkA== - dependencies: - tsparticles-basic "^2.12.0" - tsparticles-engine "^2.12.0" - tsparticles-interaction-external-attract "^2.12.0" - tsparticles-interaction-external-bounce "^2.12.0" - tsparticles-interaction-external-bubble "^2.12.0" - tsparticles-interaction-external-connect "^2.12.0" - tsparticles-interaction-external-grab "^2.12.0" - tsparticles-interaction-external-pause "^2.12.0" - tsparticles-interaction-external-push "^2.12.0" - tsparticles-interaction-external-remove "^2.12.0" - tsparticles-interaction-external-repulse "^2.12.0" - tsparticles-interaction-external-slow "^2.12.0" - tsparticles-interaction-particles-attract "^2.12.0" - tsparticles-interaction-particles-collisions "^2.12.0" - tsparticles-interaction-particles-links "^2.12.0" - tsparticles-move-base "^2.12.0" - tsparticles-move-parallax "^2.12.0" - tsparticles-particles.js "^2.12.0" - tsparticles-plugin-easing-quad "^2.12.0" - tsparticles-shape-circle "^2.12.0" - tsparticles-shape-image "^2.12.0" - tsparticles-shape-line "^2.12.0" - tsparticles-shape-polygon "^2.12.0" - tsparticles-shape-square "^2.12.0" - tsparticles-shape-star "^2.12.0" - tsparticles-shape-text "^2.12.0" - tsparticles-updater-color "^2.12.0" - tsparticles-updater-life "^2.12.0" - tsparticles-updater-opacity "^2.12.0" - tsparticles-updater-out-modes "^2.12.0" - tsparticles-updater-rotate "^2.12.0" - tsparticles-updater-size "^2.12.0" - tsparticles-updater-stroke-color "^2.12.0" - -tsparticles-updater-color@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-updater-color/-/tsparticles-updater-color-2.12.0.tgz#56d2a45f42a1578fd3b75d91baa60f2744db26a4" - integrity sha512-KcG3a8zd0f8CTiOrylXGChBrjhKcchvDJjx9sp5qpwQK61JlNojNCU35xoaSk2eEHeOvFjh0o3CXWUmYPUcBTQ== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-updater-life@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-updater-life/-/tsparticles-updater-life-2.12.0.tgz#00e9929b6011821bdf2b37f27d211fdaf74e32d8" - integrity sha512-J7RWGHAZkowBHpcLpmjKsxwnZZJ94oGEL2w+wvW1/+ZLmAiFFF6UgU0rHMC5CbHJT4IPx9cbkYMEHsBkcRJ0Bw== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-updater-opacity@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-updater-opacity/-/tsparticles-updater-opacity-2.12.0.tgz#4e822dc43d80a3964e09056ff773214b619b18fc" - integrity sha512-YUjMsgHdaYi4HN89LLogboYcCi1o9VGo21upoqxq19yRy0hRCtx2NhH22iHF/i5WrX6jqshN0iuiiNefC53CsA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-updater-out-modes@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-updater-out-modes/-/tsparticles-updater-out-modes-2.12.0.tgz#14b47c5dc7b9f2a485c819f03634656466249e5e" - integrity sha512-owBp4Gk0JNlSrmp12XVEeBroDhLZU+Uq3szbWlHGSfcR88W4c/0bt0FiH5bHUqORIkw+m8O56hCjbqwj69kpOQ== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-updater-rotate@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-updater-rotate/-/tsparticles-updater-rotate-2.12.0.tgz#fa2a977fbdcb13cbe2fa9632d6ab8766e1f83167" - integrity sha512-waOFlGFmEZOzsQg4C4VSejNVXGf4dMf3fsnQrEROASGf1FCd8B6WcZau7JtXSTFw0OUGuk8UGz36ETWN72DkCw== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-updater-size@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-updater-size/-/tsparticles-updater-size-2.12.0.tgz#c9e80c7a91487f2b58d2db0acffbffee517d51d5" - integrity sha512-B0yRdEDd/qZXCGDL/ussHfx5YJ9UhTqNvmS5X2rR2hiZhBAE2fmsXLeWkdtF2QusjPeEqFDxrkGiLOsh6poqRA== - dependencies: - tsparticles-engine "^2.12.0" - -tsparticles-updater-stroke-color@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/tsparticles-updater-stroke-color/-/tsparticles-updater-stroke-color-2.12.0.tgz#7bb7879eb8970b9525899cc9478840ae14675979" - integrity sha512-MPou1ZDxsuVq6SN1fbX+aI5yrs6FyP2iPCqqttpNbWyL+R6fik1rL0ab/x02B57liDXqGKYomIbBQVP3zUTW1A== - dependencies: - tsparticles-engine "^2.12.0" - tweetnacl-util@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" @@ -6187,10 +6255,10 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +utility-types@^3.10.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" + integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== vfile-message@^2.0.0: version "2.0.4" @@ -6210,10 +6278,10 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" -viem@^1.12.2: - version "1.19.15" - resolved "https://registry.yarnpkg.com/viem/-/viem-1.19.15.tgz#0f2307632fa0ef10dfab2d8fdd71fbb842a0a4f5" - integrity sha512-rc87AkyrUUsoOAgMNYP+X/wN4GYwbhP87DkmsqQCYKxxQyzTX0+yliKs6Bxljbjr8ybU72GOb12Oyus6393AjQ== +viem@1.x: + version "1.21.4" + resolved "https://registry.yarnpkg.com/viem/-/viem-1.21.4.tgz#883760e9222540a5a7e0339809202b45fe6a842d" + integrity sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ== dependencies: "@adraffy/ens-normalize" "1.10.0" "@noble/curves" "1.2.0" @@ -6224,31 +6292,29 @@ viem@^1.12.2: isows "1.0.3" ws "8.13.0" -watchpack@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - web-namespaces@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== +webpack-bundle-analyzer@4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz#84b7473b630a7b8c21c741f81d8fe4593208b454" + integrity sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ== dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" + "@discoveryjs/json-ext" "0.5.7" + acorn "^8.0.4" + acorn-walk "^8.0.0" + commander "^7.2.0" + debounce "^1.2.1" + escape-string-regexp "^4.0.0" + gzip-size "^6.0.0" + html-escaper "^2.0.2" + is-plain-object "^5.0.0" + opener "^1.5.2" + picocolors "^1.0.0" + sirv "^2.0.3" + ws "^7.3.1" which-boxed-primitive@^1.0.2: version "1.0.2" @@ -6307,6 +6373,25 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -6316,6 +6401,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -6331,25 +6425,23 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== -ws@^7: +ws@^7, ws@^7.3.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^8.13.0: - version "8.15.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.15.1.tgz#271ba33a45ca0cc477940f7f200cd7fba7ee1997" - integrity sha512-W5OZiCjXEmk0yZ66ZN82beM5Sz7l7coYxpRkzS+p9PP+ToQry8szKh+61eNktr7EA9DOwvFGhfC605jDHbP6QQ== - -ws@~8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== - -xmlhttprequest-ssl@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" - integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== +xlsx@^0.18.5: + version "0.18.5" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0" + integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ== + dependencies: + adler-32 "~1.3.0" + cfb "~1.2.1" + codepage "~1.15.0" + crc-32 "~1.2.1" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" xstream@^11.14.0: version "11.14.0" @@ -6409,6 +6501,11 @@ yargs@^16.1.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yarn@^1.22.19: + version "1.22.21" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.21.tgz#1959a18351b811cdeedbd484a8f86c3cc3bbaf72" + integrity sha512-ynXaJsADJ9JiZ84zU25XkPGOvVMmZ5b7tmTSpKURYwgELdjucAOydqIOrOfTxVYcNXe91xvLZwcRh68SR3liCg== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 000000000..10f15032a --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.21 + +WORKDIR /app + +COPY go.mod . +COPY go.sum . +COPY . . + +RUN go mod tidy + +EXPOSE 1323 + +RUN ["cp", "example.yaml", "config.yaml"] + +RUN go build -o app + +CMD ["./app"] diff --git a/server/README.md b/server/README.md index a1eb97ea5..d2d15880b 100644 --- a/server/README.md +++ b/server/README.md @@ -14,6 +14,8 @@ This module provides backend code for [Resolute](https://resolute.vitwit.com), a For configuration we use YAML file format. To configure `backend` and `database`, you need to add `config.yaml` file. Reference `example.yaml`. +To get the transactions history you need to add your chain details in `server/networks.json` file. + ## Database setup This project uses Postgres database. Before starting the server make sure to initialize database tables. You can find schema [Here](https://github.com/vitwit/resolute/server/schema/schema.sql). diff --git a/server/clients/chain.go b/server/clients/chain.go new file mode 100644 index 000000000..ce5b1c80b --- /dev/null +++ b/server/clients/chain.go @@ -0,0 +1,72 @@ +package clients + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/vitwit/resolute/server/config" +) + +func GetStatus(url string, chainId string) (bool, error) { + config, err := config.ParseConfig() + if err != nil { + log.Fatal(err) + } + + chanDetails := GetChain(chainId) + + if chanDetails == nil { + return false, errors.New("unable to get chain details") + } + + urlString := fmt.Sprintf("%s/cosmos/auth/v1beta1/params", url) + + // Create a new request to the new URL + req, err := http.NewRequest("GET", urlString, nil) + if err != nil { + return false, err + } + + if chanDetails.SourceEnd == "mintscan" { + authorizationToken := fmt.Sprintf("Bearer %s", config.MINTSCAN_TOKEN.Token) + req.Header.Add("Authorization", authorizationToken) + } + + if chanDetails.SourceEnd == "numia" { + bearerToken := config.NUMIA_BEARER_TOKEN.Token + var authorization = "Bearer " + bearerToken + + req.Header.Add("Authorization", authorization) + } + + // Perform the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + // Copy the response to the original response writer + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + + return false, err + } + + var result map[string]interface{} + //Convert the body to type string + if err := json.Unmarshal(body, &result); err != nil { + return true, err + } + + if resp.StatusCode == 200 { + return true, nil + } + + return false, nil +} diff --git a/server/clients/coingecko/client.go b/server/clients/coingecko/client.go index 92b6a88d7..d4e073b63 100644 --- a/server/clients/coingecko/client.go +++ b/server/clients/coingecko/client.go @@ -39,3 +39,24 @@ func (c Client) GetPrice(ids []string) (map[string]interface{}, error) { return result, nil } + +func (c Client) SearchCoingeckoId(denom string) (map[string]interface{}, error) { + resp, err := http.Get(fmt.Sprintf("%s/search?query=%s", c.API, denom)) + if err != nil { + return nil, err + } + + //We Read the response body on the line below. + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var result map[string]interface{} + //Convert the body to type string + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/server/clients/redis.go b/server/clients/redis.go new file mode 100644 index 000000000..553c17ceb --- /dev/null +++ b/server/clients/redis.go @@ -0,0 +1,106 @@ +package clients + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/redis/go-redis/v9" + "github.com/vitwit/resolute/server/config" +) + +// RedisClient is the Redis client instance +var RedisClient *redis.Client +var ctx = context.Background() + +// InitializeRedis initializes the Redis client +func InitializeRedis(addr string, password string, db int) { + RedisClient = redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: db, + }) + + _, err := RedisClient.Ping(ctx).Result() + if err != nil { + log.Fatalf("Could not connect to Redis: %v", err) + } +} + +// SetValue sets a value in Redis +func SetValue(key string, value string) error { + err := RedisClient.Set(ctx, key, value, 0).Err() + if err != nil { + return err + } + return nil +} + +// GetValue gets a value from Redis +func GetValue(key string) (string, error) { + val, err := RedisClient.Get(ctx, key).Result() + if err == redis.Nil { + return "", nil + } else if err != nil { + return "", err + } + return val, nil +} + +func GetChain(chainId string) *config.ChainConfig { + data, err := GetValue("chains") + if err != nil { + fmt.Println("Error fetching chains from Redis:", err) + return nil + } + + if data == "" { + fmt.Println("No chains found in Redis") + return nil + } + + var chains []config.ChainConfig + + e := json.Unmarshal([]byte(data), &chains) + if e != nil { + fmt.Println("Error while unmarshal chain info ", e.Error()) + return nil + } + + for _, c := range chains { + if c.ChainId == chainId { + return &c + } + } + + return nil +} + +func GetChains() []*config.ChainConfig { + + data, err := GetValue("chains") + if err != nil { + fmt.Println("Error fetching chains from Redis:", err) + return nil + } + + if data == "" { + fmt.Println("No chains found in Redis") + return nil + } + + var chains []config.ChainConfig + e := json.Unmarshal([]byte(data), &chains) + if e != nil { + fmt.Println("Error while unmarshal chain info ", e.Error()) + return nil + } + + chainPointers := make([]*config.ChainConfig, len(chains)) + for i := range chains { + chainPointers[i] = &chains[i] + } + + return chainPointers +} diff --git a/server/config/config.go b/server/config/config.go index cde4ee645..e186ee503 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,16 +1,23 @@ package config import ( + "encoding/json" "errors" "fmt" + "log" + "os" + "path/filepath" "github.com/spf13/viper" ) type Config struct { - DB DBConfig `mapstructure:"database"` - API APIConfig `mapstructure:"api"` - COINGECKO CoingeckoConfig `mapstructure:"coingecko"` + DB DBConfig `mapstructure:"database"` + API APIConfig `mapstructure:"api"` + COINGECKO CoingeckoConfig `mapstructure:"coingecko"` + NUMIA_BEARER_TOKEN NumiaBearerToken `mapstructure:"numiaBearerToken"` + MINTSCAN_TOKEN MintscanToken `mapstructure:"mintscanToken"` + REDIS_URI string `mapstructure:"redisUri"` } type DBConfig struct { @@ -29,6 +36,14 @@ type CoingeckoConfig struct { URI string `yaml:"uri"` } +type NumiaBearerToken struct { + Token string `yaml:"token"` +} + +type MintscanToken struct { + Token string `yaml:"token"` +} + func ParseConfig() (Config, error) { viper.SetConfigName("config") viper.SetConfigType("yaml") @@ -61,6 +76,13 @@ func ParseConfig() (Config, error) { cfg.COINGECKO = CoingeckoConfig{ URI: viper.GetString("production.coingecko.uri"), } + cfg.NUMIA_BEARER_TOKEN = NumiaBearerToken{ + Token: viper.GetString("production.numiaBearerToken"), + } + cfg.MINTSCAN_TOKEN = MintscanToken{ + Token: viper.GetString("production.mintscanToken"), + } + cfg.REDIS_URI = viper.GetString("production.redisUri") } case "dev": @@ -78,6 +100,13 @@ func ParseConfig() (Config, error) { cfg.COINGECKO = CoingeckoConfig{ URI: viper.GetString("production.coingecko.uri"), } + cfg.NUMIA_BEARER_TOKEN = NumiaBearerToken{ + Token: viper.GetString("dev.numiaBearerToken"), + } + cfg.MINTSCAN_TOKEN = MintscanToken{ + Token: viper.GetString("production.mintscanToken"), + } + cfg.REDIS_URI = viper.GetString("production.redisUri") } default: @@ -86,3 +115,32 @@ func ParseConfig() (Config, error) { return cfg, nil } + +type ChainConfig struct { + ChainId string `json:"chainId"` + RestURIs []string `json:"restURIs"` + RestURI string `json:"restURI"` + RpcURI string `json:"rpcURI"` + CheckStatus bool `json:"checkStatus"` + SourceEnd string `json:"sourceEnd"` +} + +func GetChainAPIs() []*ChainConfig { + wd, _ := os.Getwd() + filePath := filepath.Join(wd, "/", "networks.json") + jsonData, err := os.ReadFile(filePath) + if err != nil { + log.Fatalln("Error reading JSON file:", err) + return nil + } + + var data []*ChainConfig + + err = json.Unmarshal([]byte(jsonData), &data) + if err != nil { + log.Fatalln("Error unmarshaling JSON data:", err) + return nil + } + + return data +} diff --git a/server/cron/chainURI.go b/server/cron/chainURI.go new file mode 100644 index 000000000..2cbc2d118 --- /dev/null +++ b/server/cron/chainURI.go @@ -0,0 +1,34 @@ +package cron + +import ( + "encoding/json" + "log" + + "github.com/vitwit/resolute/server/clients" + "github.com/vitwit/resolute/server/config" +) + +func (c *Cron) StartCheckUris() { + data := config.GetChainAPIs() + for _, c := range data { + for _, u := range c.RestURIs { + if c.CheckStatus { + status, _ := clients.GetStatus(u, c.ChainId) + if status { + c.RestURI = u + break + } + } else { + c.RestURI = u + } + + } + } + + bytes, err := json.Marshal(data) + if err != nil { + log.Println(err) + } + + clients.SetValue("chains", string(bytes)) +} diff --git a/server/cron/cron.go b/server/cron/cron.go index 788083d5d..444ff5a9c 100644 --- a/server/cron/cron.go +++ b/server/cron/cron.go @@ -3,6 +3,7 @@ package cron import ( "database/sql" "encoding/json" + "errors" "log" "time" @@ -35,11 +36,17 @@ func (c *Cron) Start() error { cron := cron.New() // Every 15 minute - cron.AddFunc("15 * * * * *", func() { - c.CoinsPriceInfoList() + cron.AddFunc("0 */15 * * * *", func() { + go c.CoinsPriceInfoList() log.Println("successfully saved price information list") }) + // Every 15 minute + cron.AddFunc("0 */15 * * * *", func() { + go c.StartCheckUris() + log.Println("successfully saved chain information list") + }) + go cron.Start() return nil @@ -55,7 +62,7 @@ func (c *Cron) CoinsPriceInfoList() { } coinIds := make([]string, 0) - coinNameToDenom := make(map[string]string, 0) + coinNameToDenom := make(map[string][]string, 0) for rows.Next() { var priceInfo schema.PriceInfo @@ -68,7 +75,7 @@ func (c *Cron) CoinsPriceInfoList() { coinIds = append(coinIds, priceInfo.CoingeckoName) - coinNameToDenom[priceInfo.CoingeckoName] = priceInfo.Denom + coinNameToDenom[priceInfo.CoingeckoName] = append(coinNameToDenom[priceInfo.CoingeckoName], priceInfo.Denom) } if len(coinIds) > 0 { @@ -81,10 +88,66 @@ func (c *Cron) CoinsPriceInfoList() { for k, v := range priceInfo { val, _ := json.Marshal(v) - _, err = c.db.Exec("UPDATE price_info SET info=$1,last_updated=$2 WHERE denom=$3", val, time.Now(), coinNameToDenom[k]) - if err != nil { - utils.ErrorLogger.Printf("failed to update price information for denom = %s : %s\n", k, err.Error()) + for _, denom := range coinNameToDenom[k] { + _, err = c.db.Exec("UPDATE price_info SET info=$1,last_updated=$2 WHERE denom=$3", val, time.Now(), denom) + if err != nil { + utils.ErrorLogger.Printf("failed to update price information for denom = %s : %s\n", k, err.Error()) + } } + } } } + +/** +* if price info not found on the db, fetch price info based on denom and store it. + */ + +type CoinSearchResult struct { + Id string + Name string + ApiSymbol string + Symbol string + MarketCapRank int + Thumb string + Large string +} + +func GetNSavePriceInfoFromCoin(uri string, denom string) (map[string]interface{}, error) { + + client1 := coingecko.NewClient(uri, []string{"usd"}) + + Ids, err := client1.SearchCoingeckoId(denom) + if err != nil { + utils.ErrorLogger.Printf("failed to fetch price information %s\n", err.Error()) + return nil, err + } + + if Ids["coins"] == nil { + return nil, errors.New("invalid denom") + } + + var idsArr []CoinSearchResult + bytes, mErr := json.Marshal(Ids["coins"]) + if mErr != nil { + return nil, errors.New("unable to marshal coin gecko ids") + } + + uMerr := json.Unmarshal(bytes, &idsArr) + if uMerr != nil { + return nil, errors.New("unable to un marshal coin gecko ids") + } + + if len(idsArr) <= 0 { + return nil, errors.New("no coingecko ids found") + } + + client2 := coingecko.NewClient(uri, []string{"usd"}) + + priceInfo, err := client2.GetPrice([]string{idsArr[0].Id}) + if err != nil { + utils.ErrorLogger.Printf("failed to fetch price information %s\n", err.Error()) + } + + return priceInfo, nil +} diff --git a/server/example.yaml b/server/example.yaml index 05760bfbe..1a69a7589 100644 --- a/server/example.yaml +++ b/server/example.yaml @@ -11,10 +11,11 @@ production: port: 1323 coingecko: uri: "https://api.coingecko.com/api/v3/" + redisUri: "localhost:6379" dev: database: - host: "localhost" + host: "database" port: 5432 user: "alice" password: "password" @@ -23,3 +24,4 @@ dev: port: 1323 coingecko: uri: "https://api.coingecko.com/api/v3/" + redisUri: "localhost:6379" diff --git a/server/go.mod b/server/go.mod index c5d84b14a..958cdb329 100644 --- a/server/go.mod +++ b/server/go.mod @@ -12,10 +12,14 @@ require ( ) require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/labstack/echo v3.3.10+incompatible // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect @@ -23,6 +27,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.5.3 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/server/go.sum b/server/go.sum index 83f80e5b3..6edf34edc 100644 --- a/server/go.sum +++ b/server/go.sum @@ -38,7 +38,11 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -49,6 +53,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -133,6 +139,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= +github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilmdE= github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= @@ -159,6 +167,8 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= +github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= diff --git a/server/handler/multisig.go b/server/handler/multisig.go index 8bb3217e1..42adc5aa6 100644 --- a/server/handler/multisig.go +++ b/server/handler/multisig.go @@ -53,6 +53,14 @@ func (h *Handler) CreateMultisigAccount(c echo.Context) error { }) } + if strings.Contains(err.Error(), "value too long") { + return c.JSON(http.StatusInternalServerError, model.ErrorResponse{ + Status: "error", + Message: "Multisig name cannot contain more than 100 characters", + Log: err.Error(), + }) + } + return c.JSON(http.StatusInternalServerError, model.ErrorResponse{ Status: "error", Message: "failed to create multisig account", diff --git a/server/handler/price.go b/server/handler/price.go index f91896209..4c1af84c9 100644 --- a/server/handler/price.go +++ b/server/handler/price.go @@ -2,12 +2,19 @@ package handler import ( "database/sql" + "encoding/json" "fmt" + "log" "net/http" + "time" "github.com/labstack/echo/v4" + "github.com/vitwit/resolute/server/cron" "github.com/vitwit/resolute/server/model" "github.com/vitwit/resolute/server/schema" + "github.com/vitwit/resolute/server/utils" + + "github.com/vitwit/resolute/server/config" ) func (h *Handler) GetTokensInfo(c echo.Context) error { @@ -69,11 +76,33 @@ func (h *Handler) GetTokenInfo(c echo.Context) error { &priceInfo.LastUpdated, &priceInfo.Info, ); err != nil { + fmt.Printf("Error - %v ", err.Error()) if sql.ErrNoRows.Error() == row.Scan().Error() { - return c.JSON(http.StatusBadRequest, model.ErrorResponse{ - Status: "error", - Message: fmt.Sprintf("no token info: %s", denom), - Log: row.Scan().Error(), + config, err := config.ParseConfig() + if err != nil { + log.Fatal(err) + } + + priceInfo, err1 := cron.GetNSavePriceInfoFromCoin(config.COINGECKO.URI, denom) + if err1 != nil { + return c.JSON(http.StatusBadRequest, model.ErrorResponse{ + Status: "error", + Message: fmt.Sprintf("no token info: %s", denom), + Log: err1.Error(), + }) + } else { + for k, v := range priceInfo { + val, _ := json.Marshal(v) + _, err = h.DB.Exec("INSERT INTO price_info(denom,coingecko_name,enabled,last_updated,info) values($1, $2, $3, $4, $5)", denom, k, true, time.Now(), val) + if err != nil { + utils.ErrorLogger.Printf("failed to update price information for denom = %s : %s\n", k, err.Error()) + } + } + } + + return c.JSON(http.StatusOK, model.SuccessResponse{ + Status: "success", + Data: priceInfo, }) } diff --git a/server/handler/recent_transactions.go b/server/handler/recent_transactions.go new file mode 100644 index 000000000..2a8fa6efd --- /dev/null +++ b/server/handler/recent_transactions.go @@ -0,0 +1,405 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sort" + "time" + + "github.com/labstack/echo/v4" + "github.com/vitwit/resolute/server/clients" + "github.com/vitwit/resolute/server/config" + "github.com/vitwit/resolute/server/model" + "github.com/vitwit/resolute/server/txn_types" + "github.com/vitwit/resolute/server/utils" +) + +type ChainAddress struct { + ChainId string `json:"chain_id"` + Address string `json:"address"` +} + +type GetRecentTransactionsRequest struct { + Addresses []ChainAddress `json:"addresses"` +} + +type TxnsData struct { + Data interface{} `json:"data"` + Total string `json:"total"` +} + +func (h *Handler) GetRecentTransactions(c echo.Context) error { + module := c.QueryParams().Get("module") + req := &GetRecentTransactionsRequest{} + + if err := c.Bind(req); err != nil { + return c.JSON(http.StatusBadRequest, model.ErrorResponse{ + Status: "error", + Message: err.Error(), + }) + } + result := []txn_types.ParsedTxn{} + for i := 0; i < len(req.Addresses); i++ { + if module == "bank" { + moduleNames := []string{"bank", "transfer"} + for _, moduleName := range moduleNames { + res, err := getNetworkRecentTransactions(req.Addresses[i].ChainId, moduleName, req.Addresses[i].Address) + if err == nil { + parsedTxns, err := GetParsedTransactions(*res, req.Addresses[i].ChainId) + if err == nil { + result = append(result, parsedTxns...) + } + } + } + + } else { + res, err := getNetworkRecentTransactions(req.Addresses[i].ChainId, module, req.Addresses[i].Address) + if err == nil { + parsedTxns, err := GetParsedTransactions(*res, req.Addresses[i].ChainId) + if err == nil { + result = append(result, parsedTxns...) + } + } + } + + } + + sort.Sort(ByTimestamp(result)) + + recentTxns := result[:min(5, len(result))] + + return c.JSON(http.StatusOK, model.SuccessResponse{ + Status: "success", + Data: recentTxns, + }) +} + +func (h *Handler) GetAllTransactions(c echo.Context) error { + chainId := c.Param("chainId") + address := c.Param("address") + limit := c.QueryParams().Get("limit") + offset := c.QueryParams().Get("offset") + req := &GetRecentTransactionsRequest{} + + if err := c.Bind(req); err != nil { + return c.JSON(http.StatusBadRequest, model.ErrorResponse{ + Status: "error", + Message: err.Error(), + }) + } + result := []txn_types.ParsedTxn{} + res, err := getTransactions(chainId, address, limit, offset) + if err != nil { + return c.JSON(http.StatusBadRequest, model.ErrorResponse{ + Status: "error", + Message: "Failed to fetch transactions", + }) + } + parsedTxns, err := GetParsedTransactions(*res, chainId) + if err != nil { + log.Printf("Error parsing transactions: %v", err) + return c.JSON(http.StatusInternalServerError, model.ErrorResponse{ + Status: "error", + Message: "Failed to parse transactions", + }) + } + result = append(result, parsedTxns...) + + responseData := TxnsData{ + Data: result, + Total: res.Pagination.Total, + } + return c.JSON(http.StatusOK, model.SuccessResponse{ + Status: "success", + Data: responseData, + }) +} + +func (h *Handler) GetTxHash(c echo.Context) error { + txhash := c.Param("txhash") + + chains := clients.GetChains() + + for _, chain := range chains { + res, err := getTransaction(chain.ChainId, txhash) + if err == nil { + parsedTxns, err := GetParsedTransaction(*res, chain.ChainId) + if err != nil { + log.Printf("Error parsing transactions: %v", err) + return c.JSON(http.StatusInternalServerError, model.ErrorResponse{ + Status: "error", + Message: "Failed to parse transactions", + }) + } + + responseData := TxnsData{ + Data: parsedTxns, + } + return c.JSON(http.StatusOK, model.SuccessResponse{ + Status: "success", + Data: responseData, + }) + } + } + + return c.JSON(http.StatusOK, model.SuccessResponse{ + Status: "success", + Data: nil, + }) +} + +func (h *Handler) GetChainTxHash(c echo.Context) error { + chainId := c.Param("chainId") + txhash := c.Param("txhash") + + res, err := getTransaction(chainId, txhash) + if err != nil { + return c.JSON(http.StatusBadRequest, model.ErrorResponse{ + Status: "error", + Message: "Failed to fetch transactions", + }) + } + parsedTxns, err := GetParsedTransaction(*res, chainId) + if err != nil { + log.Printf("Error parsing transactions: %v", err) + return c.JSON(http.StatusInternalServerError, model.ErrorResponse{ + Status: "error", + Message: "Failed to parse transactions", + }) + } + + responseData := TxnsData{ + Data: parsedTxns, + } + return c.JSON(http.StatusOK, model.SuccessResponse{ + Status: "success", + Data: responseData, + }) +} + +func getTransactions(chainId string, address string, limit string, offset string) (*txn_types.TransactionResponses, error) { + config, err := config.ParseConfig() + if err != nil { + log.Fatal(err) + } + + chainConfig, err := utils.GetChainAPIs(chainId) + var networkURIs = chainConfig.RestURIs + + if err == nil { + requestURI := utils.CreateAllTxnsRequestURI(networkURIs[0], address, limit, offset) + fmt.Println("creq request URI", requestURI) + req, _ := http.NewRequest("GET", requestURI, nil) + if err != nil { + return nil, err + } + + if chainConfig.SourceEnd == "mintscan" { + + authorizationToken := fmt.Sprintf("Bearer %s", config.MINTSCAN_TOKEN.Token) + req.Header.Add("Authorization", authorizationToken) // Change this to your actual token + } + + if chainConfig.SourceEnd == "numia" { + bearerToken := config.NUMIA_BEARER_TOKEN.Token + var authorization = "Bearer " + bearerToken + + req.Header.Add("Authorization", authorization) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var result txn_types.TransactionResponses + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return &result, nil + } + return nil, err +} + +func getTransaction(chainId string, txhash string) (*txn_types.SingleTxnRes, error) { + config, err := config.ParseConfig() + if err != nil { + log.Fatal(err) + } + + chainConfig, err := utils.GetChainAPIs(chainId) + var networkURIs = chainConfig.RestURIs + + if err == nil { + requestURI := utils.CreateTxnRequestURI(networkURIs[0], txhash) + fmt.Println("creq request URI", requestURI) + req, _ := http.NewRequest("GET", requestURI, nil) + if err != nil { + return nil, err + } + + if chainConfig.SourceEnd == "mintscan" { + authorizationToken := fmt.Sprintf("Bearer %s", config.MINTSCAN_TOKEN.Token) + req.Header.Add("Authorization", authorizationToken) // Change this to your actual token + } + + if chainConfig.SourceEnd == "numia" { + bearerToken := config.NUMIA_BEARER_TOKEN.Token + var authorization = "Bearer " + bearerToken + + req.Header.Add("Authorization", authorization) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Check if the account exists + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("txn not found on chain hash %s", chainId) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var result txn_types.SingleTxnRes + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return &result, nil + } + return nil, err +} + +func getNetworkRecentTransactions(chainId string, module string, address string) (*txn_types.TransactionResponses, error) { + config, err := config.ParseConfig() + if err != nil { + log.Fatal(err) + } + + chainConfig, err := utils.GetChainAPIs(chainId) + var networkURIs = chainConfig.RestURIs + + fmt.Println("===============================================") + + if err == nil { + requestURI := utils.CreateRequestURI(networkURIs[0], module, address) + req, _ := http.NewRequest("GET", requestURI, nil) + + if chainConfig.SourceEnd == "mintscan" { + fmt.Println("ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd") + fmt.Println(config.MINTSCAN_TOKEN) + authorizationToken := fmt.Sprintf("Bearer %s", config.MINTSCAN_TOKEN.Token) + req.Header.Add("Authorization", authorizationToken) // Change this to your actual token + } + + if chainConfig.SourceEnd == "numia" { + bearerToken := config.NUMIA_BEARER_TOKEN.Token + var authorization = "Bearer " + bearerToken + req.Header.Add("Authorization", authorization) + } + + client := &http.Client{} + resp, _ := client.Do(req) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var result txn_types.TransactionResponses + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return &result, nil + } + return nil, err +} + +type ByTimestamp []txn_types.ParsedTxn + +func (a ByTimestamp) Len() int { return len(a) } +func (a ByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByTimestamp) Less(i, j int) bool { return a[i].Timestamp.After(a[j].Timestamp) } + +func GetParsedTransactions(txns txn_types.TransactionResponses, chainId string) ([]txn_types.ParsedTxn, error) { + layout := "2006-01-02T15:04:05Z" + parsedTxns := []txn_types.ParsedTxn{} + for i := 0; i < len(txns.TxResponses); i++ { + txn_response := txns.TxResponses[i] + txn := txns.Txs[i] + parsedTxn := txn_types.ParsedTxn{ + Code: txn_response.Code, + GasUsed: txn_response.GasUsed, + GasWanted: txn_response.GasWanted, + Height: txn_response.Height, + RawLog: txn_response.RawLog, + Timestamp: parseTimestamp(txn_response.Timestamp, layout), + Fee: txn.AuthInfo.Fee.Amount, + Txhash: txn_response.Txhash, + Memo: txn.Body.Memo, + Messages: txn.Body.Messages, + ChainId: chainId, + } + parsedTxns = append(parsedTxns, parsedTxn) + } + + sort.Sort(ByTimestamp(parsedTxns)) + + return parsedTxns, nil + +} + +func GetParsedTransaction(txns txn_types.SingleTxnRes, chainId string) (txn_types.ParsedTxn, error) { + layout := "2006-01-02T15:04:05Z" + txn_response := txns.TxResponses + txn := txns.Txs + parsedTxn := txn_types.ParsedTxn{ + Code: txn_response.Code, + GasUsed: txn_response.GasUsed, + GasWanted: txn_response.GasWanted, + Height: txn_response.Height, + RawLog: txn_response.RawLog, + Timestamp: parseTimestamp(txn_response.Timestamp, layout), + Fee: txn.AuthInfo.Fee.Amount, + Txhash: txn_response.Txhash, + Memo: txn.Body.Memo, + Messages: txn.Body.Messages, + ChainId: chainId, + } + + return parsedTxn, nil + +} + +func parseTimestamp(timestamp, layout string) time.Time { + parsedTime, err := time.Parse(layout, timestamp) + if err != nil { + fmt.Println("Error parsing timestamp:", err) + } + return parsedTime +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/server/handler/transactions.go b/server/handler/transactions.go index 529f24521..8a1c51254 100644 --- a/server/handler/transactions.go +++ b/server/handler/transactions.go @@ -72,10 +72,10 @@ func (h *Handler) CreateTransaction(c echo.Context) error { } var id int - err = h.DB.QueryRow(`INSERT INTO "transactions"("multisig_address","fee","status","last_updated","messages","memo") + err = h.DB.QueryRow(`INSERT INTO "transactions"("multisig_address","fee","status","last_updated","messages","memo", "title", "created_at") VALUES - ($1,$2,$3,$4,$5,$6) RETURNING "id"`, - address, feebz, model.Pending, time.Now(), msgsbz, req.Memo, + ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`, + address, feebz, model.Pending, time.Now(), msgsbz, req.Memo, req.Title, time.Now(), ).Scan(&id) if err != nil { return c.JSON(http.StatusBadRequest, model.ErrorResponse{ @@ -102,6 +102,46 @@ func (h *Handler) GetTransactions(c echo.Context) error { }) } + //count of transaction status + + var rows1 *sql.Rows + rows1, err = h.DB.Query(`SELECT CASE WHEN t.status = 'FAILED' THEN 'failed' WHEN t.status = 'SUCCESS' THEN 'completed' WHEN jsonb_array_length(t.signatures) >= a.threshold THEN 'to-broadcast' ELSE 'to-sign' END AS computed_status, COUNT(*) AS count FROM transactions t JOIN multisig_accounts a ON t.multisig_address = a.address WHERE t.multisig_address = $1 GROUP BY computed_status`, address) + + if err != nil { + if rows1 != nil && sql.ErrNoRows == rows1.Err() { + return c.JSON(http.StatusBadRequest, model.ErrorResponse{ + Status: "error", + Message: fmt.Sprintf("no transactions with address %s", address), + Log: rows1.Err().Error(), + }) + } + + return c.JSON(http.StatusInternalServerError, model.ErrorResponse{ + Status: "error", + Message: "failed to query transaction", + Log: err.Error(), + }) + } + defer rows1.Close() + + txCount := make([]schema.TransactionCount, 0) + for rows1.Next() { + var txC schema.TransactionCount + if err := rows1.Scan( + &txC.ComputedStatus, + &txC.Count, + ); err != nil { + return c.JSON(http.StatusInternalServerError, model.ErrorResponse{ + Status: "error", + Message: "failed to decode transaction", + Log: err.Error(), + }) + } + txCount = append(txCount, txC) + } + + //ends here + status := utils.GetStatus(c.QueryParam("status")) var rows *sql.Rows if status == model.Pending { @@ -160,6 +200,7 @@ func (h *Handler) GetTransactions(c echo.Context) error { return c.JSON(http.StatusOK, model.SuccessResponse{ Data: transactions, Status: "success", + Count: txCount, }) } diff --git a/server/logs.txt b/server/logs.txt new file mode 100644 index 000000000..b31f6a8fb --- /dev/null +++ b/server/logs.txt @@ -0,0 +1,82 @@ +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/08 11:31:55 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=regen,cudos,pylons-bedrock,aura-network,bitsong,realio-network,passage,ripple111,scorum,mande-network,cerberus-2,nibiru,archway,akash-network,white-whale,onomy-protocol,sifchain,coreum,self-chain,seda-2,bitcoin,sei-network,celestia,oec-token,axelar,nois,chain4energy,movement,shido-2,osmosis,saga-2,secret,oraichain-token,stargaze,neutaro,switcheo,nolus,cheqd-network,certik,starname,consciousdao,crescent-network,rebus,chihuahua-token,qwoyn,mantra-dao,andromeda-2,microtick,bzedge,bluzelle,sentinel,assetmantle,sommelier,arkham,point-network,canto,nim-network,planq,decentr,source,dhealth,shareledger,six-sigma,kava,coss-2,juno-network,racoon,kava-lend,fx-coin,vidulum,tokenize-xchange,umee,neta,unstake-fi,backbone-labs-staked-juno,fuzion,hava-coin,fanfury,usd-coin,helichain,xpla,firmachain,usk,usdx,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,fetch-ai,ixo,iris-network,bitcanna,dydx-chain,injective-protocol,tether,zetachain,wynd,gitopia,dymension,autism,mythos,stride,link,ethereum,unification,humans-ai,teritori,graviton,loop,e-money,tgrade,terra-luna-2,bostrom,desmos,odin-protocol,joltify,jackal-protocol,ki,aqualibre,imv,comdex,posthuman,neutron-3,evmos,quasar-2,dig-chain,lvn,darcmatter-coin,rizon,lambda,persistence,aioz-network,lum-network,likecoin,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,cosmos,islamic-coin,medibloc,kyve-network,gravity-bridge-usdc,nami-protocol,cacao,kava-swap,dog-wif-nuchucks,lava-network,agoric,quicksilver,provenance-blockchain,stafi&vs_currencies=usd&include_24hr_change=true": read tcp [2405:201:c41c:e892:717e:1c7a:359e:facf]:51522->[2606:4700:9ae7:52d5:9c6:b49:3aea:e80a]:443: read: network is unreachable +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out +ERROR: 2024/08/26 22:28:19 cron.go:86: failed to fetch price information Get "https://api.coingecko.com/api/v3//simple/price?ids=nolus,source,onomy-protocol,chain4energy,oec-token,pylons-bedrock,microtick,ripple111,osmosis,certik,sei-network,nois,qwoyn,akash-network,celestia,consciousdao,self-chain,canto,bzedge,arkham,stargaze,chihuahua-token,shido-2,secret,mantra-dao,realio-network,planq,crescent-network,regen,cheqd-network,andromeda-2,assetmantle,movement,scorum,white-whale,saga-2,nibiru,sifchain,mande-network,cerberus-2,decentr,passage,nim-network,starname,cudos,bitcoin,bitsong,dhealth,coreum,bluzelle,archway,point-network,oraichain-token,axelar,switcheo,seda-2,aura-network,rebus,neutaro,sentinel,sommelier,shareledger,juno-network,usdx,vidulum,ethereum,usd-coin,aqualibre,dydx-chain,teritori,quasar-2,bostrom,gitopia,helichain,odin-protocol,zetachain,autism,unstake-fi,backbone-labs-staked-juno,evmos,coss-2,dig-chain,mythos,iris-network,unification,tokenize-xchange,kava,fuzion,ixo,kava-lend,link,usk,fanfury,injective-protocol,fx-coin,fetch-ai,e-money,bitcanna,firmachain,neta,xpla,comdex,ki,loop,hava-coin,graviton,neutron-3,umee,terra-luna-2,humans-ai,dymension,six-sigma,joltify,stride,desmos,tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9,imv,jackal-protocol,posthuman,wynd,tgrade,tether,racoon,lvn,cosmos,islamic-coin,agoric,kava-swap,likecoin,medibloc,lava-network,lum-network,dog-wif-nuchucks,stafi,kyve-network,nami-protocol,cacao,lambda,rizon,persistence,mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3,darcmatter-coin,quicksilver,gravity-bridge-usdc,aioz-network,provenance-blockchain&vs_currencies=usd&include_24hr_change=true": read tcp 192.168.1.53:49686->104.22.79.164:443: read: connection timed out diff --git a/server/middleware/auth.go b/server/middleware/auth.go index fe1ab5f0e..f90edbae1 100644 --- a/server/middleware/auth.go +++ b/server/middleware/auth.go @@ -12,7 +12,7 @@ import ( func (h *Handler) AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { signature := c.QueryParams().Get("signature") - address := c.QueryParams().Get("address") + address := c.QueryParams().Get("cosmos_address") if address == "" { return c.JSON(http.StatusNotAcceptable, model.ErrorResponse{ @@ -28,11 +28,11 @@ func (h *Handler) AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { }) } - var userAddess string + var userAddress string saniSignature := strings.Replace(signature, " ", "+", -1) - err := h.DB.QueryRow(`SELECT address FROM users where address=$1 and signature=$2`, address, saniSignature).Scan(&userAddess) + err := h.DB.QueryRow(`SELECT address FROM users where address=$1 and signature=$2`, address, saniSignature).Scan(&userAddress) if err == sql.ErrNoRows { return c.JSON(http.StatusBadRequest, model.ErrorResponse{ Status: "Unauthorized", @@ -53,10 +53,11 @@ func (h *Handler) AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc { func (h *Handler) IsMultisigAdmin(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { address := c.QueryParams().Get("address") + multisigAddress := c.Param("address") - var userAddess string + var userAddress string - err := h.DB.QueryRow(`SELECT address FROM multisig_accounts where created_by=$1`, address).Scan(&userAddess) + err := h.DB.QueryRow(`SELECT address FROM multisig_accounts where created_by=$1 and address=$2`, address, multisigAddress).Scan(&userAddress) if err == sql.ErrNoRows { return c.JSON(http.StatusBadRequest, model.ErrorResponse{ Status: "Unauthorized", @@ -79,9 +80,9 @@ func (h *Handler) IsMultisigMember(next echo.HandlerFunc) echo.HandlerFunc { address := c.QueryParams().Get("address") multisigAddress := c.Param("address") - var userAddess string + var userAddress string - err := h.DB.QueryRow(`SELECT address FROM pubkeys where address=$1 and multisig_address=$2`, address, multisigAddress).Scan(&userAddess) + err := h.DB.QueryRow(`SELECT address FROM pubkeys where address=$1 and multisig_address=$2`, address, multisigAddress).Scan(&userAddress) if err == sql.ErrNoRows { return c.JSON(http.StatusBadRequest, model.ErrorResponse{ Status: "Unauthorized", diff --git a/server/model/status.go b/server/model/status.go index 110fe7ad9..9f2ab9bdc 100644 --- a/server/model/status.go +++ b/server/model/status.go @@ -10,4 +10,5 @@ type SuccessResponse struct { Status string `json:"status"` Data interface{} `json:"data"` Message string `json:"message"` + Count interface{} `json:"count"` } diff --git a/server/model/transactions.go b/server/model/transactions.go index 7fd7ad83f..6c0d0f4d6 100644 --- a/server/model/transactions.go +++ b/server/model/transactions.go @@ -55,6 +55,7 @@ type Message struct { type CreateTransactionRequest struct { Fee Fees `json:"fee"` + Title string `json:"title"` Messages []Message `json:"messages"` ChainId string `json:"chain_id"` Memo string `json:"memo"` diff --git a/server/networks.json b/server/networks.json new file mode 100644 index 000000000..90b0f650a --- /dev/null +++ b/server/networks.json @@ -0,0 +1,317 @@ +[ + { + "chainId": "cosmoshub-4", + "restURI": "https://apis.mintscan.io/cosmos/lcd", + "checkStatus": false, + "rpcURI": "", + "restURIs": [ + "https://apis.mintscan.io/cosmos/lcd" + ], + "sourceEnd": "mintscan" + }, + { + "chainId": "akashnet-2", + "restURI": "https://apis.mintscan.io/akash/lcd", + "checkStatus": false, + "rpcURI": "", + "restURIs": [ + "https://apis.mintscan.io/akash/lcd" + ], + "sourceEnd": "mintscan" + }, + { + "chainId": "archway-1", + "restURI": "https://apis.mintscan.io/archway/lcd", + "checkStatus": false, + "rpcURI": "", + "restURIs": [ + "https://apis.mintscan.io/archway/lcd" + ], + "sourceEnd": "mintscan" + }, + { + "chainId": "axelar", + "restURI": "https://apis.mintscan.io/axelar/lcd", + "checkStatus": false, + "rpcURI": "", + "restURIs": [ + "https://apis.mintscan.io/axelar/lcd" + ], + "sourceEnd": "mintscan" + }, + { + "chainId": "celestia", + "restURI": "https://apis.mintscan.io/celestia/lcd", + "checkStatus": false, + "rpcURI": "", + "restURIs": [ + "https://apis.mintscan.io/celestia/lcd" + ], + "sourceEnd": "mintscan" + }, + { + "chainId": "dydx-mainnet-1", + "restURI": "https://apis.mintscan.io/dydx/lcd", + "checkStatus": false, + "rpcURI": "", + "restURIs": [ + "https://apis.mintscan.io/dydx/lcd" + ], + "sourceEnd": "mintscan" + }, + { + "chainId": "osmosis-1", + "restURI": "https://apis.mintscan.io/osmosis/lcd", + "checkStatus": false, + "rpcURI": "", + "restURIs": [ + "https://apis.mintscan.io/osmosis/lcd" + ], + "sourceEnd": "mintscan" + }, + { + "chainId": "passage-2", + "restURI": "https://api.passage.vitwit.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://api.passage.vitwit.com", + "https://rest-passage.ecostake.com", + "https://passage-api.polkachu.com", + "https://api-passage-ia.cosmosia.notional.ventures" + ], + "sourceEnd": "" + }, + { + "chainId": "dymension_1100-1", + "restURI": "https://api.dymension.nodestake.org", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://dymension-mainnet-lcd.autostake.com:443", + "https://dymension.api.kjnodes.com", + "https://api.dymension.nodestake.org", + "https://dymension-api.lavenderfive.com:443" + ], + "sourceEnd": "" + }, + { + "chainId": "umee-1", + "restURI": "https://umee-lcd.quantnode.tech", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://umee-lcd.quantnode.tech", + "https://api-umee-ia.cosmosia.notional.ventures", + "https://umee-api.polkachu.com", + "https://api.resolute.vitwit.com/umee_api" + ], + "sourceEnd": "" + }, + { + "chainId": "quasar-1", + "restURI": "https://quasar-rest.publicnode.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://quasar-rest.publicnode.com", + "https://quasar-api.polkachu.com", + "https://quasar-mainnet-lcd.autostake.com:443" + ], + "sourceEnd": "" + }, + { + "chainId": "comdex-1", + "restURI": "https://rest.comdex.one", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://rest.comdex.one", + "https://comdex-api.polkachu.com", + "https://comdex-rest.publicnode.com" + ], + "sourceEnd": "" + }, + { + "chainId": "gravity-bridge-3", + "restURI": "https://gravitybridge-api.lavenderfive.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://gravitybridge-api.lavenderfive.com", + "https://gravity-api.polkachu.com", + "https://api-gravitybridge-ia.cosmosia.notional.ventures" + ], + "sourceEnd": "" + }, + { + "chainId": "mars-1", + "restURI": "https://rest.marsprotocol.io:443", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://mars-api.lavenderfive.com:443", + "https://rest.marsprotocol.io:443" + ], + "sourceEnd": "" + }, + { + "chainId": "archway-1", + "restURI": "https://api.mainnet.archway.io", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://api.mainnet.archway.io", + "https://archway-api.lavenderfive.com:443", + "https://api.archway.nodestake.top" + ], + "sourceEnd": "" + }, + { + "chainId": "agoric-3", + "restURI": "https://agoric-api.polkachu.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://agoric-api.polkachu.com", + "https://api-agoric-ia.cosmosia.notional.ventures", + "https://agoric-rpc.stakeandrelax.net" + ], + "sourceEnd": "" + }, + { + "chainId": "desmos-mainnet", + "restURI": "https://api.mainnet.desmos.network", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://api.mainnet.desmos.network", + "https://desmos-api.lavenderfive.com", + "https://api.resolute.vitwit.com/desmos_api" + ], + "sourceEnd": "" + }, + { + "chainId": "evmos_9001-2", + "restURI": "https://evmos-api.polkachu.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://evmos-api.polkachu.com", + "https://api.evmos.silentvalidator.com", + "https://rest.evmos.tcnetwork.io" + ], + "sourceEnd": "" + }, + { + "chainId": "juno-1", + "restURI": "https://juno-api.polkachu.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://juno-api.polkachu.com", + "https://juno-rest.kingnodes.com", + "https://api-juno-01.stakeflow.io" + ], + "sourceEnd": "" + }, + { + "chainId": "omniflixhub-1", + "restURI": "https://api-omniflixhub-ia.cosmosia.notional.ventures", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://api-omniflixhub-ia.cosmosia.notional.ventures", + "https://omniflix-rest.publicnode.com", + "https://omniflixhub-api.lavenderfive.com", + "https://api.resolute.vitwit.com/omniflix_api" + ], + "sourceEnd": "" + }, + { + "chainId": "quicksilver-2", + "restURI": "https://quicksilver-rest.staketab.org", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://quicksilver-rest.staketab.org", + "https://quicksilver-api.lavenderfive.com:443", + "https://quicksilver-rest.publicnode.com" + ], + "sourceEnd": "" + }, + { + "chainId": "regen-1", + "restURI": "https://regen-mainnet-lcd.autostake.com:443", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://regen-mainnet-lcd.autostake.com:443", + "https://api-regen-ia.cosmosia.notional.ventures", + "https://regen-rest.publicnode.com" + ], + "sourceEnd": "" + }, + { + "chainId": "stargaze-1", + "restURI": "https://stargaze-api.polkachu.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://stargaze-api.polkachu.com", + "https://stargaze-rest.publicnode.com", + "https://api-stargaze-ia.cosmosia.notional.ventures" + ], + "sourceEnd": "" + }, + { + "chainId": "noble-1", + "restURI": "https://noble-api.polkachu.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://noble-api.polkachu.com", + "https://noble-api.lavenderfive.com:443" + ], + "sourceEnd": "" + }, + { + "chainId": "ssc-1", + "restURI": "https://saga-rest.publicnode.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://saga-rest.publicnode.com", + "https://api.saga.nodestake.org", + "https://saga.api.kjnodes.com", + "https://saga-api.lavenderfive.com:443" + ], + "sourceEnd": "" + }, + { + "chainId": "neutron-1", + "restURI": "https://neutron-api.lavenderfive.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://neutron-api.lavenderfive.com", + "https://lcd-neutron.whispernode.com", + "https://api-neutron.cosmos-spaces.cloud", + "https://neutron-rest.publicnode.com" + ], + "sourceEnd": "" + }, + { + "chainId": "sentinelhub-2", + "restURI": "https://lcd-sentinel.whispernode.com", + "checkStatus": true, + "rpcURI": "", + "restURIs": [ + "https://lcd-sentinel.whispernode.com", + "https://api.sentinel.quokkastake.io", + "https://sentinel-api.validatornode.com", + "https://api-sentinel.chainvibes.com" + ], + "sourceEnd": "" + } +] \ No newline at end of file diff --git a/server/schema/schema.sql b/server/schema/schema.sql index 8bf7cbad3..51b33175f 100644 --- a/server/schema/schema.sql +++ b/server/schema/schema.sql @@ -54,7 +54,7 @@ CREATE TABLE public.multisig_accounts ( threshold integer NOT NULL, chain_id character varying(20) NOT NULL, pubkey_type character varying(50) NOT NULL, - name character varying(20) NOT NULL, + name character varying(100) NOT NULL, created_by character varying(50) NOT NULL, created_at timestamp with time zone DEFAULT '2022-09-23 22:26:53.911454+05:30'::timestamp with time zone NOT NULL ); @@ -132,6 +132,16 @@ uosmo osmosis t 2022-10-04 09:10:29.043476+00 {} ujuno juno-network t 2022-10-04 09:10:29.043476+00 {} ustars stargaze t 2022-10-04 09:10:29.043476+00 {} uakt akash-network t 2022-10-04 09:10:29.043476+00 {} +utia celestia t 2022-10-04 09:10:29.043476+00 {} +ubld agoric t 2022-10-04 09:10:29.043476+00 {} +attoaioz aioz-network t 2022-10-04 09:10:29.043476+00 {} +uandr andromeda-2 t 2022-10-04 09:10:29.043476+00 {} +aarch archway t 2022-10-04 09:10:29.043476+00 {} +arkh arkham t 2022-10-04 09:10:29.043476+00 {} +umntl assetmantle t 2022-10-04 09:10:29.043476+00 {} +uaura aura-network t 2022-10-04 09:10:29.043476+00 {} +uaxl axelar t 2022-10-04 09:10:29.043476+00 {} +ubze bzedge t 2022-10-04 09:10:29.043476+00 {} \. diff --git a/server/schema/transactions.go b/server/schema/transactions.go index 14d7f7f34..35c894807 100644 --- a/server/schema/transactions.go +++ b/server/schema/transactions.go @@ -19,8 +19,14 @@ type Transaction struct { CreatedAt time.Time `pg:"created_at,use_zero" json:"created_at"` } +type TransactionCount struct { + ComputedStatus string `pg:"computed_status" json:"computed_status"` + Count int `pg:"count" json:"count"` +} + type AllTransactionResult struct { ID int `pg:"id,pk" json:"id"` + Title string `pg:"title" json:"title,omitempty" sql:"-"` MultisigAddress string `pg:"multisig_address,use_zero" json:"multisig_address"` Fee *json.RawMessage `pg:"fee" json:"fee,omitempty" sql:"-"` Status string `pg:"status,use_zero" json:"status"` diff --git a/server/schema/update_denom_price.sql b/server/schema/update_denom_price.sql new file mode 100644 index 000000000..e06427328 --- /dev/null +++ b/server/schema/update_denom_price.sql @@ -0,0 +1,177 @@ + +-- Check if the primary key constraint exists +DO $$ +BEGIN + -- Attempt to create the primary key constraint on denom + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.table_constraints + WHERE table_name = 'price_info' + AND constraint_type = 'PRIMARY KEY' + ) THEN + ALTER TABLE price_info + ADD CONSTRAINT price_info_pkey PRIMARY KEY (denom); + END IF; +END $$; + +-- Example data to be inserted or updated +-- Replace this with your actual data +WITH new_data AS ( + VALUES + ('utia', 'celestia', true, NOW(), '{}'::jsonb), + ('ubld', 'agoric', true, NOW(), '{}'::jsonb), + ('attoaioz', 'aioz-network', true, NOW(), '{}'::jsonb), + ('uandr', 'andromeda-2', true, NOW(), '{}'::jsonb), + ('aarch', 'archway', true, NOW(), '{}'::jsonb), + ('arkh', 'arkham', true, NOW(), '{}'::jsonb), + ('umntl', 'assetmantle', true, NOW(), '{}'::jsonb), + ('uaura', 'aura-network', true, NOW(), '{}'::jsonb), + ('uaxl', 'axelar', true, NOW(), '{}'::jsonb), + ('ubze', 'bzedge', true, NOW(), '{}'::jsonb), + ('ubcna', 'bitcanna', true, NOW(), '{}'::jsonb), + ('ubtsg', 'bitsong', true, NOW(), '{}'::jsonb), + ('ubnt', 'bluzelle', true, NOW(), '{}'::jsonb), + ('boot', 'bostrom', true, NOW(), '{}'::jsonb), + ('acanto', 'canto', true, NOW(), '{}'::jsonb), + ('swth', 'switcheo', true, NOW(), '{}'::jsonb), + ('ucrbrus', 'cerberus-2', true, NOW(), '{}'::jsonb), + ('uc4e', 'chain4energy', true, NOW(), '{}'::jsonb), + ('ncheq', 'cheqd-network', true, NOW(), '{}'::jsonb), + ('uhuahua', 'chihuahua-token', true, NOW(), '{}'::jsonb), + ('ucmdx', 'comdex', true, NOW(), '{}'::jsonb), + ('acvnt', 'consciousdao', true, NOW(), '{}'::jsonb), + ('ucore', 'coreum', true, NOW(), '{}'::jsonb), + ('uatom', 'cosmos', true, NOW(), '{}'::jsonb), + ('ucoss', 'coss-2', true, NOW(), '{}'::jsonb), + ('ucre', 'crescent-network', true, NOW(), '{}'::jsonb), + ('acudos', 'cudos', true, NOW(), '{}'::jsonb), + ('udec', 'decentr', true, NOW(), '{}'::jsonb), + ('udsm', 'desmos', true, NOW(), '{}'::jsonb), + ('udhp', 'dhealth', true, NOW(), '{}'::jsonb), + ('udig', 'dig-chain', true, NOW(), '{}'::jsonb), + ('adydx', 'dydx-chain', true, NOW(), '{}'::jsonb), + ('adym', 'dymension', true, NOW(), '{}'::jsonb), + ('ungm', 'e-money', true, NOW(), '{}'::jsonb), + ('aevmos', 'evmos', true, NOW(), '{}'::jsonb), + ('afet', 'fetch-ai', true, NOW(), '{}'::jsonb), + ('cony', 'link', true, NOW(), '{}'::jsonb), + ('ufct', 'firmachain', true, NOW(), '{}'::jsonb), + ('ufury', 'fanfury', true, NOW(), '{}'::jsonb), + ('FX', 'fx-coin', true, NOW(), '{}'::jsonb), + ('ulore', 'gitopia', true, NOW(), '{}'::jsonb), + ('gusdc', 'gravity-bridge-usdc', true, NOW(), '{}'::jsonb), + ('ugraviton', 'graviton', true, NOW(), '{}'::jsonb), + ('aISLM', 'islamic-coin', true, NOW(), '{}'::jsonb), + ('uheli', 'helichain', true, NOW(), '{}'::jsonb), + ('aheart', 'humans-ai', true, NOW(), '{}'::jsonb), + ('uixo', 'ixo', true, NOW(), '{}'::jsonb), + ('aimv', 'imv', true, NOW(), '{}'::jsonb), + ('inj', 'injective-protocol', true, NOW(), '{}'::jsonb), + ('autism', 'autism', true, NOW(), '{}'::jsonb), + ('NINJA', 'dog-wif-nuchucks', true, NOW(), '{}'::jsonb), + ('hava', 'hava-coin', true, NOW(), '{}'::jsonb), + ('uiris', 'iris-network', true, NOW(), '{}'::jsonb), + ('ujkl', 'jackal-protocol', true, NOW(), '{}'::jsonb), + ('ujolt', 'joltify', true, NOW(), '{}'::jsonb), + ('ujuno', 'juno-network', true, NOW(), '{}'::jsonb), + ('neta', 'neta', true, NOW(), '{}'::jsonb), + ('rac', 'racoon', true, NOW(), '{}'::jsonb), + ('loop', 'loop', true, NOW(), '{}'::jsonb), + ('phmn', 'posthuman', true, NOW(), '{}'::jsonb), + ('wynd', 'wynd', true, NOW(), '{}'::jsonb), + ('bJUNO', 'backbone-labs-staked-juno', true, NOW(), '{}'::jsonb), + ('usdc', 'usd-coin', true, NOW(), '{}'::jsonb), + ('uusdc', 'usd-coin', true, NOW(), '{}'::jsonb), + ('erc20/tether/usdt', 'tether', true, NOW(), '{}'::jsonb), + ('ukava', 'kava', true, NOW(), '{}'::jsonb), + ('hard', 'kava-lend', true, NOW(), '{}'::jsonb), + ('swp', 'kava-swap', true, NOW(), '{}'::jsonb), + ('usdx', 'usdx', true, NOW(), '{}'::jsonb), + ('usdt', 'tether', true, NOW(), '{}'::jsonb), + ('uxki', 'ki', true, NOW(), '{}'::jsonb), + ('lvn', 'lvn', true, NOW(), '{}'::jsonb), + ('udarc', 'darcmatter-coin', true, NOW(), '{}'::jsonb), + ('usk', 'usk', true, NOW(), '{}'::jsonb), + ('fuzn', 'fuzion', true, NOW(), '{}'::jsonb), + ('AQLA', 'aqualibre', true, NOW(), '{}'::jsonb), + ('nstk', 'unstake-fi', true, NOW(), '{}'::jsonb), + ('nami', 'nami-protocol', true, NOW(), '{}'::jsonb), + ('ukyve', 'kyve-network', true, NOW(), '{}'::jsonb), + ('ulamb', 'lambda', true, NOW(), '{}'::jsonb), + ('ulava', 'lava-network', true, NOW(), '{}'::jsonb), + ('nanolike', 'likecoin', true, NOW(), '{}'::jsonb), + ('ulum', 'lum-network', true, NOW(), '{}'::jsonb), + ('amand', 'mande-network', true, NOW(), '{}'::jsonb), + ('uom', 'mantra-dao', true, NOW(), '{}'::jsonb), + ('umars', 'mars-protocol-a7fcbcfb-fd61-4017-92f0-7ee9f9cc6da3', true, NOW(), '{}'::jsonb), + ('cacao', 'cacao', true, NOW(), '{}'::jsonb), + ('utick', 'microtick', true, NOW(), '{}'::jsonb), + ('uwhale', 'white-whale', true, NOW(), '{}'::jsonb), + ('umove', 'movement', true, NOW(), '{}'::jsonb), + ('aMYT', 'mythos', true, NOW(), '{}'::jsonb), + ('uneutaro', 'neutaro', true, NOW(), '{}'::jsonb), + ('untrn', 'neutron-3', true, NOW(), '{}'::jsonb), + ('unibi', 'nibiru', true, NOW(), '{}'::jsonb), + ('anim', 'nim-network', true, NOW(), '{}'::jsonb), + ('unois', 'nois', true, NOW(), '{}'::jsonb), + ('unls', 'nolus', true, NOW(), '{}'::jsonb), + ('loki', 'odin-protocol', true, NOW(), '{}'::jsonb), + ('wei', 'oec-token', true, NOW(), '{}'::jsonb), + ('anom', 'onomy-protocol', true, NOW(), '{}'::jsonb), + ('orai', 'oraichain-token', true, NOW(), '{}'::jsonb), + ('uosmo', 'osmosis', true, NOW(), '{}'::jsonb), + ('umed', 'medibloc', true, NOW(), '{}'::jsonb), + ('upasg', 'passage', true, NOW(), '{}'::jsonb), + ('uxprt', 'persistence', true, NOW(), '{}'::jsonb), + ('aplanq', 'planq', true, NOW(), '{}'::jsonb), + ('apoint', 'point-network', true, NOW(), '{}'::jsonb), + ('nhash', 'provenance-blockchain', true, NOW(), '{}'::jsonb), + ('ubedrock', 'pylons-bedrock', true, NOW(), '{}'::jsonb), + ('uqsr', 'quasar-2', true, NOW(), '{}'::jsonb), + ('uqck', 'quicksilver', true, NOW(), '{}'::jsonb), + ('uqwoyn', 'qwoyn', true, NOW(), '{}'::jsonb), + ('ario', 'realio-network', true, NOW(), '{}'::jsonb), + ('arebus', 'rebus', true, NOW(), '{}'::jsonb), + ('uregen', 'regen', true, NOW(), '{}'::jsonb), + ('uatolo', 'rizon', true, NOW(), '{}'::jsonb), + ('usaga', 'saga-2', true, NOW(), '{}'::jsonb), + ('nscr', 'scorum', true, NOW(), '{}'::jsonb), + ('uscrt', 'secret', true, NOW(), '{}'::jsonb), + ('aseda', 'seda-2', true, NOW(), '{}'::jsonb), + ('usei', 'sei-network', true, NOW(), '{}'::jsonb), + ('uslf', 'self-chain', true, NOW(), '{}'::jsonb), + ('udvpn', 'sentinel', true, NOW(), '{}'::jsonb), + ('usge', 'six-sigma', true, NOW(), '{}'::jsonb), + ('nshr', 'shareledger', true, NOW(), '{}'::jsonb), + ('uctk', 'certik', true, NOW(), '{}'::jsonb), + ('shido', 'shido-2', true, NOW(), '{}'::jsonb), + ('rowan', 'sifchain', true, NOW(), '{}'::jsonb), + ('usomm', 'sommelier', true, NOW(), '{}'::jsonb), + ('usource', 'source', true, NOW(), '{}'::jsonb), + ('ufis', 'stafi', true, NOW(), '{}'::jsonb), + ('ustars', 'stargaze', true, NOW(), '{}'::jsonb), + ('uiov', 'starname', true, NOW(), '{}'::jsonb), + ('ustrd', 'stride', true, NOW(), '{}'::jsonb), + ('atenet', 'tenet-1b000f7b-59cb-4e06-89ce-d62b32d362b9', true, NOW(), '{}'::jsonb), + ('utori', 'teritori', true, NOW(), '{}'::jsonb), + ('uluna', 'terra-luna-2', true, NOW(), '{}'::jsonb), + ('utgd', 'tgrade', true, NOW(), '{}'::jsonb), + ('atkx', 'tokenize-xchange', true, NOW(), '{}'::jsonb), + ('uumee', 'umee', true, NOW(), '{}'::jsonb), + ('nund', 'unification', true, NOW(), '{}'::jsonb), + ('uvdl', 'vidulum', true, NOW(), '{}'::jsonb), + ('axpla', 'xpla', true, NOW(), '{}'::jsonb), + ('azeta', 'zetachain', true, NOW(), '{}'::jsonb), + ('weth', 'weth', true, NOW(), '{}'::jsonb), + ('wbtc', 'wrapped-bitcoin', true, NOW(), '{}'::jsonb) +) + +-- Insert or update the data +INSERT INTO price_info (denom, coingecko_name, enabled, last_updated, info) +SELECT * FROM new_data +ON CONFLICT (denom) DO UPDATE +SET + coingecko_name = EXCLUDED.coingecko_name, + enabled = EXCLUDED.enabled, + last_updated = EXCLUDED.last_updated, + info = EXCLUDED.info; \ No newline at end of file diff --git a/server/schema/update_schema.sql b/server/schema/update_schema.sql new file mode 100644 index 000000000..43f739066 --- /dev/null +++ b/server/schema/update_schema.sql @@ -0,0 +1,14 @@ +-- File: add_columns_if_not_exists.sql + +DO $$ +BEGIN + -- Check and add new_column_name if it doesn't exist + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'transactions' AND column_name = 'title' + ) THEN + ALTER TABLE transactions + ADD COLUMN title VARCHAR(255) DEFAULT ''; + END IF; +END $$; diff --git a/server/server.go b/server/server.go index dd90d7f3c..ab8e832c4 100644 --- a/server/server.go +++ b/server/server.go @@ -3,12 +3,19 @@ package main import ( + "bytes" + "compress/gzip" + "encoding/json" + "io" + "io/ioutil" "net/http" + "github.com/andybalholm/brotli" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" + "github.com/vitwit/resolute/server/clients" "github.com/vitwit/resolute/server/config" "github.com/vitwit/resolute/server/cron" "github.com/vitwit/resolute/server/handler" @@ -21,6 +28,15 @@ import ( _ "github.com/lib/pq" ) +func init() { + config, err := config.ParseConfig() + if err != nil { + log.Fatal(err) + } + // Initialize the Redis client + clients.InitializeRedis(config.REDIS_URI, "", 0) +} + func main() { e := echo.New() e.Logger.SetLevel(log.ERROR) @@ -76,6 +92,10 @@ func main() { e.POST("/multisig/:address/sign-tx/:id", h.SignTransaction, m.AuthMiddleware, m.IsMultisigMember) e.GET("/multisig/:address/txs", h.GetTransactions) e.GET("/accounts/:address/all-txns", h.GetAllMultisigTxns) + e.POST("/transactions", h.GetRecentTransactions) + e.GET("/txns/:chainId/:address", h.GetAllTransactions) + e.GET("/txns/:chainId/:address/:txhash", h.GetChainTxHash) + e.GET("/search/txns/:txhash", h.GetTxHash) // users e.POST("/users/:address/signature", h.CreateUserSignature) @@ -84,7 +104,12 @@ func main() { e.GET("/tokens-info", h.GetTokensInfo) e.GET("/tokens-info/:denom", h.GetTokenInfo) + e.POST("/cosmos/tx/v1beta1/txs", proxyHandler1) + + e.Any("/*", proxyHandler) + e.GET("/", func(c echo.Context) error { + return c.JSON(http.StatusOK, model.SuccessResponse{ Status: "success", Message: "server up", @@ -105,3 +130,178 @@ func main() { // TODO: add ip and port e.Logger.Fatal(e.Start(fmt.Sprintf(":%s", apiCfg.Port))) } + +func proxyHandler1(c echo.Context) error { + config, err := config.ParseConfig() + if err != nil { + log.Fatal(err) + } + + type RequestBody struct { + Mode string `json:"mode"` + TxBytes string `json:"tx_bytes"` + } + + reqBody := new(RequestBody) + + // Bind the request body to the struct + if err := c.Bind(reqBody); err != nil { + return c.String(http.StatusBadRequest, "Invalid request") + } + + // Convert the struct to JSON + jsonData, err := json.Marshal(reqBody) + if err != nil { + return c.String(http.StatusInternalServerError, "Error encoding JSON") + } + + chanDetails := clients.GetChain(c.QueryParam("chain")) + + if chanDetails == nil { + return c.String(http.StatusInternalServerError, "Failed to get the server") + } + + // URL to which the POST request will be sent + targetURL := chanDetails.RestURI + "/cosmos/tx/v1beta1/txs" + + // Create a new HTTP request + req, err := http.NewRequest("POST", targetURL, bytes.NewBuffer(jsonData)) + if err != nil { + return c.String(http.StatusInternalServerError, "Error creating request") + } + + // Set the Content-Type header + req.Header.Set("Content-Type", "application/json") + + if chanDetails.SourceEnd == "mintscan" { + authorizationToken := fmt.Sprintf("Bearer %s", config.MINTSCAN_TOKEN.Token) + + req.Header.Add("Authorization", authorizationToken) + } + + if chanDetails.SourceEnd == "numia" { + bearerToken := config.NUMIA_BEARER_TOKEN.Token + var authorization = "Bearer " + bearerToken + + req.Header.Add("Authorization", authorization) + } + + // Create a new HTTP client and send the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return c.String(http.StatusInternalServerError, "Error sending request") + } + defer resp.Body.Close() + + // Read the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error reading response") + } + + // Respond back to the original request + return c.JSON(http.StatusOK, string(body)) +} + +func proxyHandler(c echo.Context) error { + config, err := config.ParseConfig() + if err != nil { + log.Fatal(err) + } + + chanDetails := clients.GetChain(c.QueryParam("chain")) + + if chanDetails == nil { + return c.String(http.StatusInternalServerError, "Failed to get the server") + } + // Construct the target URL based on the incoming request + targetBase := chanDetails.RestURI // Change this to your target service base URL + + targetURL := targetBase + c.Request().URL.Path + if c.Request().URL.RawQuery != "" { + targetURL += "?" + c.Request().URL.RawQuery + } + + // Create a new request to the target URL + req, err := http.NewRequest(c.Request().Method, targetURL, c.Request().Body) + if err != nil { + log.Printf("Failed to create request: %v", err) + return c.String(http.StatusInternalServerError, "Failed to create request") + } + // Forward headers from the original request + for name, values := range c.Request().Header { + for _, value := range values { + req.Header.Add(name, value) + } + } + req.Header.Set("Content-Type", "application/json") + + // Add Authorization header + if chanDetails.SourceEnd == "mintscan" { + authorizationToken := fmt.Sprintf("Bearer %s", config.MINTSCAN_TOKEN.Token) + req.Header.Add("Authorization", authorizationToken) // Change this to your actual token + } + + if chanDetails.SourceEnd == "numia" { + bearerToken := config.NUMIA_BEARER_TOKEN.Token + var authorization = "Bearer " + bearerToken + + req.Header.Add("Authorization", authorization) + } + + // Make the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Failed to make request: %v", err) + return c.String(http.StatusInternalServerError, "Failed to make request") + } + defer resp.Body.Close() + + // Check the content encoding and decode accordingly + var reader io.ReadCloser + switch resp.Header.Get("Content-Encoding") { + case "gzip": + reader, err = gzip.NewReader(resp.Body) + if err != nil { + log.Printf("Failed to create gzip reader: %v", err) + return c.String(http.StatusInternalServerError, "Failed to decompress response") + } + defer reader.Close() + case "br": + reader = ioutil.NopCloser(brotli.NewReader(resp.Body)) + defer reader.Close() + default: + reader = resp.Body + } + + // Read the decompressed or raw body + bodyBytes, err := ioutil.ReadAll(reader) + if err != nil { + log.Printf("Failed to read response body: %v", err) + return c.String(http.StatusInternalServerError, "Failed to read response body") + } + + // Set content type and response + c.Response().Header().Set("Content-Type", resp.Header.Get("Content-Type")) + c.Response().WriteHeader(resp.StatusCode) + _, err = c.Response().Writer.Write(bodyBytes) + if err != nil { + log.Printf("Failed to write response body: %v", err) + return c.String(http.StatusInternalServerError, "Failed to write response body") + } + + return nil + + // c.Response().WriteHeader(resp.StatusCode) + + // // Copy the response body to the original response + // _, err = io.Copy(c.Response().Writer, resp.Body) + // if err != nil { + // log.Printf("Failed to read response body: %v", err) + // return c.String(http.StatusInternalServerError, "Failed to read response body") + // } + + // return nil +} diff --git a/server/txn_types/recent_transactions.go b/server/txn_types/recent_transactions.go new file mode 100644 index 000000000..79a42c2c7 --- /dev/null +++ b/server/txn_types/recent_transactions.go @@ -0,0 +1,91 @@ +package txn_types + +import "time" + +type TxResponse struct { + Code int `json:"code"` + Codespace string `json:"codespace"` + Data string `json:"data"` + Events []interface{} `json:"events"` + GasUsed string `json:"gas_used"` + GasWanted string `json:"gas_wanted"` + Height string `json:"height"` + Info string `json:"info"` + Logs []interface{} `json:"logs"` + RawLog string `json:"raw_log"` + Timestamp string `json:"timestamp"` + Tx interface{} `json:"tx"` + Txhash string `json:"txhash"` +} + +type Amount []struct { + Amount string `json:"amount"` + Denom string `json:"denom"` +} + +type AuthInfo struct { + Fee struct { + Amount Amount `json:"amount"` + GasLimit string `json:"gas_limit"` + Granter string `json:"granter"` + Payer string `json:"payer"` + } `json:"fee"` + SignerInfos []struct { + ModeInfo struct { + Single struct { + Mode string `json:"mode"` + } `json:"single"` + } `json:"mode_info"` + PublicKey struct { + Type string `json:"@type"` + Key string `json:"key"` + } `json:"public_key"` + Sequence string `json:"sequence"` + } `json:"signer_infos"` + Tip interface{} `json:"tip"` +} + +type Body struct { + ExtensionOptions []interface{} `json:"extension_options"` + Memo string `json:"memo"` + Messages []interface{} `json:"messages"` + NonCriticalExtensionOptions []interface{} `json:"non_critical_extension_options"` + TimeoutHeight string `json:"timeout_height"` +} + +type Tx struct { + AuthInfo AuthInfo `json:"auth_info"` + Body Body `json:"body"` + Signatures []string `json:"signatures"` +} + +type Pagination struct { + Next_key string `json:"next_key"` + Total string `json:"total"` +} + +type TransactionResponses struct { + Pagination Pagination `json:"pagination"` + Total string `json:"total"` + TxResponses []TxResponse `json:"tx_responses"` + Txs []Tx `json:"txs"` +} + +type ParsedTxn struct { + Code int `json:"code"` + GasUsed string `json:"gas_used"` + GasWanted string `json:"gas_wanted"` + Fee Amount `json:"fee"` + Height string `json:"height"` + RawLog string `json:"raw_log"` + Timestamp time.Time `json:"timestamp"` + Txhash string `json:"txhash"` + Memo string `json:"memo"` + Messages []interface{} `json:"messages"` + ChainId string `json:"chain_id"` +} + +type SingleTxnRes struct { + TxResponses TxResponse `json:"tx_response"` + Txs Tx `json:"tx"` +} diff --git a/server/utils/recent_transactions.go b/server/utils/recent_transactions.go new file mode 100644 index 000000000..9d38cdb19 --- /dev/null +++ b/server/utils/recent_transactions.go @@ -0,0 +1,75 @@ +package utils + +import ( + "errors" + "fmt" + "net/url" + + "github.com/vitwit/resolute/server/config" +) + +// type ChainConfig struct { +// ChainId string `json:"chainId"` +// RestURIs []string `json:"restURIs"` +// } + +func GetChainAPIs(chainId string) (*config.ChainConfig, error) { + + data := config.GetChainAPIs() + // wd, _ := os.Getwd() + // filePath := filepath.Join(wd, "/", "networks.json") + // jsonData, err := os.ReadFile(filePath) + // if err != nil { + // fmt.Println("Error reading JSON file:", err) + // return nil, err + // } + + // var data []ChainConfig + + // err = json.Unmarshal([]byte(jsonData), &data) + // if err != nil { + // fmt.Println("Error unmarshaling JSON data:", err) + // return nil, err + // } + + var result *config.ChainConfig + for _, config := range data { + if config.ChainId == chainId { + result = config + break + } + } + + fmt.Println("result============================", result.SourceEnd, result.ChainId) + + if result == nil { + return nil, errors.New("chain id not found") + } + + return result, nil +} + +func CreateRequestURI(api string, module string, address string) string { + // senderEvent := fmt.Sprintf(`message.sender="%v"`, address) + + // params := url.Values{} + // params.Set("events", senderEvent) + // if module == "all" { + // moduleEvent := fmt.Sprintf(`message.module="%v"`, module) + // params.Set("events", moduleEvent) + // } + // return fmt.Sprintf(api + "/cosmos/tx/v1beta1/txs" + params.Encode()) + if module == "all" { + return api + "/cosmos/tx/v1beta1/txs" + "?events=message.sender=%27" + address + "%27" + "&order_by=2&pagination.limit=5" + } else { + return api + "/cosmos/tx/v1beta1/txs" + "?events=message.sender=%27" + address + "%27&events=message.module=%27" + module + "%27" + "&order_by=2&pagination.limit=5" + } +} + +func CreateAllTxnsRequestURI(api string, address string, limit string, offset string) string { + return fmt.Sprintf("%s/cosmos/tx/v1beta1/txs?events=message.sender='%s'&order_by=2&pagination.limit=%s&pagination.offset=%s", api, url.QueryEscape(address), url.QueryEscape(limit), url.QueryEscape(offset)) +} + +func CreateTxnRequestURI(api string, txhash string) string { + return fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", api, txhash) +}