diff --git a/.github/workflows/buildweb.yaml b/.github/workflows/buildweb.yaml deleted file mode 100644 index c43cc35fb..000000000 --- a/.github/workflows/buildweb.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: Build web UI as a test - -on: - pull_request: - branches: - - main - paths: - - web-ui/** - push: - paths: - - web-ui/** - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest # or specify a version - - - name: Install Dependencies - run: | - cd web-ui - bun install --frozen-lockfile - - - name: Build Project - run: | - cd web-ui - bun run build - diff --git a/.github/workflows/deploytestweb.yaml b/.github/workflows/deploytestweb.yaml new file mode 100644 index 000000000..c0b189645 --- /dev/null +++ b/.github/workflows/deploytestweb.yaml @@ -0,0 +1,41 @@ +name: Web UI Dev Deployment +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + +jobs: + Deploy-Dev: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Vercel CLI + run: npm install --global vercel@^33.2.0 # Replace with the current major version + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest # or specify a version + + - name: Pull Vercel Environment Information + run: vercel pull --yes --token=${{ secrets.VERCEL_TOKEN }} + + - name: Build Project Artifacts + run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy to Vercel Action + uses: BetaHuhn/deploy-to-vercel-action@v1 + with: + GITHUB_TOKEN: ${{ secrets.GH_PAT }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + ALIAS_DOMAINS: | + {BRANCH}.app-dev.quicksilver.zone + PR_PREVIEW_DOMAIN: "pr-{PR}.app-dev.quicksilver.zone" + diff --git a/.github/workflows/devWebUiDeploy.yaml b/.github/workflows/devWebUiDeploy.yaml deleted file mode 100644 index 3ace0d25c..000000000 --- a/.github/workflows/devWebUiDeploy.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: Build & Deploy web-ui with dev environment to Github Pages - -on: - push: - branches: - - bunDev2 - paths: - - web-ui/** - pull_request: - branches: - - bunDev2 - paths: - - web-ui/** - - workflow_dispatch: - -permissions: - contents: write - pages: write - id-token: write - actions: write - deployments: write - pull-requests: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - - name: Setup Environment File - run: | - cd web-ui - cp .env.development .env - - - name: Install Dependencies - run: | - cd web-ui - bun install - - - name: Build Project - run: | - cd web-ui - bun run build - - - name: Export Project - run: | - cd web-ui - bun export - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./web-ui/out diff --git a/web-ui/.env b/web-ui/.env index e2ee1d560..638c38600 100644 --- a/web-ui/.env +++ b/web-ui/.env @@ -13,16 +13,20 @@ NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_SOMMELIER="https://lcd.sommelier-3.quicksilver. NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_SOMMELIER="https://rpc.sommelier-3.quicksilver.zone" NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_JUNO="https://lcd.juno-1.quicksilver.zone" NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_JUNO="https://rpc.juno-1.quicksilver.zone" +NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_DYDX="https://lcd.dydx-mainnet-1.quicksilver.zone" +NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_DYDX="https://rpc.dydx-mainnet-1.quicksilver.zone" NEXT_PUBLIC_QUICKSILVER_API="https://lcd.quicksilver.zone" NEXT_PUBLIC_QUICKSILVER_DATA_API="https://data.quicksilver.zone" ZONE_URL="quicksilver.zone" APY_ZONES_ENDPOINT = "https://chains.cosmos.directory" NEXT_PUBLIC_OSMOSIS_API="https://api.osmosis.zone" -NEXT_PUBLIC_WHITELISTED_DENOM="uatom,ustars,uosmo,usomm,uregen" -NEXT_PUBLIC_WHITELISTED_ZONES="osmosis-1,stargaze-1,regen-1,cosmoshub-4,sommelier-3" +NEXT_PUBLIC_WHITELISTED_DENOM="uatom,ustars,uosmo,usomm,uregen,ujuno,udydx" +NEXT_PUBLIC_WHITELISTED_ZONES="osmosis-1,stargaze-1,regen-1,cosmoshub-4,sommelier-3,juno-1,dydx-mainnet-1" NEXT_PUBLIC_COSMOSHUB_CHAIN_ID=cosmoshub-4 NEXT_PUBLIC_OSMOSIS_CHAIN_ID=osmosis-1 NEXT_PUBLIC_STARGAZE_CHAIN_ID=stargaze-1 NEXT_PUBLIC_REGEN_CHAIN_ID=regen-1 NEXT_PUBLIC_SOMMELIER_CHAIN_ID=sommelier-3 -NEXT_PUBLIC_JUNO_CHAIN_ID=juno-1 \ No newline at end of file +NEXT_PUBLIC_JUNO_CHAIN_ID=juno-1 +NEXT_PUBLIC_DYDX_CHAIN_ID=dydx-mainnet-1 +NEXT_PRIVATE_WALLET_CONNECT_TOKEN="41a0749c331d209190beeac1c2530c90" \ No newline at end of file diff --git a/web-ui/README.md b/web-ui/README.md index a10bfc611..50ea63205 100644 --- a/web-ui/README.md +++ b/web-ui/README.md @@ -31,25 +31,33 @@ Please use this Prettier config to format your code before opening a pull reques Please ensure your IDE is configured to use Typescript v4.9.3 -### Development ToDo +### TODO: -**Governance** +**Staking** + +- [ ] Add support for Dymension +- [ ] Add more weight options IE `equal`, `custom`, `most votes`, `lowest commission` etc +- [x] Make back button in staking modal larger +- [x] Fix skeleton spam when searching for non existent validator in staking modal -- add liquid staked governance (when its built) +**Governance** **UI/UX** -- focus on mobile landscape breakpoints, (mainly staking page) +- [ ] Double check breakpoints **Mobile Menu** -- graphic elements +- [ ] improve mobile menu **DevOps** -- make onboarding networks seamless +- [ ] Add doc for adding networks + +**Has Blockers** + +- [ ] Build liquid governance page -**Blockers** +**Assets** -- main net reward current & epoch queries -- update grantee address in query & tx +- [ ] Fix the way queries for networks and entries in components are created. Rather than defining one for each network, create a function that iterates through the .env entry or liveNetworks call for live networks and creates the queries and components for each. `pages/assets.tsx` diff --git a/web-ui/addNetworks.md b/web-ui/addNetworks.md new file mode 100644 index 000000000..2fb66e528 --- /dev/null +++ b/web-ui/addNetworks.md @@ -0,0 +1,134 @@ +# Adding Networks + +This document will guide you through the process of adding networks to the web UI. + +## Table of Contents + +- [.env](#.env) +- [Files](#files) +- [Queries](#queries) +- [Components](#components) + +### .env + +You will need to add a couple of environment variables to the `.env` file in the root of the project. These variables are used to configure the networks that are available to the user and the endpoints that the web UI will use to interact with the blockchain. + +**Example .env file additions** + +```bash +NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_NETWORK_NAME="REST_ENDPOINT" # change `NETWORK_NAME`, `REST_ENDPOINT` to the name & endpoint of the network you are adding. +NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_NETWORK_NAME="RPC_ENDPOINT" # change `NETWORK_NAME` and `RPC_ENDPOINT` to the name & endpoint of the network you are adding. + +NEXT_PUBLIC_WHITELISTED_DENOM="uatom,ustars,uosmo,usomm,uregen,ujuno,udydx,NEW_UDENOM" # change `NEW_UDENOM` to the appropriate denom for the network you are adding. +NEXT_PUBLIC_WHITELISTED_ZONES="osmosis-1,stargaze-1,regen-1,cosmoshub-4,sommelier-3,juno-1,dydx-mainnet-1,NEW_CHAIN_ID" # change `NEW_CHAIN_ID` to the appropriate chain_id for the network you are adding. + +NEXT_PUBLIC_CHAIN_NAME_CHAIN_ID="CHAIN_ID" # change `CHAIN_ID` to the appropriate chain_id for the network you are adding. +``` + +### Files + +There are various files that will require updates to add a new network to the web UI. These files include: + +- `hooks/useGrpcQueryClient.ts` +- `hooks/useRpcQueryClient.ts` +- `pages/_app.tsx` +- + +### Queries & Signing + +The web UI uses tanstack react-query for data fetching and cosmology for query client building. You will need to add a new entry in `hooks/useGrpcQueryClient.ts` & `hooks/useRpcQueryClient.ts` in order to fetch data for your additional network. + +Here we rely on the entries in the `.env` file to determine which endpoint to use for the network we are fetching data for so be sure to use the correct endpoint or the query will be broken. + +**Example useGrpcQueryClient.ts** + +```typescript +const endpoints: { [key: string]: string | undefined } = { + quicksilver: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_QUICKSILVER : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_QUICKSILVER, + cosmoshub: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_COSMOSHUB : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_COSMOSHUB, + sommelier: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_SOMMELIER : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_SOMMELIER, + stargaze: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_STARGAZE : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_STARGAZE, + regen: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_REGEN : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_REGEN, + osmosis: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_OSMOSIS : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_OSMOSIS, + juno: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_JUNO : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_JUNO, + dydx: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_DYDX : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_DYDX, + NEW_NETWORK: + env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_NEW_NETWORK : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_NEW_NETWORK, +}; +``` + +**Example useRpcQueryClient.ts** + +```typescript +const endpoints: { [key: string]: string | undefined } = { + quicksilver: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_QUICKSILVER : process.env.MAINNET_RPC_ENDPOINT_QUICKSILVER, + cosmoshub: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_COSMOSHUB : process.env.MAINNET_RPC_ENDPOINT_COSMOSHUB, + sommelier: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_SOMMELIER : process.env.MAINNET_RPC_ENDPOINT_SOMMELIER, + stargaze: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_STARGAZE : process.env.MAINNET_RP_ENDPOINTC_STARGAZE, + regen: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_REGEN : process.env.MAINNET_RPC_ENDPOINT_REGEN, + osmosis: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_OSMOSIS : process.env.MAINNET_RPC_ENDPOINT_OSMOSIS, + juno: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_JUNO : process.env.MAINNET_RPC_ENDPOINT_JUNO, + dydx: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_DYDX : process.env.MAINNET_RPC_ENDPOINT_DYDX, + NEW_NETWORK: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_NEW_NETWORK : process.env.MAINNET_RPC_ENDPOINT_NEW_NETWORK, +}; +``` + +For signing we must update the `pages/_app.tsx` file to include the new network in the `ChainProvider` `endpointOptions` array. This array is used to determine which endpoints to use. + +```typescript + endpointOptions={{ + isLazy: true, + endpoints: { + quicksilver: { + rpc: [rpcEndpoints.quicksilver ?? ''], + rest: [lcdEndpoints.quicksilver ?? ''], + }, + quicksilvertestnet: { + rest: ['https://lcd.test.quicksilver.zone/'], + rpc: ['https://rpc.test.quicksilver.zone'], + }, + cosmoshub: { + rpc: [rpcEndpoints.cosmoshub ?? ''], + rest: [lcdEndpoints.cosmoshub ?? ''], + }, + sommelier: { + rpc: [rpcEndpoints.sommelier ?? ''], + rest: [lcdEndpoints.sommelier ?? ''], + }, + stargaze: { + rpc: [rpcEndpoints.stargaze ?? ''], + rest: [lcdEndpoints.stargaze ?? ''], + }, + regen: { + rpc: [rpcEndpoints.regen ?? ''], + rest: [lcdEndpoints.regen ?? ''], + }, + osmosis: { + rpc: [rpcEndpoints.osmosis ?? ''], + rest: [lcdEndpoints.osmosis ?? ''], + }, + osmosistestnet: { + rpc: [rpcEndpoints.osmosis ?? ''], + rest: [lcdEndpoints.osmosis ?? ''], + }, + umee: { + rpc: ['https://rpc-umee-ia.cosmosia.notional.ventures/'], + rest: ['https://api-umee-ia.cosmosia.notional.ventures/'], + }, + dydx: { + rpc: [rpcEndpoints.dydx ?? ''], + rest: [lcdEndpoints.dydx ?? ''], + }, + NEW_NETWORK: { + rpc: [rpcEndpoints.NEW_NETWORK ?? ''], + rest: [lcdEndpoints.NEW_NETWORK ?? ''], + }, + }, + }} +``` + +### Components diff --git a/web-ui/bun.lockb b/web-ui/bun.lockb index a03b7aa27..fc829ea07 100755 Binary files a/web-ui/bun.lockb and b/web-ui/bun.lockb differ diff --git a/web-ui/components/Assets/assetsGrid.tsx b/web-ui/components/Assets/assetsGrid.tsx index be26d968f..041d45622 100644 --- a/web-ui/components/Assets/assetsGrid.tsx +++ b/web-ui/components/Assets/assetsGrid.tsx @@ -2,6 +2,7 @@ import { WarningIcon } from '@chakra-ui/icons'; import { Box, VStack, Text, Divider, HStack, Flex, Grid, GridItem, Spinner, Tooltip } from '@chakra-ui/react'; import React from 'react'; +import { truncateToTwoDecimals } from '@/utils'; import { shiftDigits, formatQasset } from '@/utils'; import QDepositModal from './modals/qTokenDepositModal'; @@ -13,6 +14,7 @@ interface AssetCardProps { balance: string; apy: number; nativeAssetName: string; + redemptionRates: string; isWalletConnected: boolean; nonNative: LiquidRewardsData | undefined; } @@ -24,6 +26,7 @@ interface AssetGridProps { balance: string; apy: number; native: string; + redemptionRates: string; }>; nonNative: LiquidRewardsData | undefined; } @@ -50,17 +53,12 @@ type LiquidRewardsData = { errors: Errors; }; -function truncateToTwoDecimals(num: number) { - const multiplier = Math.pow(10, 2); - return Math.floor(num * multiplier) / multiplier; -} - -const AssetCard: React.FC = ({ assetName, balance, apy }) => { +const AssetCard: React.FC = ({ assetName, balance, apy, redemptionRates }) => { const calculateTotalBalance = (nonNative: LiquidRewardsData | undefined, nativeAssetName: string) => { if (!nonNative) { return '0'; } - const chainIds = ['osmosis-1', 'secret-1', 'umee-1', 'cosmoshub-4', 'stargaze-1', 'sommelier-3', 'regen-1', 'juno-1']; + const chainIds = ['osmosis-1', 'secret-1', 'umee-1', 'cosmoshub-4', 'stargaze-1', 'sommelier-3', 'regen-1', 'juno-1', 'dydx-mainnet-1']; let totalAmount = 0; chainIds.forEach((chainId) => { @@ -69,13 +67,13 @@ const AssetCard: React.FC = ({ assetName, balance, apy }) => { assetsInChain.forEach((asset: any) => { const assetAmount = asset.Amount.find((amount: { denom: string }) => amount.denom === `uq${nativeAssetName.toLowerCase()}`); if (assetAmount) { - totalAmount += parseInt(assetAmount.amount, 10); // assuming amount is a string + totalAmount += parseInt(assetAmount.amount, 10); } }); } }); - return shiftDigits(totalAmount.toString(), -6); // Adjust the shift as per your data's scale + return shiftDigits(totalAmount.toString(), -6); }; // const nativeAssets = nonNative?.assets['quicksilver-2'] @@ -105,7 +103,7 @@ const AssetCard: React.FC = ({ assetName, balance, apy }) => { } return ( - + @@ -132,18 +130,35 @@ const AssetCard: React.FC = ({ assetName, balance, apy }) => { {balance.toString()} {assetName} - {/* - - NON-NATIVE: - - - - - - */} + {balance > '0' ? ( + <> + + + REDEEMABLE FOR: + + + + + {truncateToTwoDecimals(Number(balance) * Number(redemptionRates)).toString()} {assetName.slice('q'.length)} + + + + ) : ( + <> + + + REDEEMABLE FOR: + + + + + Placeholder + + + + )} - @@ -159,9 +174,6 @@ const AssetsGrid: React.FC = ({ assets, isWalletConnected, nonNa qAssets - - - {!isWalletConnected && ( = ({ assets, isWalletConnected, nonNa balance={asset.balance} apy={asset.apy} nonNative={nonNative} + redemptionRates={asset.redemptionRates} /> ))} diff --git a/web-ui/components/Assets/intents.tsx b/web-ui/components/Assets/intents.tsx index 5ebc66413..46851cdf7 100644 --- a/web-ui/components/Assets/intents.tsx +++ b/web-ui/components/Assets/intents.tsx @@ -16,13 +16,13 @@ import { } from '@chakra-ui/react'; import { Key, useState } from 'react'; + import { useIntentQuery, useValidatorLogos, useValidatorsQuery } from '@/hooks/useQueries'; import { networks as prodNetworks, testNetworks as devNetworks } from '@/state/chains/prod'; import { truncateString } from '@/utils'; import SignalIntentModal from './modals/signalIntentProcess'; - export interface StakingIntentProps { address: string; isWalletConnected: boolean; @@ -31,7 +31,7 @@ export interface StakingIntentProps { const StakingIntent: React.FC = ({ address, isWalletConnected }) => { const networks = process.env.NEXT_PUBLIC_CHAIN_ENV === 'mainnet' ? prodNetworks : devNetworks; - const chains = ['Cosmos', 'Osmosis', 'Stargaze', 'Regen', 'Sommelier', 'Juno']; + const chains = ['Cosmos', 'Osmosis', 'Dydx', 'Stargaze', 'Regen', 'Sommelier', 'Juno']; const [currentChainIndex, setCurrentChainIndex] = useState(0); const [isSignalIntentModalOpen, setIsSignalIntentModalOpen] = useState(false); @@ -64,14 +64,16 @@ const StakingIntent: React.FC = ({ address, isWalletConnecte }, {}) || {}; const validatorsWithDetails = - intent?.data?.intent.intents.map((validatorIntent: { valoper_address: string; weight: string }) => { - const validatorDetails = validatorsMap[validatorIntent.valoper_address]; - return { - moniker: validatorDetails?.moniker, - logoUrl: validatorDetails?.logoUrl, - percentage: `${(parseFloat(validatorIntent.weight) * 100).toFixed(2)}%`, - }; - }) || []; + intent?.data?.intent.intents + .filter((validatorIntent: { valoper_address: string; weight: string }) => parseFloat(validatorIntent.weight) > 0) + .map((validatorIntent: { valoper_address: string; weight: string }) => { + const validatorDetails = validatorsMap[validatorIntent.valoper_address]; + return { + moniker: validatorDetails?.moniker, + logoUrl: validatorDetails?.logoUrl, + percentage: `${(parseFloat(validatorIntent.weight) * 100).toFixed(2)}%`, + }; + }) || []; const handleLeftArrowClick = () => { setCurrentChainIndex((prevIndex) => (prevIndex === 0 ? networks.length - 1 : prevIndex - 1)); @@ -136,6 +138,7 @@ const StakingIntent: React.FC = ({ address, isWalletConnecte transform: 'scale(0.75)', color: 'complimentary.800', }} + color="GrayText" aria-label="Previous chain" icon={} onClick={handleLeftArrowClick} @@ -150,6 +153,7 @@ const StakingIntent: React.FC = ({ address, isWalletConnecte transform: 'scale(0.75)', color: 'complimentary.800', }} + color="GrayText" _hover={{ bgColor: 'transparent', color: 'complimentary.900' }} variant="ghost" aria-label="Next chain" diff --git a/web-ui/components/Assets/modals/intentMultiModal.tsx b/web-ui/components/Assets/modals/intentMultiModal.tsx index 15a7c9394..181ad9e8c 100644 --- a/web-ui/components/Assets/modals/intentMultiModal.tsx +++ b/web-ui/components/Assets/modals/intentMultiModal.tsx @@ -94,15 +94,13 @@ export const IntentMultiModal: React.FC = ({ - Choose which validator(s) you would like to liquid stake to. You can select from the list below or utilize the quick - select to pick the highest ranked validators. To learn more about rankings read the{' '} - Validator Selection Doc. + Choose which validator(s) you would like to liquid stake to. - {/* Positioning by default should be top right */} + {isLoading ? ( diff --git a/web-ui/components/Assets/modals/qTokenDepositModal.tsx b/web-ui/components/Assets/modals/qTokenDepositModal.tsx index d9e5d937c..9fd67dca5 100644 --- a/web-ui/components/Assets/modals/qTokenDepositModal.tsx +++ b/web-ui/components/Assets/modals/qTokenDepositModal.tsx @@ -18,11 +18,11 @@ import { StdFee, coins } from '@cosmjs/stargate'; import { ChainName } from '@cosmos-kit/core'; import { useChain, useManager } from '@cosmos-kit/react'; import BigNumber from 'bignumber.js'; -import { ibc } from 'quicksilverjs'; +import { ibc } from 'interchain-query'; import { useState, useMemo, useEffect } from 'react'; import { ChooseChain } from '@/components/react/choose-chain'; -import { handleSelectChainDropdown, ChainOption } from '@/components/types'; +import { handleSelectChainDropdown, ChainOption, ChooseChainInfo } from '@/components/types'; import { useTx } from '@/hooks'; import { useIbcBalanceQuery } from '@/hooks/useQueries'; import { ibcDenomDepositMapping } from '@/state/chains/prod'; @@ -47,7 +47,7 @@ const QDepositModal: React.FC = ({ token }) => { .filter((chainRecord) => desiredChains.includes(chainRecord.name)) .map((chainRecord) => ({ chainName: chainRecord?.name, - label: chainRecord?.chain.pretty_name, + label: chainRecord?.chain?.pretty_name, value: chainRecord?.name, icon: getChainLogo(chainRecord.name), })); @@ -66,7 +66,7 @@ const QDepositModal: React.FC = ({ token }) => { } }; - const chooseChain = ; + const chooseChain = ; const fromChain = chainName; const toChain = 'quicksilver'; @@ -76,8 +76,6 @@ const QDepositModal: React.FC = ({ token }) => { const { address: qAddress } = useChain('quicksilver'); const { balance } = useIbcBalanceQuery(fromChain ?? '', address ?? ''); const { tx } = useTx(fromChain ?? ''); - const qckBalance = - balance?.balances.find((b) => b.denom === 'ibc/635CB83EF1DFE598B10A3E90485306FD0D47D34217A4BE5FD9977FA010A5367D')?.amount ?? ''; const onSubmitClick = async () => { setIsLoading(true); @@ -90,7 +88,7 @@ const QDepositModal: React.FC = ({ token }) => { gas: '300000', }; - const { sourcePort, sourceChannel } = getIbcInfo(fromChain ?? '', toChain ?? ''); + const { source_port, source_channel } = getIbcInfo(fromChain ?? '', toChain ?? ''); // Function to get the correct IBC denom trace based on chain and token type ChainDenomMappingKeys = keyof typeof ibcDenomDepositMapping; @@ -130,15 +128,14 @@ const QDepositModal: React.FC = ({ token }) => { const timeoutInNanos = (stamp + 1.2e6) * 1e6; const msg = transfer({ - sourcePort, - sourceChannel, + sourcePort: source_port, + sourceChannel: source_channel, sender: address ?? '', receiver: qAddress ?? '', token: ibcToken, timeoutHeight: undefined, //@ts-ignore timeoutTimestamp: timeoutInNanos, - memo: '', }); await tx([msg], { diff --git a/web-ui/components/Assets/modals/qTokenWithdrawlModal.tsx b/web-ui/components/Assets/modals/qTokenWithdrawlModal.tsx index 9b629104f..29d8489a3 100644 --- a/web-ui/components/Assets/modals/qTokenWithdrawlModal.tsx +++ b/web-ui/components/Assets/modals/qTokenWithdrawlModal.tsx @@ -18,11 +18,11 @@ import { StdFee, coins } from '@cosmjs/stargate'; import { ChainName } from '@cosmos-kit/core'; import { useChain, useManager } from '@cosmos-kit/react'; import BigNumber from 'bignumber.js'; -import { ibc } from 'quicksilverjs'; +import { ibc } from 'interchain-query'; import { useState, useMemo, useEffect } from 'react'; import { ChooseChain } from '@/components/react/choose-chain'; -import { handleSelectChainDropdown, ChainOption } from '@/components/types'; +import { handleSelectChainDropdown, ChainOption, ChooseChainInfo } from '@/components/types'; import { useTx } from '@/hooks'; import { useIbcBalanceQuery } from '@/hooks/useQueries'; import { ibcDenomWithdrawMapping } from '@/state/chains/prod'; @@ -47,7 +47,7 @@ const QWithdrawModal: React.FC = ({ token }) => { .filter((chainRecord) => desiredChains.includes(chainRecord.name)) .map((chainRecord) => ({ chainName: chainRecord?.name, - label: chainRecord?.chain.pretty_name, + label: chainRecord?.chain?.pretty_name, value: chainRecord?.name, icon: getChainLogo(chainRecord.name), })); @@ -66,7 +66,7 @@ const QWithdrawModal: React.FC = ({ token }) => { } }; - const chooseChain = ; + const chooseChain = ; const fromChain = 'quicksilver'; const toChain = chainName; @@ -88,7 +88,7 @@ const QWithdrawModal: React.FC = ({ token }) => { gas: '300000', }; - const { sourcePort, sourceChannel } = getIbcInfo(fromChain ?? '', toChain ?? ''); + const { source_port, source_channel } = getIbcInfo(fromChain ?? '', toChain ?? ''); // Function to get the correct IBC denom trace based on chain and token type ChainDenomMappingKeys = keyof typeof ibcDenomWithdrawMapping; @@ -128,15 +128,14 @@ const QWithdrawModal: React.FC = ({ token }) => { const timeoutInNanos = (stamp + 1.2e6) * 1e6; const msg = transfer({ - sourcePort, - sourceChannel, + sourcePort: source_port, + sourceChannel: source_channel, sender: qAddress ?? '', receiver: address ?? '', token: ibcToken, timeoutHeight: undefined, //@ts-ignore timeoutTimestamp: timeoutInNanos, - memo: '', }); await tx([msg], { diff --git a/web-ui/components/Assets/modals/qckDepositModal.tsx b/web-ui/components/Assets/modals/qckDepositModal.tsx index 6cef3519f..d0a576627 100644 --- a/web-ui/components/Assets/modals/qckDepositModal.tsx +++ b/web-ui/components/Assets/modals/qckDepositModal.tsx @@ -18,11 +18,11 @@ import { ChainName } from '@cosmos-kit/core'; import { useChain, useManager } from '@cosmos-kit/react'; import BigNumber from 'bignumber.js'; import { assets, chains } from 'chain-registry'; -import { ibc } from 'quicksilverjs'; +import { ibc } from 'interchain-query'; import { useState, useMemo, useEffect } from 'react'; import { ChooseChain } from '@/components/react/choose-chain'; -import { handleSelectChainDropdown, ChainOption } from '@/components/types'; +import { handleSelectChainDropdown, ChainOption, ChooseChainInfo } from '@/components/types'; import { useTx } from '@/hooks'; import { useIbcBalanceQuery } from '@/hooks/useQueries'; import { getIbcInfo, shiftDigits } from '@/utils'; @@ -40,7 +40,7 @@ export function DepositModal() { .filter((chainRecord) => chainRecord.name === 'osmosis') .map((chainRecord) => ({ chainName: chainRecord?.name, - label: chainRecord?.chain.pretty_name, + label: chainRecord?.chain?.pretty_name, value: chainRecord?.name, icon: getChainLogo(chainRecord.name), })); @@ -59,7 +59,7 @@ export function DepositModal() { } }; - const chooseChain = ; + const chooseChain = ; const fromChain = chainName; const toChain = 'quicksilver'; @@ -91,7 +91,7 @@ export function DepositModal() { gas: '500000', }; - const { sourcePort, sourceChannel } = getIbcInfo(fromChain ?? '', toChain ?? ''); + const { source_port, source_channel } = getIbcInfo(fromChain ?? '', toChain ?? ''); const token = { denom: 'ibc/635CB83EF1DFE598B10A3E90485306FD0D47D34217A4BE5FD9977FA010A5367D', @@ -102,16 +102,14 @@ export function DepositModal() { const timeoutInNanos = (stamp + 1.2e6) * 1e6; const msg = transfer({ - sourcePort, - sourceChannel, + sourcePort: source_port, + sourceChannel: source_channel, sender: address ?? '', receiver: qAddress ?? '', token, - //@ts-ignore - timeoutHeight: 0, + timeoutHeight: undefined, //@ts-ignore timeoutTimestamp: timeoutInNanos, - memo: '', }); await tx([msg], { diff --git a/web-ui/components/Assets/modals/qckWithdrawModal.tsx b/web-ui/components/Assets/modals/qckWithdrawModal.tsx index 171671e41..590af5c7f 100644 --- a/web-ui/components/Assets/modals/qckWithdrawModal.tsx +++ b/web-ui/components/Assets/modals/qckWithdrawModal.tsx @@ -17,11 +17,11 @@ import { StdFee, coins } from '@cosmjs/stargate'; import { ChainName } from '@cosmos-kit/core'; import { useChain, useManager } from '@cosmos-kit/react'; import BigNumber from 'bignumber.js'; -import { ibc } from 'quicksilverjs'; +import { ibc } from 'interchain-query'; import { useState, useMemo, useEffect } from 'react'; import { ChooseChain } from '@/components/react/choose-chain'; -import { handleSelectChainDropdown, ChainOption } from '@/components/types'; +import { handleSelectChainDropdown, ChainOption, ChooseChainInfo } from '@/components/types'; import { useTx } from '@/hooks'; import { getCoin, getIbcInfo } from '@/utils'; @@ -38,7 +38,7 @@ export function WithdrawModal() { .filter((chainRecord) => chainRecord.name === 'osmosis') .map((chainRecord) => ({ chainName: chainRecord?.name, - label: chainRecord?.chain.pretty_name, + label: chainRecord?.chain?.pretty_name, value: chainRecord?.name, icon: getChainLogo(chainRecord.name), })); @@ -57,7 +57,7 @@ export function WithdrawModal() { } }; - const chooseChain = ; + const chooseChain = ; const fromChain = 'quicksilver'; const toChain = chainName; @@ -79,7 +79,7 @@ export function WithdrawModal() { gas: '300000', }; - const { sourcePort, sourceChannel } = getIbcInfo(fromChain ?? '', toChain ?? ''); + const { source_port, source_channel } = getIbcInfo(fromChain ?? '', toChain ?? ''); const token = { denom: 'uqck', @@ -90,15 +90,14 @@ export function WithdrawModal() { const timeoutInNanos = (stamp + 1.2e6) * 1e6; const msg = transfer({ - sourcePort, - sourceChannel, + sourcePort: source_port, + sourceChannel: source_channel, sender: qAddress ?? '', receiver: address ?? '', token, timeoutHeight: undefined, //@ts-ignore timeoutTimestamp: timeoutInNanos, - memo: '', }); await tx([msg], { diff --git a/web-ui/components/Assets/modals/signalIntentProcess.tsx b/web-ui/components/Assets/modals/signalIntentProcess.tsx index 5fcaa24fd..6f7aa8d72 100644 --- a/web-ui/components/Assets/modals/signalIntentProcess.tsx +++ b/web-ui/components/Assets/modals/signalIntentProcess.tsx @@ -101,11 +101,7 @@ export const SignalIntentModal: React.FC = ({ isOpen, onClose }; const retreatStep = () => { - if (step === 3) { - setStep(1); // If on step 3 and checkbox is checked, go back to step 1 - } else { - setStep((prevStep) => Math.max(prevStep - 1, 1)); // Otherwise, go to the previous step - } + setStep((prevStep) => Math.max(prevStep - 1, 1)); // Otherwise, go to the previous step }; const totalWeights = 1; @@ -201,7 +197,7 @@ export const SignalIntentModal: React.FC = ({ isOpen, onClose setIsSigning(true); try { - const result = await tx([msgSignalIntent], { + await tx([msgSignalIntent], { fee, onSuccess: () => { refetch(); @@ -327,9 +323,9 @@ export const SignalIntentModal: React.FC = ({ isOpen, onClose mt={2} color="white" _hover={{ - bgColor: 'rgba(255, 128, 0, 0.25)', + bgColor: 'rgba(255, 128, 0, 0.5)', }} - variant="ghost" + bgColor="rgba(255, 128, 0, 0.25)" width="35%" size="xs" onClick={() => setModalOpen(true)} @@ -343,7 +339,7 @@ export const SignalIntentModal: React.FC = ({ isOpen, onClose )} - + + + {/* */} diff --git a/web-ui/components/Assets/unbondingTable.tsx b/web-ui/components/Assets/unbondingTable.tsx index 74dbe9978..3b7660925 100644 --- a/web-ui/components/Assets/unbondingTable.tsx +++ b/web-ui/components/Assets/unbondingTable.tsx @@ -12,8 +12,18 @@ const statusCodes = new Map([ [5, 'COMPLETED'], ]); -const formatDate = (dateString: string | number | Date) => { - return new Date(dateString).toLocaleDateString(undefined); +const formatDateAndTime = (dateString: string | number | Date) => { + const date = new Date(dateString); + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + + hour12: false, + }; + return date.toLocaleString(undefined, options); }; const formatDenom = (denom: string) => { @@ -26,9 +36,10 @@ interface UnbondingAssetsTableProps { } const UnbondingAssetsTable: React.FC = ({ address, isWalletConnected }) => { - const chains = ['Cosmos', 'Stargaze', 'Osmosis', 'Regen', 'Sommelier', 'Juno']; + const chains = ['Cosmos', 'Stargaze', 'Osmosis', 'Regen', 'Sommelier', 'Juno', 'Dydx']; const [currentChainIndex, setCurrentChainIndex] = useState(0); + // Switcher lets us use a pretty name for the chain in the UI, but query the chain by its actual name. const currentChainName = chains[currentChainIndex]; let newChainName: string | undefined; if (currentChainName === 'Cosmos') { @@ -43,6 +54,8 @@ const UnbondingAssetsTable: React.FC = ({ address, is newChainName = 'sommelier'; } else if (currentChainName === 'Juno') { newChainName = 'juno'; + } else if (currentChainName === 'Dydx') { + newChainName = 'dydx'; } else { // Default case newChainName = currentChainName; @@ -58,6 +71,12 @@ const UnbondingAssetsTable: React.FC = ({ address, is const handleRightArrowClick = () => { setCurrentChainIndex((prevIndex: number) => (prevIndex === chains.length - 1 ? 0 : prevIndex + 1)); }; + + const hideOnMobile = { + base: 'none', + md: 'table-cell', + }; + const noUnbondingAssets = isWalletConnected && unbondingData?.withdrawals.length === 0; if (!isWalletConnected) { return ( @@ -241,16 +260,16 @@ const UnbondingAssetsTable: React.FC = ({ address, is Burn Amount - + Status Redemption Amount - + Epoch Number - + Completion Time @@ -261,17 +280,21 @@ const UnbondingAssetsTable: React.FC = ({ address, is {Number(shiftDigits(withdrawal.burn_amount.amount, -6))} {formatDenom(withdrawal.burn_amount.denom)} - {statusCodes.get(withdrawal.status)} + + {statusCodes.get(withdrawal.status)} + {withdrawal.amount.map((amt) => `${shiftDigits(amt.amount, -6)} ${formatDenom(amt.denom)}`).join(', ')} - {withdrawal.epoch_number} - + + {withdrawal.epoch_number} + + {withdrawal.status === 2 ? 'Pending' : withdrawal.status === 4 - ? 'A few moments' - : formatDate(withdrawal.completion_time)} + ? 'A few moments' + : formatDateAndTime(withdrawal.completion_time)} ))} diff --git a/web-ui/components/Defi/defiBox.tsx b/web-ui/components/Defi/defiBox.tsx index a13f985c1..b97b98f7e 100644 --- a/web-ui/components/Defi/defiBox.tsx +++ b/web-ui/components/Defi/defiBox.tsx @@ -17,6 +17,7 @@ import { Tooltip, Center, Spinner, + useBreakpointValue, } from '@chakra-ui/react'; import React, { useState, useEffect } from 'react'; @@ -97,8 +98,8 @@ const DefiTable = () => { return key in providerIcons; }; - const [sortColumn, setSortColumn] = useState(null); - const [sortOrder, setSortOrder] = useState('asc'); + const [sortColumn, setSortColumn] = useState('apy'); + const [sortOrder, setSortOrder] = useState('desc'); const sortData = (data: DefiData[], column: SortableColumn | null, order: SortOrder) => { if (!column) return data; @@ -178,68 +179,74 @@ const DefiTable = () => { ))} )} - + - - } + {!isMobile && ( + - + )} + {!isMobile && ( + + )} + + )} + + @@ -261,17 +268,24 @@ const DefiTable = () => { {defi && sortedData.map((asset, index) => ( - - - + {!isMobile && ( + + )} + {!isMobile && ( + + )} + {!isMobile && ( + + )} + + - {sortedValidators.length === 0 - ? Array.from({ length: 5 }).map((_, index) => ( - - + {' '} + + + ) : ( + sortedValidators.map((validator, index) => { + const votingPowerPercentage = totalVotingPower > 0 ? ((validator.votingPower || 0) / totalVotingPower) * 100 : 0; + const validatorLogo = logos[validator.address]; + return ( + + onValidatorClick({ + name: validator.name || '', + operatorAddress: validator.address || '', + }) + } + backgroundColor={ + selectedValidators.some((v) => v.name === validator.name) ? 'rgba(255, 128, 0, 0.25)' : 'transparent' + } + > + - - - )) - : sortedValidators.map((validator, index) => { - const votingPowerPercentage = totalVotingPower > 0 ? ((validator.votingPower || 0) / totalVotingPower) * 100 : 0; - const validatorLogo = logos[validator.address]; - return ( - - onValidatorClick({ - name: validator.name || '', - operatorAddress: validator.address || '', - }) - } - backgroundColor={ - selectedValidators.some((v) => v.name === validator.name) ? 'rgba(255, 128, 0, 0.25)' : 'transparent' - } - style={{ maxHeight: '50px' }} - > - - - - - - ); - })} + ); + }) + )}
Asset Pair handleSort('apy')} - style={{ cursor: 'pointer' }} - > - APY{' '} - {sortColumn === 'apy' ? ( - sortOrder === 'asc' ? ( - + {!isMobile && Asset Pair handleSort('apy')} + style={{ cursor: 'pointer' }} + > + APY{' '} + {sortColumn === 'apy' ? ( + sortOrder === 'asc' ? ( + + ) : ( + + ) ) : ( - ) - ) : ( - - )} - handleSort('tvl')} - > - TVL{' '} - {sortColumn === 'tvl' ? ( - sortOrder === 'asc' ? ( - + )} + handleSort('tvl')} + > + TVL{' '} + {sortColumn === 'tvl' ? ( + sortOrder === 'asc' ? ( + + ) : ( + + ) ) : ( - ) - ) : ( - - )} - Provider Action
- - {asset.assetPair} - - - {formatApy(asset.apy)} - - ${asset.tvl.toLocaleString()} - + + {asset.assetPair} + + + {formatApy(asset.apy)} + + ${asset.tvl.toLocaleString()} + {isProviderKey(asset.provider.toLowerCase()) && ( @@ -286,6 +300,7 @@ const DefiTable = () => { )} + + )} + {step === 2 && ( + <> + + Your shares have been successfully reverted back to tokens and should arrive in your wallet. + + + )} + + + + + + ); +}; +export default RevertSharesProcessModal; diff --git a/web-ui/components/Staking/modals/stakingProcessModal.tsx b/web-ui/components/Staking/modals/stakingProcessModal.tsx index aaeb229cd..f32178416 100644 --- a/web-ui/components/Staking/modals/stakingProcessModal.tsx +++ b/web-ui/components/Staking/modals/stakingProcessModal.tsx @@ -19,11 +19,11 @@ import { Checkbox, } from '@chakra-ui/react'; import { coins, StdFee } from '@cosmjs/amino'; -import { useChain } from '@cosmos-kit/react'; import styled from '@emotion/styled'; import { bech32 } from 'bech32'; -import { assets, chains } from 'chain-registry'; -import { cosmos } from 'interchain-query'; +import { assets } from 'chain-registry'; +import chains from 'chain-registry'; +import { cosmos } from 'quicksilverjs'; import React, { useEffect, useState } from 'react'; @@ -74,10 +74,11 @@ interface StakingModalProps { chainName: string; chainId: string; }; + address: string; } -export const StakingProcessModal: React.FC = ({ isOpen, onClose, selectedOption, tokenAmount }) => { - const [step, setStep] = React.useState(1); +export const StakingProcessModal: React.FC = ({ isOpen, onClose, selectedOption, tokenAmount, address }) => { + const [step, setStep] = useState(1); const getProgressColor = (circleStep: number) => { if (step >= circleStep) return 'complimentary.900'; return 'rgba(255,255,255,0.2)'; @@ -102,12 +103,10 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo newChainName = selectedOption?.chainName; } - const { address } = useChain(newChainName || ''); - const labels = ['Choose validators', `Set weights`, `Sign & Submit`, `Receive q${selectedOption?.value}`]; const [isModalOpen, setModalOpen] = useState(false); - const [selectedValidators, setSelectedValidators] = React.useState<{ name: string; operatorAddress: string }[]>([]); + const [selectedValidators, setSelectedValidators] = useState<{ name: string; operatorAddress: string }[]>([]); const advanceStep = () => { if (selectedValidators.length > 0) { @@ -199,7 +198,7 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo }; }); - const { data: zone } = useZoneQuery(selectedOption?.chainId ?? ''); + const { data: zone, isLoading: isZoneLoading } = useZoneQuery(selectedOption?.chainId ?? ''); const valToByte = (val: number) => { if (val > 1) { @@ -235,7 +234,13 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo numericAmount = 0; } - const smallestUnitAmount = numericAmount * Math.pow(10, 6); + let smallestUnitAmount: number; + + if (zone?.chainId === 'dydx-mainnet-1') { + smallestUnitAmount = numericAmount * Math.pow(10, 18); + } else { + smallestUnitAmount = numericAmount * Math.pow(10, 6); + } const { send } = cosmos.bank.v1beta1.MessageComposer.withTypeUrl; @@ -246,7 +251,7 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo }); const mainTokens = assets.find(({ chain_name }) => chain_name === newChainName); - const fees = chains.find(({ chain_name }) => chain_name === newChainName)?.fees?.fee_tokens; + const fees = chains.chains.find(({ chain_name }) => chain_name === newChainName)?.fees?.fee_tokens; const mainDenom = mainTokens?.assets[0].base ?? ''; let feeAmount; if (selectedOption?.chainName === 'sommelier') { @@ -275,7 +280,7 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo setIsSigning(true); setTransactionStatus('Pending'); try { - const result = await tx([msgSend], { + await tx([msgSend], { memo, fee, onSuccess: () => { @@ -341,10 +346,6 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo } }; - type Weights = { - [key: string]: number; - }; - const handleEqualWeightAssignment = () => { const numberOfValidators = selectedValidators.length; const equalWeight = (1 / numberOfValidators).toFixed(4); @@ -358,8 +359,8 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo return ( - - + + {/* Left Section */} @@ -435,9 +436,9 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo mt={2} color="white" _hover={{ - bgColor: 'rgba(255, 128, 0, 0.25)', + bgColor: 'rgba(255, 128, 0, 0.5)', }} - variant="ghost" + bgColor="rgba(255, 128, 0, 0.25)" width="35%" size="xs" onClick={() => setModalOpen(true)} @@ -451,7 +452,7 @@ export const StakingProcessModal: React.FC = ({ isOpen, onClo )} - + */} = ({ validators, onValidatorClick, selectedValidators, searchTerm, logos }) => { - const [sortedValidators, setSortedValidators] = React.useState([]); - const [sortBy, setSortBy] = React.useState(null); - const [sortOrder, setSortOrder] = React.useState<'asc' | 'desc'>('asc'); + const [sortedValidators, setSortedValidators] = useState([]); + const [sortBy, setSortBy] = useState(null); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); const handleSort = (column: string) => { if (sortBy === column) { @@ -38,16 +23,16 @@ export const ValidatorsTable: React.FC<{ } }; - const [totalVotingPower, setTotalVotingPower] = React.useState(0); + const [totalVotingPower, setTotalVotingPower] = useState(0); - React.useEffect(() => { + useEffect(() => { const totalVP = validators.reduce((acc, validator) => { return acc + (validator.votingPower || 0); }, 0); setTotalVotingPower(totalVP); }, [validators]); - React.useEffect(() => { + useEffect(() => { let filteredValidators = [...validators]; if (searchTerm) { @@ -167,88 +152,72 @@ export const ValidatorsTable: React.FC<{
- - + {sortedValidators.length === 0 && searchTerm ? ( +
+ No matches found +
+ {!validatorLogo && ( + + )} + {validatorLogo && ( + {validator.name} + )} + {(validator.name.length || 0) > 20 ? validator.name.substring(0, 14) + '...' : validator.name || ''} - + + {validator.commission ? validator.commission : 'N/A'} - + + {`${votingPowerPercentage.toFixed(2)}%`}
- {!validatorLogo && ( - - )} - {validatorLogo && ( - {validator.name} - )} - {(validator.name.length || 0) > 20 ? validator.name.substring(0, 14) || '' + '...' : validator.name || ''} - - {validator.commission ? validator.commission : 'N/A'} - - {`${votingPowerPercentage.toFixed(2)}%`} -
diff --git a/web-ui/components/Staking/networkSelectButton.tsx b/web-ui/components/Staking/networkSelectButton.tsx index 9cd99a909..428a79dd4 100644 --- a/web-ui/components/Staking/networkSelectButton.tsx +++ b/web-ui/components/Staking/networkSelectButton.tsx @@ -2,7 +2,8 @@ import { ChevronDownIcon } from '@chakra-ui/icons'; import { Menu, MenuButton, MenuList, MenuItem, Button, Flex, Image, Text, useDisclosure } from '@chakra-ui/react'; -import React from 'react'; +import axios from 'axios'; +import React, { useEffect, useState } from 'react'; import { networks as prodNetworks, testNetworks as devNetworks } from '@/state/chains/prod'; @@ -14,6 +15,15 @@ interface CustomMenuProps { setSelectedNetwork: (network: (typeof networks)[0]) => void; } +type Network = { + value: string; + logo: string; + qlogo: string; + name: string; + chainName: string; + chainId: string; +}; + export const NetworkSelect: React.FC = ({ buttonTextColor = 'white', selectedOption, setSelectedNetwork }) => { const handleOptionClick = (network: (typeof networks)[0]) => { setSelectedNetwork(network); @@ -33,6 +43,29 @@ export const NetworkSelect: React.FC = ({ buttonTextColor = 'wh const { isOpen } = useDisclosure(); + const fetchLiveZones = async () => { + try { + const response = await axios.get(`${process.env.NEXT_PUBLIC_QUICKSILVER_API}/quicksilver/interchainstaking/v1/zones`); + const liveZones = response.data.zones.map((zone: { chain_id: any }) => zone.chain_id); + return liveZones; + } catch (error) { + console.error('Failed to fetch live zones:', error); + return []; + } + }; + + const [liveNetworks, setLiveNetworks] = useState([]); + + useEffect(() => { + const getLiveZones = async () => { + const liveZones = await fetchLiveZones(); + const filteredNetworks = networks.filter((network) => liveZones.includes(network.chainId)); + setLiveNetworks(filteredNetworks); + }; + + getLiveZones(); + }, []); + return ( = ({ buttonTextColor = 'wh {selectedOption.value.toUpperCase()} - {networks.map((network) => ( + {liveNetworks.map((network) => ( void; isTransferModalOpen: boolean; setTransferModalOpen: (isOpen: boolean) => void; + isRevertSharesModalOpen: boolean; + setRevertSharesModalOpen: (isOpen: boolean) => void; setBalance: (balance: string) => void; setQBalance: (qBalance: string) => void; }; @@ -71,6 +74,8 @@ export const StakingBox = ({ setStakingModalOpen, isTransferModalOpen, setTransferModalOpen, + isRevertSharesModalOpen, + setRevertSharesModalOpen, setBalance, setQBalance, }: StakingBoxProps) => { @@ -83,6 +88,9 @@ export const StakingBox = ({ const openTransferModal = () => setTransferModalOpen(true); const closeTransferModal = () => setTransferModalOpen(false); + const openRevertSharesModal = () => setRevertSharesModalOpen(true); + const closeRevertSharesModal = () => setRevertSharesModalOpen(false); + const { address } = useChain(selectedOption.chainName); const { address: qAddress } = useChain('quicksilver'); @@ -98,7 +106,7 @@ export const StakingBox = ({ const baseBalance = shiftDigits(balance?.balance?.amount || '0', -exp); - const { data: zone } = useZoneQuery(selectedOption.chainId); + const { data: zone, isLoading: isZoneLoading } = useZoneQuery(selectedOption.chainId); useEffect(() => { setQBalance(qAssets); @@ -125,7 +133,8 @@ export const StakingBox = ({ const [inputError, setInputError] = useState(false); - const qAssetsExponent = shiftDigits(qAssets, -6); + const exponent = qBalance?.balance.denom === 'aqdydx' ? -18 : -6; + const qAssetsExponent = shiftDigits(qAssets, exponent); const qAssetsDisplay = qAssetsExponent.includes('.') ? qAssetsExponent.substring(0, qAssetsExponent.indexOf('.') + 3) : qAssetsExponent; const maxUnstakingAmount = truncateToThreeDecimals(Number(qAssetsDisplay)); @@ -162,7 +171,7 @@ export const StakingBox = ({ event.preventDefault(); setIsSigning(true); try { - const result = await tx([msgRequestRedemption], { + await tx([msgRequestRedemption], { fee, }); } catch (error) { @@ -172,13 +181,29 @@ export const StakingBox = ({ } }; + // DO NOT REMOVE THESE COMMENTS + // import { useToaster, ToastType, type CustomToast } from '@/hooks/useToaster'; + // import useToast from chakra-ui + // You can use this Toast handler and the below message to show there is an issue with unbonding + // const toaster = useToaster(); + const handleTabsChange = (index: number) => { setActiveTabIndex(index); setTokenAmount(''); + // You can use this Toast Msg to show there is an issue with unbonding + // if (index === 1) { + // toaster.toast({ + // type: ToastType.Error, + // title: 'Issues with unbonding', + // message: 'Unbondings can be submitted but are currently not being processed and will be queued until the issue is resolved.', + // }); + // } }; const { delegations, delegationsIsError, delegationsIsLoading } = useNativeStakeQuery(selectedOption.chainName, address ?? ''); + const delegationsResponse = delegations?.delegation_responses; + const nativeStakedAmount = delegationsResponse?.reduce((acc: number, delegationResponse: { balance: { amount: any } }) => { const amount = Number(delegationResponse?.balance?.amount) || 0; return acc + amount; @@ -244,17 +269,21 @@ export const StakingBox = ({ // Combine delegationsResponse with valoper entries from allBalances const combinedDelegations = safeDelegationsResponse.concat( + // @ts-ignore safeAllBalances .filter((balance) => balance.denom.includes('valoper')) .map((balance) => { const [validatorAddress, uniqueId] = balance.denom.split('/'); return { delegation: { + delegator_address: '', validator_address: validatorAddress, unique_id: uniqueId, + shares: '', }, balance: { amount: balance.amount, + denom: balance.denom, }, isTokenized: true, denom: balance.denom, @@ -302,6 +331,7 @@ export const StakingBox = ({ + {/* Staking TabPanel */} @@ -311,20 +341,20 @@ export const StakingBox = ({ {selectedOption.name === 'Cosmos Hub' && ( - {(nativeStakedAmount > 0 || hasTokenized) && ( + {((nativeStakedAmount ?? 0) > 0 || hasTokenized) && ( 0 - ? `You currently have ${shiftDigits(nativeStakedAmount, -6)} ${ - selectedOption.value - } natively staked to ${delegationsResponse?.length} validators.` - : hasTokenized - ? 'You have tokenized shares available for transfer.' - : '' + ? "You don't have any native staked tokens or tokenized shares." + : nativeStakedAmount ?? 0 > 0 + ? `You currently have ${shiftDigits(nativeStakedAmount ?? '', -6)} ${ + selectedOption.value + } natively staked to ${delegationsResponse?.length} validators.` + : hasTokenized + ? 'You have tokenized shares available for transfer.' + : '' } > @@ -434,19 +464,12 @@ export const StakingBox = ({ ) : ( - {address ? ( - balance?.balance?.amount && Number(balance?.balance?.amount) !== 0 ? ( - `${truncatedBalance} ${selectedOption.value.toUpperCase()}` - ) : ( - - Get {selectedOption.value.toUpperCase()} tokens here - - ) + {balance?.balance?.amount && Number(balance.balance.amount) > 0 ? ( + `${truncatedBalance} ${selectedOption.value.toUpperCase()}` ) : ( - '0' + + Get {selectedOption.value.toUpperCase()} tokens here + )} )} @@ -498,7 +521,7 @@ export const StakingBox = ({ - + What you'll get q{selectedOption.value.toUpperCase()}: @@ -506,7 +529,11 @@ export const StakingBox = ({ {/* This pushes the next Stat component to the right */} - {(Number(tokenAmount) / (Number(zone?.redemptionRate) || 1)).toFixed(2)} + {!isZoneLoading ? ( + (Number(tokenAmount) / Number(zone?.redemptionRate || 1)).toFixed(2) + ) : ( + + )} @@ -532,6 +559,7 @@ export const StakingBox = ({ isOpen={isStakingModalOpen} onClose={closeStakingModal} selectedOption={selectedOption} + address={address ?? ''} /> )} @@ -542,6 +570,7 @@ export const StakingBox = ({ {/* Combine delegationsResponse with valoper entries from allBalances */} {combinedDelegations.map( + // @ts-ignore ( delegation: { delegation: { validator_address: string | number; unique_id: any }; @@ -576,8 +605,8 @@ export const StakingBox = ({ key={uniqueKey} mb={2} > - - + + {!validatorLogo ? ( ) : ( @@ -589,25 +618,45 @@ export const StakingBox = ({ borderRadius={'full'} /> )} - - - - {validator?.name ?? 'Validator'} - {delegation.isTokenized && ( - - - - - - )} - - - {shiftDigits(delegation.balance.amount, -6)} {selectedOption.value} - - + + + {validator?.name ?? 'Validator'} + {delegation.isTokenized && ( + + + + + + )} + + + {shiftDigits(delegation.balance.amount, -6)} {selectedOption.value} + + + + {isSelected && delegation.isTokenized && ( + + )} ); @@ -646,11 +695,32 @@ export const StakingBox = ({ isTokenized={selectedValidatorData.isTokenized} denom={selectedValidatorData.denom} /> + )} + {/* Unstake TabPanel */} + { selectedOption.value.toUpperCase() == "DYDX" && ( + + + + + DyDx unstaking will be live very soon! + + + + + ) || ( @@ -775,7 +845,7 @@ export const StakingBox = ({ - + What you'll get {selectedOption.value.toUpperCase()}: @@ -783,7 +853,11 @@ export const StakingBox = ({ {/* This pushes the next Stat component to the right */} - {(Number(tokenAmount) * Number(zone?.redemptionRate || 1)).toFixed(2)} + {!isZoneLoading ? ( + (Number(tokenAmount) * Number(zone?.redemptionRate || 1)).toFixed(2) + ) : ( + + )} @@ -809,6 +883,7 @@ export const StakingBox = ({ + )} diff --git a/web-ui/components/react/accountControlModal.tsx b/web-ui/components/react/accountControlModal.tsx new file mode 100644 index 000000000..5c9d8cc7a --- /dev/null +++ b/web-ui/components/react/accountControlModal.tsx @@ -0,0 +1,409 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, + Button, + Text, + Spacer, + Spinner, + Flex, + Box, +} from '@chakra-ui/react'; +import { StdFee } from '@cosmjs/amino'; +import { useChain } from '@cosmos-kit/react'; +import { assets, chains } from 'chain-registry'; +import { quicksilver, cosmos } from 'quicksilverjs'; +import { GenericAuthorization } from 'quicksilverjs/dist/codegen/cosmos/authz/v1beta1/authz'; +import { useState } from 'react'; + +import { useTx } from '@/hooks'; +import { useAuthChecker, useIncorrectAuthChecker } from '@/hooks/useQueries'; + +interface AccountControlModalProps { + isOpen: boolean; + onClose: () => void; +} + +export const AccountControlModal: React.FC = ({ isOpen, onClose }) => { + const [authzSection, setAuthzSection] = useState(false); + const [lsmSection, setLsmSection] = useState(false); + + const { address: lsmAddress } = useChain('cosmoshub'); + const { tx: lsmTx } = useTx('cosmoshub'); + + const { address: authAddress } = useChain('quicksilver'); + const { tx: authTx } = useTx('quicksilver'); + + const { authData: incorrectAccount } = useIncorrectAuthChecker(authAddress ?? ''); + const { authData: correctAccount } = useAuthChecker(authAddress ?? ''); + + const { enableTokenizeShares } = cosmos.staking.v1beta1.MessageComposer.withTypeUrl; + const { disableTokenizeShares } = cosmos.staking.v1beta1.MessageComposer.withTypeUrl; + + const msgEnable = enableTokenizeShares({ + delegatorAddress: lsmAddress ?? '', + }); + + const msgDisable = disableTokenizeShares({ + delegatorAddress: lsmAddress ?? '', + }); + + const [isSigningEnable, setIsSigningEnable] = useState(false); + const [isSigningDisable, setIsSingingDisable] = useState(false); + const [isError, setIsError] = useState(false); + + const mainTokens = assets.find(({ chain_name }) => chain_name === chain_name); + const fees = chains.find(({ chain_name }) => chain_name === chain_name)?.fees?.fee_tokens; + const mainDenom = mainTokens?.assets[0].base ?? ''; + const fixedMinGasPrice = fees?.find(({ denom }) => denom === mainDenom)?.high_gas_price ?? ''; + const feeAmount = Number(fixedMinGasPrice) * 750000; + + const fee: StdFee = { + amount: [ + { + denom: mainDenom, + amount: feeAmount.toString(), + }, + ], + gas: '750000', // test txs were using well in excess of 600k + }; + + const handleDisable = async (event: React.MouseEvent) => { + event.preventDefault(); + setIsSingingDisable(true); + + try { + await lsmTx([msgDisable], { + fee, + onSuccess: () => { + onClose(); + }, + }); + } catch (error) { + console.error('Transaction failed', error); + + setIsError(true); + } finally { + setIsSingingDisable(false); + } + }; + + const handleEnable = async (event: React.MouseEvent) => { + event.preventDefault(); + setIsSigningEnable(true); + + try { + await lsmTx([msgEnable], { + fee, + onSuccess: () => { + onClose(); + }, + }); + } catch (error) { + console.error('Transaction failed', error); + + setIsError(true); + } finally { + setIsSigningEnable(false); + } + }; + + const { grant, revoke } = cosmos.authz.v1beta1.MessageComposer.withTypeUrl; + + const genericAuth = { + msg: quicksilver.participationrewards.v1.MsgSubmitClaim.typeUrl, + }; + + const binaryMessage = GenericAuthorization.encode(genericAuth).finish(); + const msgGrant = grant({ + granter: authAddress ?? '', + grantee: 'quick1psevptdp90jad76zt9y9x2nga686hutgmasmwd', + grant: { + authorization: { + typeUrl: cosmos.authz.v1beta1.GenericAuthorization.typeUrl, + value: binaryMessage, + }, + }, + }); + + const revokeGrant = revoke({ + granter: authAddress ?? '', + grantee: 'quick1psevptdp90jad76zt9y9x2nga686hutgmasmwd', + + msgTypeUrl: quicksilver.participationrewards.v1.MsgSubmitClaim.typeUrl, + }); + + const msgRevokeBad = revoke({ + granter: authAddress ?? '', + grantee: 'quick1w5ennfhdqrpyvewf35sv3y3t8yuzwq29mrmyal', + msgTypeUrl: quicksilver.participationrewards.v1.MsgSubmitClaim.typeUrl, + }); + + const handleAutoClaimRewards = async (event: React.MouseEvent) => { + event.preventDefault(); + setIsSigningEnable(true); + + try { + await authTx([msgGrant], { + fee, + onSuccess: () => {}, + }); + } catch (error) { + console.error('Transaction failed', error); + + setIsError(true); + } finally { + setIsSigningEnable(false); + } + }; + + const handleRemoveAutoClaim = async (event: React.MouseEvent) => { + event.preventDefault(); + setIsSingingDisable(true); + + try { + if (incorrectAccount) { + // Call msgRevokeBad + await authTx([msgRevokeBad], { + fee, + onSuccess: () => {}, + }); + } + // Continue with msgGrant + if (correctAccount) { + // Call msgRevokeBad + await authTx([revokeGrant], { + fee, + onSuccess: () => {}, + }); + } + } catch (error) { + console.error('Transaction failed', error); + setIsError(true); + } finally { + setIsSingingDisable(false); + } + }; + + const handleAuthzSection = () => { + setAuthzSection(!authzSection); + }; + + const handleLsmSection = () => { + setLsmSection(!lsmSection); + }; + + return ( + + + + {!authzSection && !lsmSection && ( + + Account Controls + + + + + + LSM Controls + + + + + + + + + Authz Controls + + + + + + {/* Buttons or any other footer content */} + + )} + + {/* Authz Section */} + {authzSection && ( + + XCC Authz Controls + + + + Disable or reenable the ability to auto claim your cross chain rewards. + + + + Disabling this feature will prevent you from automatically claiming your cross chain rewards. + + + + + + + + + + + + + )} + + {/* LSM Section */} + {lsmSection && ( + + Liquid Staking Module Controls + + + + If your wallet is compromised, hackers can easily tokenize your staked assets and steal them. Disabling LSM prevents this from + happening. + + + + Remember that you will not be able to directly stake your LSM-supported assets, such as Atom, unless you re-enable LSM. + + + + + + + + + + + + + )} + + ); +}; diff --git a/web-ui/components/react/sideHeader.tsx b/web-ui/components/react/sideHeader.tsx index 8f9afe9f0..19aa19dfb 100644 --- a/web-ui/components/react/sideHeader.tsx +++ b/web-ui/components/react/sideHeader.tsx @@ -24,12 +24,17 @@ import { useState, useEffect } from 'react'; import { FaDiscord, FaGithub, FaInfo } from 'react-icons/fa'; import { FaXTwitter } from 'react-icons/fa6'; import { IoIosDocument } from 'react-icons/io'; +import { MdPrivacyTip } from 'react-icons/md'; import { DrawerControlProvider } from '@/state/chains/drawerControlProvider'; +import { AccountControlModal } from './accountControlModal'; + + export const SideHeader = () => { const router = useRouter(); const [selectedPage, setSelectedPage] = useState(''); + const { isOpen, onOpen, onClose } = useDisclosure(); const [showSocialLinks, setShowSocialLinks] = useState(false); @@ -369,6 +374,7 @@ export const SideHeader = () => { + { + + + + + + { - {/* - router.push('/privacy-policy')} - _hover={{ - cursor: 'pointer', - boxShadow: `0 0 15px 5px ${commonBoxShadowColor}, inset 0 0 15px 5px ${commonBoxShadowColor}`, - transition: transitionStyle, - }} - > - - - */} )} diff --git a/web-ui/components/wallet-button.tsx b/web-ui/components/wallet-button.tsx index 29df2e5d4..18fd67e1b 100644 --- a/web-ui/components/wallet-button.tsx +++ b/web-ui/components/wallet-button.tsx @@ -15,7 +15,7 @@ import { } from '@/components'; export const WalletButton: React.FC = () => { - const chains = useChains(['quicksilver', 'cosmoshub', 'osmosis', 'stargaze', 'juno', 'sommelier', 'regen', 'umee']); + const chains = useChains(['quicksilver', 'cosmoshub', 'osmosis', 'stargaze', 'juno', 'sommelier', 'regen', 'umee', 'dydx']); const { connect, openView, status, message, wallet, isWalletError } = chains.quicksilver; diff --git a/web-ui/config/theme.ts b/web-ui/config/theme.ts index 0f5d8ce62..d7a1411c3 100644 --- a/web-ui/config/theme.ts +++ b/web-ui/config/theme.ts @@ -5,6 +5,7 @@ const defaultThemeObject = { initialColorMode: 'light', useSystemColorMode: false, }, + // Chakra UI cosmology wallet modal color manipulation styles: { global: (props: { colorMode: string }) => ({ '._1n3anio3': { diff --git a/web-ui/hooks/useFeeEstimation.ts b/web-ui/hooks/useFeeEstimation.ts new file mode 100644 index 000000000..f83496c19 --- /dev/null +++ b/web-ui/hooks/useFeeEstimation.ts @@ -0,0 +1,43 @@ +import { EncodeObject } from '@cosmjs/proto-signing'; +import { GasPrice, calculateFee } from '@cosmjs/stargate'; +import { useChain } from '@cosmos-kit/react'; + +import { getCoin } from '../config'; + +export const useFeeEstimation = (chainName: string) => { + const { getSigningStargateClient, chain } = useChain(chainName); + + const gasPrice = chain.fees?.fee_tokens[0].average_gas_price || 0.025; + + const coin = getCoin(chainName); + + const estimateFee = async ( + address: string, + messages: EncodeObject[], + modifier?: number, + memo?: string + ) => { + const stargateClient = await getSigningStargateClient(); + if (!stargateClient) { + throw new Error('getSigningStargateClient error'); + } + + const gasEstimation = await stargateClient.simulate( + address, + messages, + memo + ); + if (!gasEstimation) { + throw new Error('estimate gas error'); + } + + const fee = calculateFee( + Math.round(gasEstimation * (modifier || 1.5)), + GasPrice.fromString(gasPrice + coin.base) + ); + + return fee; + }; + + return { estimateFee }; +}; \ No newline at end of file diff --git a/web-ui/hooks/useGrpcQueryClient.ts b/web-ui/hooks/useGrpcQueryClient.ts index 62c0a3edd..bb442a8a0 100644 --- a/web-ui/hooks/useGrpcQueryClient.ts +++ b/web-ui/hooks/useGrpcQueryClient.ts @@ -1,5 +1,5 @@ -import { quicksilver } from 'quicksilverjs'; import { useQuery } from '@tanstack/react-query'; +import { quicksilver } from 'quicksilverjs'; const createGrpcGateWayClient = quicksilver.ClientFactory.createGrpcGateWayClient; @@ -11,7 +11,7 @@ export const useGrpcQueryClient = (chainName: string) => { const env = process.env.NEXT_PUBLIC_CHAIN_ENV; - +// Build the query client with the correct endpoint const endpoints: { [key: string]: string | undefined } = { quicksilver: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_QUICKSILVER : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_QUICKSILVER, cosmoshub: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_COSMOSHUB : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_COSMOSHUB, @@ -20,6 +20,7 @@ export const useGrpcQueryClient = (chainName: string) => { regen: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_REGEN : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_REGEN, osmosis: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_OSMOSIS : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_OSMOSIS, juno: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_JUNO : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_JUNO, + dydx: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_DYDX : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_DYDX, }; diff --git a/web-ui/hooks/useQueries.ts b/web-ui/hooks/useQueries.ts index 5b151e448..9c1b105b3 100644 --- a/web-ui/hooks/useQueries.ts +++ b/web-ui/hooks/useQueries.ts @@ -1,8 +1,7 @@ import { useChain } from '@cosmos-kit/react'; -import { useQuery } from '@tanstack/react-query'; +import { useQueries, useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { cosmos } from 'interchain-query'; -import { quicksilver } from 'quicksilverjs'; import { Zone } from 'quicksilverjs/dist/codegen/quicksilver/interchainstaking/v1/interchainstaking'; import { useGrpcQueryClient } from './useGrpcQueryClient'; @@ -76,17 +75,17 @@ type UseLiquidRewardsQueryReturnType = { interface ProofOp { type: string; - key: Uint8Array; // Updated to Uint8Array - data: Uint8Array; // Updated to Uint8Array + key: Uint8Array; + data: Uint8Array; } interface Proof { - key: Uint8Array; // Updated to Uint8Array - data: Uint8Array; // Updated to Uint8Array + key: Uint8Array; + data: Uint8Array; proof_ops: { ops: ProofOp[]; }; - height: Long; // Assuming height is a number + height: Long; proof_type: string; } @@ -96,7 +95,7 @@ interface Message { src_zone: string; claim_type: number; proofs: Proof[]; - // Remove height and proof_type if they are not needed here + } interface AssetAmount { @@ -110,7 +109,7 @@ interface LiquidEpochData { errors: Record; } -// Type for the useLiquidEpochQuery return + interface UseLiquidEpochQueryReturnType { liquidEpoch: LiquidEpochData | undefined; isLoading: boolean; @@ -118,7 +117,6 @@ interface UseLiquidEpochQueryReturnType { } - const BigNumber = require('bignumber.js'); const Long = require('long'); @@ -152,6 +150,37 @@ export const useBalanceQuery = (chainName: string, address: string) => { }; }; +export const useIncorrectAuthChecker = (address: string) => { + const authQuery = useQuery( + ['authWrong', address], + async () => { + if (!address) { + throw new Error('Address is undefined or null'); + } + + try { + const url = `https://lcd.quicksilver.zone/cosmos/authz/v1beta1/grants?granter=${address}&grantee=quick1w5ennfhdqrpyvewf35sv3y3t8yuzwq29mrmyal&msgTypeUrl=/quicksilver.participationrewards.v1.MsgSubmitClaim`; + const response = await axios.get(url); + return { data: response.data, error: null }; + } catch (error) { + // Capture and return error + return { data: null, error: error }; + } + }, + { + enabled: !!address, + staleTime: Infinity, + }, + ); + + return { + authData: authQuery.data?.data, + authError: authQuery.data?.error, + isLoading: authQuery.isLoading, + isError: authQuery.isError, + }; +}; + export const useAuthChecker = (address: string) => { const authQuery = useQuery( ['auth', address], @@ -161,7 +190,7 @@ export const useAuthChecker = (address: string) => { } try { - const url = `https://lcd.quicksilver.zone/cosmos/authz/v1beta1/grants?granter=${address}&grantee=quick1w5ennfhdqrpyvewf35sv3y3t8yuzwq29mrmyal&msgTypeUrl=/quicksilver.participationrewards.v1.MsgSubmitClaim`; + const url = `https://lcd.quicksilver.zone/cosmos/authz/v1beta1/grants?granter=${address}&grantee=quick1psevptdp90jad76zt9y9x2nga686hutgmasmwd&msgTypeUrl=/quicksilver.participationrewards.v1.MsgSubmitClaim`; const response = await axios.get(url); return { data: response.data, error: null }; } catch (error) { @@ -223,11 +252,11 @@ export const useAllBalancesQuery = (chainName: string, address: string) => { if (!grpcQueryClient) { throw new Error('RPC Client not ready'); } - const nextKey = new Uint8Array() + const next_key = new Uint8Array() const balance = await grpcQueryClient.cosmos.bank.v1beta1.allBalances({ address: address || '', pagination: { - key: nextKey, + key: next_key, offset: Long.fromNumber(0), limit: Long.fromNumber(100), countTotal: true, @@ -258,11 +287,11 @@ export const useIbcBalanceQuery = (chainName: string, address: string) => { if (!grpcQueryClient) { throw new Error('RPC Client not ready'); } - const nextKey = new Uint8Array() + const next_key = new Uint8Array() const balance = await grpcQueryClient.cosmos.bank.v1beta1.allBalances({ address: address || '', pagination: { - key: nextKey, + key: next_key, offset: Long.fromNumber(0), limit: Long.fromNumber(100), countTotal: true, @@ -302,24 +331,28 @@ export const useTokenPriceQuery = (tokenSymbol: string) => { }); }; -export const useQBalanceQuery = (chainName: string, address: string, qAsset: string) => { +export const useQBalanceQuery = (chainName: string, address: string, qAsset: string, liveNetworks?: string[], chainId?: string) => { const { grpcQueryClient } = useGrpcQueryClient(chainName); + + const isLive = liveNetworks?.includes(chainId ?? ''); + const balanceQuery = useQuery( ['balance', qAsset], async () => { if (!grpcQueryClient) { throw new Error('RPC Client not ready'); } + const denom = qAsset === 'dydx' ? 'aq'+ qAsset : 'uq' + qAsset; const balance = await grpcQueryClient.cosmos.bank.v1beta1.balance({ address: address || '', - denom: 'uq' + qAsset, + denom: denom, }); return balance; }, { - enabled: !!grpcQueryClient && !!address, + enabled: !!grpcQueryClient && !!address && isLive, staleTime: Infinity, }, ); @@ -383,7 +416,7 @@ export const useLiquidRewardsQuery = (address: string): UseLiquidRewardsQueryRet throw new Error('Address is not avaialble'); } - const response = await axios.get(`https://claim.test.quicksilver.zone/${address}/current`); + const response = await axios.get(`https://claim.quicksilver.zone/${address}/current`); return response.data; }, { @@ -408,7 +441,7 @@ export const useLiquidEpochQuery = (address: string): UseLiquidEpochQueryReturnT throw new Error('Address is not available'); } - const response = await axios.get(`https://claim.test.quicksilver.zone/${address}/epoch`); + const response = await axios.get(`https://claim.quicksilver.zone/${address}/epoch`); if (response.data.messages.length === 0) { @@ -486,21 +519,23 @@ export const useValidatorsQuery = (chainName: string) => { reverse: false, }, }); + return validators; }; + //TODO: migrate this to use evince cache endpoint. const validatorQuery = useQuery( ['validators', chainName], async () => { let allValidators: any[] = []; - let nextKey = new Uint8Array(); + let next_key = new Uint8Array(); do { - const response = await fetchValidators(nextKey); + const response = await fetchValidators(next_key); allValidators = allValidators.concat(response.validators); - nextKey = response.pagination.next_key ?? new Uint8Array(); - } while (nextKey && nextKey.length > 0); + next_key = response.pagination.next_key ?? new Uint8Array(); + } while (next_key && next_key.length > 0); const sorted = allValidators.sort((a, b) => new BigNumber(b.tokens).minus(a.tokens).toNumber()); return parseValidators(sorted); }, @@ -548,13 +583,16 @@ const fetchAPY = async (chainId: any) => { return chainInfo ? chainInfo.apr : 0; }; -export const useAPYQuery = (chainId: any) => { + + +export const useAPYQuery = (chainId: any, liveNetworks?: string[] ) => { + const isLive = liveNetworks?.some(network => network === chainId); const query = useQuery( ['APY', chainId], () => fetchAPY(chainId), { staleTime: Infinity, - enabled: !!chainId, + enabled: !!chainId && isLive, } ); @@ -565,17 +603,72 @@ export const useAPYQuery = (chainId: any) => { }; }; -export const useZoneQuery = (chainId: string) => { +function parseZone(apiZone: any): Zone { + + return { + connectionId: apiZone.connection_id, + chainId: apiZone.chain_id, + depositAddress: apiZone.deposit_address, + withdrawalAddress: apiZone.withdrawal_address, + performanceAddress: apiZone.performance_address, + delegationAddress: apiZone.delegation_address, + accountPrefix: apiZone.account_prefix, + localDenom: apiZone.local_denom, + baseDenom: apiZone.base_denom, + redemptionRate: apiZone.redemption_rate, + lastRedemptionRate: apiZone.last_redemption_rate, + validators: apiZone.validators, + aggregateIntent: apiZone.aggregate_intent, + multiSend: apiZone.multi_send, + liquidityModule: apiZone.liquidity_module, + withdrawalWaitgroup: apiZone.withdrawal_waitgroup, + ibcNextValidatorsHash: apiZone.ibc_next_validators_hash, + validatorSelectionAllocation: apiZone.validator_selection_allocation, + holdingsAllocation: apiZone.holdings_allocation, + lastEpochHeight: apiZone.last_epoch_height, + tvl: apiZone.tvl, + unbondingPeriod: apiZone.unbonding_period, + messagesPerTx: apiZone.messages_per_tx, + decimals: apiZone.decimals, + returnToSender: apiZone.return_to_sender, + unbondingEnabled: apiZone.unbonding_enabled, + depositsEnabled: apiZone.deposits_enabled, + is118: apiZone.is118, + subzoneInfo: apiZone.subzoneInfo, + }; +} + +export function useZonesData(networks: { chainId: string }[]) { + return useQueries({ + queries: networks.map(({ chainId }) => ({ + queryKey: ['zone', chainId], + queryFn: async () => { + const response = await axios.get(`${process.env.NEXT_PUBLIC_QUICKSILVER_API}/quicksilver/interchainstaking/v1/zones`); + const zones: any[] = response.data.zones; + const apiZone = zones.find(z => z.chain_id === chainId); + if (!apiZone) { + throw new Error(`No zone with chain id ${chainId} found`); + } + return parseZone(apiZone); + }, + enabled: !!chainId, + })) + }); +} + +export const useZoneQuery = (chainId: string, liveNetworks?: string[]) => { + const isLive = liveNetworks?.some(network => network === chainId); return useQuery( ['zone', chainId], async () => { + const res = await axios.get(`${process.env.NEXT_PUBLIC_QUICKSILVER_API}/quicksilver/interchainstaking/v1/zones`); const { zones } = res.data; if (!zones || zones.length === 0) { throw new Error('Failed to query zones'); } - + const apiZone = zones.find((z: { chain_id: string }) => z.chain_id === chainId); if (!apiZone) { throw new Error(`No zone with chain id ${chainId} found`); @@ -584,35 +677,40 @@ export const useZoneQuery = (chainId: string) => { // Parse or map the API zone data to your Zone interface const parsedZone: Zone = { connectionId: apiZone.connection_id, - chainId: apiZone.chain_id, - depositAddress: apiZone.deposit_address, - withdrawalAddress: apiZone.withdrawal_address, - performanceAddress: apiZone.performance_address, - delegationAddress: apiZone.delegation_address, - accountPrefix: apiZone.account_prefix, - localDenom: apiZone.local_denom, - baseDenom: apiZone.base_denom, - redemptionRate: apiZone.redemption_rate, - lastRedemptionRate: apiZone.last_redemption_rate, - validators: apiZone.validators, - aggregateIntent: apiZone.aggregate_intent, - multiSend: apiZone.multi_send, - liquidityModule: apiZone.liquidity_module, - withdrawalWaitgroup: apiZone.withdrawal_waitgroup, - ibcNextValidatorsHash: apiZone.ibc_next_validators_hash, - validatorSelectionAllocation: apiZone.validator_selection_allocation, - holdingsAllocation: apiZone.holdings_allocation, - lastEpochHeight: apiZone.last_epoch_height, - tvl: apiZone.tvl, - unbondingPeriod: apiZone.unbonding_period, - messagesPerTx: apiZone.messages_per_tx, - // ... other fields as needed + chainId: apiZone.chain_id, + depositAddress: apiZone.deposit_address, + withdrawalAddress: apiZone.withdrawal_address, + performanceAddress: apiZone.performance_address, + delegationAddress: apiZone.delegation_address, + accountPrefix: apiZone.account_prefix, + localDenom: apiZone.local_denom, + baseDenom: apiZone.base_denom, + redemptionRate: apiZone.redemption_rate, + lastRedemptionRate: apiZone.last_redemption_rate, + validators: apiZone.validators, + aggregateIntent: apiZone.aggregate_intent, + multiSend: apiZone.multi_send, + liquidityModule: apiZone.liquidity_module, + withdrawalWaitgroup: apiZone.withdrawal_waitgroup, + ibcNextValidatorsHash: apiZone.ibc_next_validators_hash, + validatorSelectionAllocation: apiZone.validator_selection_allocation, + holdingsAllocation: apiZone.holdings_allocation, + lastEpochHeight: apiZone.last_epoch_height, + tvl: apiZone.tvl, + unbondingPeriod: apiZone.unbonding_period, + messagesPerTx: apiZone.messages_per_tx, + decimals: apiZone.decimals, + returnToSender: apiZone.return_to_sender, + unbondingEnabled: apiZone.unbonding_enabled, + depositsEnabled: apiZone.deposits_enabled, + is118: apiZone.is118, + subzoneInfo: apiZone.subzoneInfo, }; return parsedZone; }, { - enabled: !!chainId, + enabled: !!chainId && isLive } ); }; @@ -640,12 +738,12 @@ export const useMissedBlocks = (chainName: string) => { } let allMissedBlocks: any[] = []; - let nextKey = new Uint8Array(); + let next_key = new Uint8Array(); do { const response = await grpcQueryClient.cosmos.slashing.v1beta1.signingInfos({ pagination: { - key: nextKey, + key: next_key, offset: Long.fromNumber(0), limit: Long.fromNumber(100), countTotal: true, @@ -662,8 +760,8 @@ export const useMissedBlocks = (chainName: string) => { }); allMissedBlocks = allMissedBlocks.concat(filteredMissedBlocks); - nextKey = response.pagination?.next_key ?? new Uint8Array(); - } while (nextKey && nextKey.length > 0); + next_key = response.pagination?.next_key ?? new Uint8Array(); + } while (next_key && next_key.length > 0); return allMissedBlocks; }; @@ -716,6 +814,44 @@ export const useDefiData = () => { }; }; +export const useGovernanceQuery = (chainName: string) => { + const { grpcQueryClient } = useGrpcQueryClient(chainName); + const governanceQuery = useQuery( + ['governance', chainName], + async () => { + if (!grpcQueryClient) { + throw new Error('RPC Client not ready'); + } + const next_key = new Uint8Array() + const governance = await grpcQueryClient.cosmos.gov.v1beta1.proposals({ + proposalStatus: cosmos.gov.v1.ProposalStatus.PROPOSAL_STATUS_UNSPECIFIED, + pagination: { + key: next_key, + offset: Long.fromNumber(0), + limit: Long.fromNumber(100), + countTotal: true, + reverse: true, + }, + voter: '', + depositor: '', + }); + + return governance; + }, + { + enabled: !!grpcQueryClient, + staleTime: Infinity, + }, + ); + + return { + governance: governanceQuery.data, + isLoading: governanceQuery.isLoading, + isError: governanceQuery.isError, + }; + +} + export const useNativeStakeQuery = (chainName: string, address: string) => { const { grpcQueryClient } = useGrpcQueryClient(chainName); const delegationQuery = useQuery( @@ -724,18 +860,18 @@ export const useNativeStakeQuery = (chainName: string, address: string) => { if (!grpcQueryClient) { throw new Error('RPC Client not ready'); } - const nextKey = new Uint8Array() + const next_key = new Uint8Array() const balance = await grpcQueryClient.cosmos.staking.v1beta1.delegatorDelegations({ delegator_addr: address || '', pagination: { - key: nextKey, + key: next_key, offset: Long.fromNumber(0), limit: Long.fromNumber(100), countTotal: true, reverse: false, }, }); - + return balance; }, { diff --git a/web-ui/hooks/useRpcQueryClient.ts b/web-ui/hooks/useRpcQueryClient.ts index 53dcb011c..90c22bbed 100644 --- a/web-ui/hooks/useRpcQueryClient.ts +++ b/web-ui/hooks/useRpcQueryClient.ts @@ -10,7 +10,7 @@ export const useRpcQueryClient = (chainName: string) => { let rpcEndpoint: string | HttpEndpoint | undefined; const env = process.env.NEXT_PUBLIC_CHAIN_ENV; - +// Builds the query client with the proper endpoint const endpoints: { [key: string]: string | undefined } = { quicksilver: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_QUICKSILVER : process.env.MAINNET_RPC_ENDPOINT_QUICKSILVER, cosmoshub: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_COSMOSHUB : process.env.MAINNET_RPC_ENDPOINT_COSMOSHUB, @@ -18,6 +18,8 @@ export const useRpcQueryClient = (chainName: string) => { stargaze: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_STARGAZE : process.env.MAINNET_RP_ENDPOINTC_STARGAZE, regen: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_REGEN : process.env.MAINNET_RPC_ENDPOINT_REGEN, osmosis: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_OSMOSIS : process.env.MAINNET_RPC_ENDPOINT_OSMOSIS, + juno: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_JUNO : process.env.MAINNET_RPC_ENDPOINT_JUNO, + dydx: env === 'testnet' ? process.env.TESTNET_RPC_ENDPOINT_DYDX : process.env.MAINNET_RPC_ENDPOINT_DYDX, }; rpcEndpoint = endpoints[chainName]; @@ -30,9 +32,6 @@ export const useRpcQueryClient = (chainName: string) => { }, enabled: !!rpcEndpoint, staleTime: Infinity, - onError: (error) => { - console.error('Error in fetching RPC Query Client:', error); - } }); diff --git a/web-ui/hooks/useToaster.tsx b/web-ui/hooks/useToaster.tsx index fe289b6c1..0bd132602 100644 --- a/web-ui/hooks/useToaster.tsx +++ b/web-ui/hooks/useToaster.tsx @@ -1,5 +1,7 @@ import { useToast, Text, Box, Link } from '@chakra-ui/react'; +import { convertChainName } from '@/utils'; + export enum ToastType { Info = 'info', Error = 'error', @@ -28,7 +30,7 @@ export const useToaster = () => { let description; if (type === ToastType.Success && txHash) { - const mintscanUrl = `https://www.mintscan.io/${chainName}/txs/${txHash}`; + const mintscanUrl = `https://www.mintscan.io/${convertChainName(chainName ?? '')}/txs/${txHash}`; description = ( diff --git a/web-ui/hooks/useTx.tsx b/web-ui/hooks/useTx.tsx index f74e09fea..b3490cfa1 100644 --- a/web-ui/hooks/useTx.tsx +++ b/web-ui/hooks/useTx.tsx @@ -4,9 +4,10 @@ import { useChain } from '@cosmos-kit/react'; import { cosmos } from 'interchain-query'; import { TxRaw } from 'interchain-query/cosmos/tx/v1beta1/tx'; import { Event } from 'interchain-query/tendermint/abci/types'; -import { useToaster, ToastType, type CustomToast } from './useToaster'; import { useState } from 'react'; +import { useToaster, ToastType, type CustomToast } from './useToaster'; + interface Msg { typeUrl: string; value: any; diff --git a/web-ui/hooks/useVotingData.ts b/web-ui/hooks/useVotingData.ts index 393050074..da7366be5 100644 --- a/web-ui/hooks/useVotingData.ts +++ b/web-ui/hooks/useVotingData.ts @@ -1,6 +1,7 @@ import { useChain } from '@cosmos-kit/react'; import { useQueries } from '@tanstack/react-query'; -import { ProposalStatus } from 'interchain-query/cosmos/gov/v1/gov'; +import { ProposalStatus } from 'interchain-query/cosmos/gov/v1beta1/gov'; +import Long from 'long'; import { useEffect, useMemo, @@ -108,8 +109,8 @@ export const useVotingData = ( if (!grpcQueryClient || !proposalId || !address) { throw new Error("Required information for query is missing"); } - return grpcQueryClient.cosmos.gov.v1.vote({ - proposal_id: proposalId, + return grpcQueryClient.cosmos.gov.v1beta1.vote({ + proposalId: new Long(Number(proposalId)), voter: address || '', }); }, diff --git a/web-ui/next.config.js b/web-ui/next.config.js index 6b0e45bdd..611e2d91a 100644 --- a/web-ui/next.config.js +++ b/web-ui/next.config.js @@ -13,10 +13,4 @@ module.exports = { }, ]; }, - typescript: { - // !! WARN !! // - // There are no fatal errors in this project, this option is used as a workaround due to the amalgamation of packages we are using // - // This option will be removed once all dependencies are updated to use the latest versions // - ignoreBuildErrors: true, - }, }; diff --git a/web-ui/package.json b/web-ui/package.json index 0e80fb6a1..ad5e444c9 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "bun-qs", - "version": "0.15.3", + "version": "0.15.4", "private": true, "homepage": "https://quicksilver-zone.github.io/quicksilver/", "scripts": { @@ -13,7 +13,7 @@ "dependencies": { "@chain-registry/assets": "^1.26.0", "@chakra-ui/icons": "^2.0.12", - "@chakra-ui/react": "2.5.1", + "@chakra-ui/react": "2.8.2", "@chakra-ui/system": "^2.1.3", "@cosmjs/amino": "^0.32.2", "@cosmjs/cosmwasm-stargate": "^0.32.2", @@ -24,32 +24,27 @@ "@cosmos-kit/keplr": "^2.6.2", "@cosmos-kit/leap": "^2.6.2", "@cosmos-kit/react": "^2.6.2", - "@emotion/react": "11.10.6", + "@emotion/react": "11.11.3", "@emotion/styled": "11.10.6", "@interchain-ui/react": "1.10.0", "@osmonauts/lcd": "^1.0.3", "@radix-ui/react-icons": "^1.3.0", - "@tanstack/react-query": "^4.29.12", - "@tanstack/react-query-devtools": "^5.17.9", + "@tanstack/react-query": "4.36.1", + "@tanstack/react-query-devtools": "4.36.1", "@types/crypto-js": "^4.2.1", - "@types/geoip-lite": "^1.4.4", - "@types/mixpanel-browser": "^2.48.1", "bech32": "^2.0.0", - "bun": "^1.0.3", - "bun-framework-next": "latest", - "chain-registry": "1.19.0", + "chain-registry": "1.28.0", "chakra-react-select": "^4.7.6", - "cosmjs-types": "0.5.0", + "cosmjs-types": "0.9.0", "crypto-js": "^4.2.0", "dayjs": "^1.11.9", "express": "^4.18.2", "fast-fuzzy": "^1.12.0", "framer-motion": "10.18.0", - "geoip-lite": "^1.4.9", - "interchain-query": "^1.8.7", - "mixpanel-browser": "^2.48.1", + "interchain-query": "1.8.7", "next": "12.2.3", - "quicksilverjs": "1.1.0", + "next-transpile-modules": "^10.0.1", + "quicksilverjs": "1.3.8", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "5.0.1", @@ -60,15 +55,14 @@ }, "devDependencies": { "@testing-library/react": "^14.0.0", - "@types/node": "20.11.0", - "@types/react": "18.0.25", - "@types/react-dom": "18.0.9", - "@types/three": "^0.155.1", - "eslint": "8.28.0", + "@types/node": "20.11.16", + "@types/react": "18.2.52", + "@types/react-dom": "18.2.18", + "eslint": "8.56.0", "eslint-config-next": "13.0.5", "eslint-plugin-unused-imports": "^3.0.0", "prettier": "^3.0.3", "react-refresh": "0.10.0", - "typescript": "4.9.3" + "typescript": "5.2.2" } } diff --git a/web-ui/pages/_app.tsx b/web-ui/pages/_app.tsx index 5861d45e4..5e4e94394 100644 --- a/web-ui/pages/_app.tsx +++ b/web-ui/pages/_app.tsx @@ -1,4 +1,6 @@ import '../styles/globals.css'; + +import '@interchain-ui/react/styles'; import { Chain } from '@chain-registry/types'; import { Box, ChakraProvider, Flex } from '@chakra-ui/react'; import { Registry } from '@cosmjs/proto-signing'; @@ -8,55 +10,46 @@ import { wallets as cosmostationWallets } from '@cosmos-kit/cosmostation'; import { wallets as keplrWallets } from '@cosmos-kit/keplr'; import { wallets as leapWallets } from '@cosmos-kit/leap'; import { ChainProvider, ThemeCustomizationProps } from '@cosmos-kit/react'; +import { ThemeProvider, useTheme } from '@interchain-ui/react'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { chains, assets } from 'chain-registry'; +import { ibcAminoConverters, ibcProtoRegistry } from 'interchain-query'; import type { AppProps } from 'next/app'; -import { - quicksilverProtoRegistry, - quicksilverAminoConverters, - cosmosAminoConverters, - cosmosProtoRegistry, - ibcAminoConverters, - ibcProtoRegistry, -} from 'quicksilverjs'; +import { quicksilverProtoRegistry, quicksilverAminoConverters, cosmosAminoConverters, cosmosProtoRegistry } from 'quicksilverjs'; import { DynamicHeaderSection, SideHeader } from '@/components'; import { defaultTheme } from '@/config'; +import { LiveZonesProvider } from '@/state/LiveZonesContext'; -import '@interchain-ui/react/styles'; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 2, + refetchOnWindowFocus: false, + }, + }, +}); function QuickApp({ Component, pageProps }: AppProps) { + const { themeClass } = useTheme(); const signerOptions: SignerOptions = { - //@ts-ignore - signingStargate: (chain: Chain): SigningStargateClientOptions | undefined => { - //@ts-ignore + signingStargate: (_chain: string | Chain): SigningStargateClientOptions | undefined => { const mergedRegistry = new Registry([...quicksilverProtoRegistry, ...ibcProtoRegistry, ...cosmosProtoRegistry]); - const mergedAminoTypes = new AminoTypes({ ...cosmosAminoConverters, ...quicksilverAminoConverters, ...ibcAminoConverters, }); - return { aminoTypes: mergedAminoTypes, - //@ts-ignore registry: mergedRegistry, }; }, }; - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 2, - refetchOnWindowFocus: false, - }, - }, - }); - const env = process.env.NEXT_PUBLIC_CHAIN_ENV; + const walletConnectToken = process.env.NEXT_PUBLIC_WALLET_CONNECT_TOKEN; const rpcEndpoints = { quicksilver: @@ -73,6 +66,7 @@ function QuickApp({ Component, pageProps }: AppProps) { osmosis: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_OSMOSIS : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_OSMOSIS, juno: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_JUNO : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_JUNO, + dydx: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_RPC_ENDPOINT_DYDX : process.env.NEXT_PUBLIC_MAINNET_RPC_ENDPOINT_DYDX, }; const lcdEndpoints = { @@ -90,6 +84,7 @@ function QuickApp({ Component, pageProps }: AppProps) { osmosis: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_OSMOSIS : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_OSMOSIS, juno: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_JUNO : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_JUNO, + dydx: env === 'testnet' ? process.env.NEXT_PUBLIC_TESTNET_LCD_ENDPOINT_DYDX : process.env.NEXT_PUBLIC_MAINNET_LCD_ENDPOINT_DYDX, }; const modalThemeOverrides: ThemeCustomizationProps = { @@ -218,110 +213,94 @@ function QuickApp({ Component, pageProps }: AppProps) { }, }; - function isWalletClientAvailable(walletName: string) { - if (typeof window === 'undefined') { - return false; - } - - switch (walletName) { - case 'Keplr': - return typeof window.keplr !== 'undefined'; - case 'Cosmostation': - return typeof window.cosmostation !== 'undefined'; - case 'Leap': - return typeof window.leap !== 'undefined'; - default: - return false; - } - } - - const availableWallets = []; - - if (isWalletClientAvailable('Keplr')) { - availableWallets.push(...keplrWallets); - } - if (isWalletClientAvailable('Cosmostation')) { - availableWallets.push(...cosmostationWallets); - } - if (isWalletClientAvailable('Leap')) { - availableWallets.push(...leapWallets); - } - return ( - - - - - - - - - - - - - - + + + + + + + +
+ + + + + + + +
+
+
+
+
+
); } diff --git a/web-ui/pages/about.tsx b/web-ui/pages/about.tsx index 2641dcc3d..9f0a5222d 100644 --- a/web-ui/pages/about.tsx +++ b/web-ui/pages/about.tsx @@ -17,7 +17,7 @@ const AboutPage = () => { About - + diff --git a/web-ui/pages/airdrop.tsx b/web-ui/pages/airdrop.tsx index 11d456231..29566a9fa 100644 --- a/web-ui/pages/airdrop.tsx +++ b/web-ui/pages/airdrop.tsx @@ -20,7 +20,7 @@ export default function Home() { Airdrop - + Airdrop diff --git a/web-ui/pages/assets.tsx b/web-ui/pages/assets.tsx index 798aa6e41..8abd886d5 100644 --- a/web-ui/pages/assets.tsx +++ b/web-ui/pages/assets.tsx @@ -1,4 +1,4 @@ -import { Box, Container, Flex, SlideFade, Spacer, Text, Image } from '@chakra-ui/react'; +import { Box, Container, Flex, SlideFade, Spacer, Text, Center } from '@chakra-ui/react'; import { useChain } from '@cosmos-kit/react'; import dynamic from 'next/dynamic'; import Head from 'next/head'; @@ -11,7 +11,8 @@ import QuickBox from '@/components/Assets/quickbox'; import RewardsClaim from '@/components/Assets/rewardsClaim'; import UnbondingAssetsTable from '@/components/Assets/unbondingTable'; import { useAPYQuery, useAuthChecker, useLiquidRewardsQuery, useQBalanceQuery, useTokenPrices, useZoneQuery } from '@/hooks/useQueries'; -import { shiftDigits, toNumber } from '@/utils'; +import { useLiveZones } from '@/state/LiveZonesContext'; +import { shiftDigits, truncateToTwoDecimals } from '@/utils'; export interface PortfolioItemInterface { title: string; @@ -21,9 +22,20 @@ export interface PortfolioItemInterface { qTokenPrice: number; } -type NumericRedemptionRates = { - [key: string]: number; -}; +interface RedemptionRate { + current: number; + last: number; +} + +interface RedemptionRates { + atom: RedemptionRate; + osmo: RedemptionRate; + stars: RedemptionRate; + regen: RedemptionRate; + somm: RedemptionRate; + juno: RedemptionRate; + [key: string]: RedemptionRate; +} type BalanceRates = { [key: string]: string; @@ -35,24 +47,32 @@ type APYRates = { function Home() { const { address } = useChain('quicksilver'); - const tokens = ['atom', 'osmo', 'stars', 'regen', 'somm', 'juno']; // Example tokens + const tokens = ['atom', 'osmo', 'stars', 'regen', 'somm', 'juno', 'dydx']; const { data: tokenPrices, isLoading: isLoadingPrices } = useTokenPrices(tokens); + // TODO: Use live chain ids from .env const COSMOSHUB_CHAIN_ID = process.env.NEXT_PUBLIC_COSMOSHUB_CHAIN_ID; const OSMOSIS_CHAIN_ID = process.env.NEXT_PUBLIC_OSMOSIS_CHAIN_ID; const STARGAZE_CHAIN_ID = process.env.NEXT_PUBLIC_STARGAZE_CHAIN_ID; const REGEN_CHAIN_ID = process.env.NEXT_PUBLIC_REGEN_CHAIN_ID; const SOMMELIER_CHAIN_ID = process.env.NEXT_PUBLIC_SOMMELIER_CHAIN_ID; const JUNO_CHAIN_ID = process.env.NEXT_PUBLIC_JUNO_CHAIN_ID; + const DYDX_CHAIN_ID = process.env.NEXT_PUBLIC_DYDX_CHAIN_ID; + + // Retrieve list of zones that are enabled for liquid staking || Will use the above instead + const { liveNetworks } = useLiveZones(); + // TODO: Figure out how to cycle through live networks and retrieve data for each with less lines of code // Retrieve balance for each token + // Depending on whether the chain exists in liveNetworks or not, the query will be enabled/disabled const { balance: qAtom, isLoading: isLoadingQABalance } = useQBalanceQuery('quicksilver', address ?? '', 'atom'); const { balance: qOsmo, isLoading: isLoadingQOBalance } = useQBalanceQuery('quicksilver', address ?? '', 'osmo'); const { balance: qStars, isLoading: isLoadingQSBalance } = useQBalanceQuery('quicksilver', address ?? '', 'stars'); const { balance: qRegen, isLoading: isLoadingQRBalance } = useQBalanceQuery('quicksilver', address ?? '', 'regen'); const { balance: qSomm, isLoading: isLoadingQSOBalance } = useQBalanceQuery('quicksilver', address ?? '', 'somm'); const { balance: qJuno, isLoading: isLoadingQJBalance } = useQBalanceQuery('quicksilver', address ?? '', 'juno'); + const { balance: qDydx, isLoading: isLoadingQDBalance } = useQBalanceQuery('quicksilver', address ?? '', 'dydx'); // Retrieve zone data for each token const { data: CosmosZone, isLoading: isLoadingCosmosZone } = useZoneQuery(COSMOSHUB_CHAIN_ID ?? ''); @@ -61,6 +81,7 @@ function Home() { const { data: RegenZone, isLoading: isLoadingRegenZone } = useZoneQuery(REGEN_CHAIN_ID ?? ''); const { data: SommZone, isLoading: isLoadingSommZone } = useZoneQuery(SOMMELIER_CHAIN_ID ?? ''); const { data: JunoZone, isLoading: isLoadingJunoZone } = useZoneQuery(JUNO_CHAIN_ID ?? ''); + const { data: DydxZone, isLoading: isLoadingDydxZone } = useZoneQuery(DYDX_CHAIN_ID ?? ''); // Retrieve APY data for each token const { APY: cosmosAPY, isLoading: isLoadingCosmosApy } = useAPYQuery('cosmoshub-4'); const { APY: osmoAPY, isLoading: isLoadingOsmoApy } = useAPYQuery('osmosis-1'); @@ -69,6 +90,7 @@ function Home() { const { APY: sommAPY, isLoading: isLoadingSommApy } = useAPYQuery('sommelier-3'); const { APY: quickAPY } = useAPYQuery('quicksilver-2'); const { APY: junoAPY, isLoading: isLoadingJunoApy } = useAPYQuery('juno-1'); + const { APY: dydxAPY, isLoading: isLoadingDydxApy } = useAPYQuery('dydx-mainnet-1'); const isLoadingAll = isLoadingPrices || @@ -78,55 +100,82 @@ function Home() { isLoadingQRBalance || isLoadingQSOBalance || isLoadingQJBalance || + isLoadingQDBalance || isLoadingCosmosZone || isLoadingOsmoZone || isLoadingStarZone || isLoadingRegenZone || isLoadingSommZone || isLoadingJunoZone || + isLoadingDydxZone || isLoadingCosmosApy || isLoadingOsmoApy || isLoadingStarsApy || isLoadingRegenApy || isLoadingSommApy || - isLoadingJunoApy; + isLoadingJunoApy || + isLoadingDydxApy; // useMemo hook to cache APY data const qAPYRates: APYRates = useMemo( () => ({ - qAtom: cosmosAPY, - qOsmo: osmoAPY, - qStars: starsAPY, - qRegen: regenAPY, - qSomm: sommAPY, - qJuno: junoAPY, + qAtom: cosmosAPY ?? 0, + qOsmo: osmoAPY ?? 0, + qStars: starsAPY ?? 0, + qRegen: regenAPY ?? 0, + qSomm: sommAPY ?? 0, + qJuno: junoAPY ?? 0, + qDydx: dydxAPY ?? 0, }), - [cosmosAPY, osmoAPY, starsAPY, regenAPY, sommAPY, junoAPY], + [cosmosAPY, osmoAPY, starsAPY, regenAPY, sommAPY, junoAPY, dydxAPY], ); // useMemo hook to cache qBalance data const qBalances: BalanceRates = useMemo( () => ({ - qAtom: shiftDigits(qAtom?.balance?.amount ?? '', -6), - qOsmo: shiftDigits(qOsmo?.balance?.amount ?? '', -6), - qStars: shiftDigits(qStars?.balance?.amount ?? '', -6), - qRegen: shiftDigits(qRegen?.balance?.amount ?? '', -6), - qSomm: shiftDigits(qSomm?.balance?.amount ?? '', -6), - qJuno: shiftDigits(qJuno?.balance?.amount ?? '', -6), + qAtom: shiftDigits(qAtom?.balance?.amount ?? '000000', -6), + qOsmo: shiftDigits(qOsmo?.balance?.amount ?? '000000', -6), + qStars: shiftDigits(qStars?.balance?.amount ?? '000000', -6), + qRegen: shiftDigits(qRegen?.balance?.amount ?? '000000', -6), + qSomm: shiftDigits(qSomm?.balance?.amount ?? '000000', -6), + qJuno: shiftDigits(qJuno?.balance?.amount ?? '000000', -6), + qDydx: shiftDigits(qDydx?.balance?.amount ?? '000000', -18), }), - [qAtom, qOsmo, qStars, qRegen, qSomm, qJuno], + [qAtom, qOsmo, qStars, qRegen, qSomm, qJuno, qDydx], ); // useMemo hook to cache redemption rate data - const redemptionRates: NumericRedemptionRates = useMemo( + const redemptionRates: RedemptionRates = useMemo( () => ({ - atom: CosmosZone?.redemptionRate ? parseFloat(CosmosZone.redemptionRate) : 1, - osmo: OsmoZone?.redemptionRate ? parseFloat(OsmoZone.redemptionRate) : 1, - stars: StarZone?.redemptionRate ? parseFloat(StarZone.redemptionRate) : 1, - regen: RegenZone?.redemptionRate ? parseFloat(RegenZone.redemptionRate) : 1, - somm: SommZone?.redemptionRate ? parseFloat(SommZone.redemptionRate) : 1, - juno: JunoZone?.redemptionRate ? parseFloat(JunoZone.redemptionRate) : 1, + atom: { + current: CosmosZone?.redemptionRate ? parseFloat(CosmosZone.redemptionRate) : 1, + last: CosmosZone?.lastRedemptionRate ? parseFloat(CosmosZone.lastRedemptionRate) : 1, + }, + osmo: { + current: OsmoZone?.redemptionRate ? parseFloat(OsmoZone.redemptionRate) : 1, + last: OsmoZone?.lastRedemptionRate ? parseFloat(OsmoZone.lastRedemptionRate) : 1, + }, + stars: { + current: StarZone?.redemptionRate ? parseFloat(StarZone.redemptionRate) : 1, + last: StarZone?.lastRedemptionRate ? parseFloat(StarZone.lastRedemptionRate) : 1, + }, + regen: { + current: RegenZone?.redemptionRate ? parseFloat(RegenZone.redemptionRate) : 1, + last: RegenZone?.lastRedemptionRate ? parseFloat(RegenZone.lastRedemptionRate) : 1, + }, + somm: { + current: SommZone?.redemptionRate ? parseFloat(SommZone.redemptionRate) : 1, + last: SommZone?.lastRedemptionRate ? parseFloat(SommZone.lastRedemptionRate) : 1, + }, + juno: { + current: JunoZone?.redemptionRate ? parseFloat(JunoZone.redemptionRate) : 1, + last: JunoZone?.lastRedemptionRate ? parseFloat(JunoZone.lastRedemptionRate) : 1, + }, + dydx: { + current: DydxZone?.redemptionRate ? parseFloat(DydxZone.redemptionRate) : 1, + last: DydxZone?.lastRedemptionRate ? parseFloat(DydxZone.lastRedemptionRate) : 1, + }, }), - [CosmosZone, OsmoZone, StarZone, RegenZone, SommZone, JunoZone], + [CosmosZone, OsmoZone, StarZone, RegenZone, SommZone, JunoZone, DydxZone], ); // State hooks for portfolio items, total portfolio value, and other metrics @@ -134,77 +183,72 @@ function Home() { const [totalPortfolioValue, setTotalPortfolioValue] = useState(0); const [averageApy, setAverageAPY] = useState(0); const [totalYearlyYield, setTotalYearlyYield] = useState(0); - const [isLoading, setIsLoading] = useState(false); + // useEffect hook to compute portfolio metrics when dependencies change + // TODO: cache the computation and make it faster + const computedValues = useMemo(() => { + if (isLoadingAll) { + return { updatedItems: [], totalValue: 0, weightedAPY: 0, totalYearlyYield: 0 }; + } - useEffect(() => { - const updatePortfolioItems = async () => { - // Check if all data is loaded - if (isLoadingAll) { - return; - } - - setIsLoading(true); - let totalValue = 0; - let totalYearlyYield = 0; - let weightedAPY = 0; - let updatedItems = []; - - // Loop through each token to compute value, APY, and yield - for (const token of Object.keys(qBalances)) { - const baseToken = token.replace('q', '').toLowerCase(); - // Find the price for the current token - const tokenPriceInfo = tokenPrices?.find((priceInfo: { token: string }) => priceInfo.token === baseToken); - const qTokenPrice = tokenPriceInfo ? tokenPriceInfo.price * Number(redemptionRates[baseToken]) : 0; - const qTokenBalance = qBalances[token]; - const itemValue = Number(qTokenBalance) * qTokenPrice; - - const qTokenAPY = qAPYRates[token] || 0; - const yearlyYield = itemValue * Number(qTokenAPY); - // Accumulate total values and compute weighted APY - totalValue += itemValue; - totalYearlyYield += yearlyYield; - weightedAPY += (itemValue / totalValue) * Number(qTokenAPY); - - updatedItems.push({ - title: token.toUpperCase(), - percentage: 0, - progressBarColor: 'complimentary.700', - amount: qTokenBalance, - qTokenPrice: qTokenPrice || 0, - }); - } - - // Recalculate percentages for each item based on total value - updatedItems = updatedItems.map((item) => { - const itemValue = Number(item.amount) * item.qTokenPrice; - return { - ...item, - percentage: (((itemValue / totalValue) * 100) / 100).toFixed(2), - }; + let totalValue = 0; + let totalYearlyYield = 0; + let weightedAPY = 0; + let updatedItems = []; + + for (const token of Object.keys(qBalances)) { + const baseToken = token.replace('q', '').toLowerCase(); + const tokenPriceInfo = tokenPrices?.find((priceInfo) => priceInfo.token === baseToken); + const qTokenPrice = tokenPriceInfo ? tokenPriceInfo.price * Number(redemptionRates[baseToken].current) : 0; + const qTokenBalance = qBalances[token]; + const itemValue = Number(qTokenBalance) * qTokenPrice; + + const qTokenAPY = qAPYRates[token] || 0; + const yearlyYield = itemValue * Number(qTokenAPY); + totalValue += itemValue; + totalYearlyYield += yearlyYield; + weightedAPY += (itemValue / totalValue) * Number(qTokenAPY); + + updatedItems.push({ + title: token.toUpperCase(), + percentage: 0, + progressBarColor: 'complimentary.700', + amount: qTokenBalance, + qTokenPrice: qTokenPrice || 0, }); + } + + updatedItems = updatedItems.map((item) => { + const itemValue = Number(item.amount) * item.qTokenPrice; + return { + ...item, + percentage: (((itemValue / totalValue) * 100) / 100).toFixed(2), + }; + }); - // Update state with calculated data - setPortfolioItems(updatedItems); - setTotalPortfolioValue(totalValue); - setAverageAPY(weightedAPY); - setTotalYearlyYield(totalYearlyYield); - setIsLoading(false); - }; + return { updatedItems, totalValue, weightedAPY, totalYearlyYield }; + }, [isLoadingAll, qBalances, tokenPrices, redemptionRates, qAPYRates]); - updatePortfolioItems(); - }, [qBalances, CosmosZone, OsmoZone, StarZone, RegenZone, SommZone, redemptionRates, qAPYRates, tokenPrices, isLoadingAll]); + useEffect(() => { + if (!isLoadingAll) { + setPortfolioItems(computedValues.updatedItems); + setTotalPortfolioValue(computedValues.totalValue); + setAverageAPY(computedValues.weightedAPY); + setTotalYearlyYield(computedValues.totalYearlyYield); + } + }, [computedValues, isLoadingAll]); const assetsData = useMemo(() => { return Object.keys(qBalances).map((token) => { return { name: token.toUpperCase(), - balance: toNumber(qBalances[token], 2).toString(), + balance: truncateToTwoDecimals(Number(qBalances[token])).toString(), apy: parseFloat(qAPYRates[token]?.toFixed(2)) || 0, native: token.replace('q', '').toUpperCase(), + redemptionRates: redemptionRates[token.replace('q', '').toLowerCase()].last.toString(), }; }); - }, [qBalances, qAPYRates]); + }, [qBalances, qAPYRates, redemptionRates]); const { liquidRewards } = useLiquidRewardsQuery(address ?? ''); const { authData, authError } = useAuthChecker(address ?? ''); @@ -226,6 +270,50 @@ function Home() { setUserClosedRewardsClaim(true); }; + if (!address) { + return ( + +
+ + + + Assets + + + + + Assets + + + + Please connect your wallet to interact with your qAssets. + + + + +
+
+ ); + } + return ( <> @@ -242,7 +330,7 @@ function Home() { Assets - + Assets diff --git a/web-ui/pages/defi.tsx b/web-ui/pages/defi.tsx index 8cecdeb51..be0631bcb 100644 --- a/web-ui/pages/defi.tsx +++ b/web-ui/pages/defi.tsx @@ -9,7 +9,7 @@ export default function Home() { DeFi - + diff --git a/web-ui/pages/geo-block.tsx b/web-ui/pages/geo-block.tsx index f2cf00930..74ecbff6f 100644 --- a/web-ui/pages/geo-block.tsx +++ b/web-ui/pages/geo-block.tsx @@ -18,7 +18,7 @@ export default function Home() { Geo Block - +
diff --git a/web-ui/pages/governance.tsx b/web-ui/pages/governance.tsx index 91cae5791..cf7141d32 100644 --- a/web-ui/pages/governance.tsx +++ b/web-ui/pages/governance.tsx @@ -1,4 +1,5 @@ -import { Box, Container, SlideFade, Text } from '@chakra-ui/react'; +import { Box, Center, Container, Flex, SlideFade, Text } from '@chakra-ui/react'; +import { useChain } from '@cosmos-kit/react'; import dynamic from 'next/dynamic'; import Head from 'next/head'; @@ -11,6 +12,52 @@ const DynamicVotingSection = dynamic(() => Promise.resolve(VotingSection), { export default function Home() { const chainName = 'quicksilver'; + const { address } = useChain(chainName); + + if (!address) { + return ( + +
+ + + + Governance + + + + + Proposals + + + + Please connect your wallet to view and vote on proposals. + + + + +
+
+ ); + } + return ( <> @@ -18,7 +65,7 @@ export default function Home() { zIndex={2} position="relative" maxW="container.lg" - height="100vh" // Full viewport height + height="100vh" display="flex" flexDirection="column" justifyContent="center" // Center vertically @@ -29,10 +76,10 @@ export default function Home() { Governance - + - - + + Proposals {chainName && } diff --git a/web-ui/pages/privacy-policy.tsx b/web-ui/pages/privacy-policy.tsx index c1bbf9214..a4ff21259 100644 --- a/web-ui/pages/privacy-policy.tsx +++ b/web-ui/pages/privacy-policy.tsx @@ -17,7 +17,7 @@ const PrivacyPolicyPage = () => { Privacy Policy - + diff --git a/web-ui/pages/staking/[chainId]/[valoperAddress].tsx b/web-ui/pages/staking/[chainId]/[valoperAddress].tsx index 7958d5132..7ddc0dfd3 100644 --- a/web-ui/pages/staking/[chainId]/[valoperAddress].tsx +++ b/web-ui/pages/staking/[chainId]/[valoperAddress].tsx @@ -78,7 +78,7 @@ export default function Home() { Staking on {selectedNetwork?.name} - + {selectedNetwork && validValoperAddress && isValidValoperAddress() ? ( @@ -141,11 +141,7 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) const exp = getExponent(selectedOption.chainName); const { balance, isLoading } = useBalanceQuery(selectedOption.chainName, address ?? ''); - const { - balance: qBalance, - isLoading: qIsLoading, - isError: qIsError, - } = useQBalanceQuery('quicksilver', qAddress ?? '', selectedOption.value.toLowerCase()); + const { balance: qBalance } = useQBalanceQuery('quicksilver', qAddress ?? '', selectedOption.value.toLowerCase()); const qAssets = qBalance?.balance.amount || ''; @@ -170,7 +166,8 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) const [inputError, setInputError] = useState(false); - const qAssetsExponent = shiftDigits(qAssets, -6); + const exponent = qBalance?.balance.denom === 'aqdydx' ? -18 : -6; + const qAssetsExponent = shiftDigits(qAssets, exponent); const qAssetsDisplay = qAssetsExponent.includes('.') ? qAssetsExponent.substring(0, qAssetsExponent.indexOf('.') + 3) : qAssetsExponent; const maxUnstakingAmount = truncateToThreeDecimals(Number(qAssetsDisplay)); @@ -178,18 +175,15 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) const [isSigning, setIsSigning] = useState(false); - const [isError, setIsError] = useState(false); - const [transactionStatus, setTransactionStatus] = useState('Pending'); - const env = process.env.NEXT_PUBLIC_CHAIN_ENV; const quicksilverChainName = env === 'testnet' ? 'quicksilvertestnet' : 'quicksilver'; - const isCalculationDataLoaded = tokenAmount && !isNaN(Number(tokenAmount)) && zone && !isNaN(Number(zone.redemptionRate)); - const { requestRedemption } = quicksilver.interchainstaking.v1.MessageComposer.withTypeUrl; const numericAmount = Number(tokenAmount); const smallestUnitAmount = numericAmount * Math.pow(10, 6); const value: Coin = { amount: smallestUnitAmount.toFixed(0), denom: zone?.localDenom ?? '' }; + + // Create the message only executes if the unstake button is clickable const msgRequestRedemption = requestRedemption({ value: value, fromAddress: qAddress ?? '', @@ -212,16 +206,12 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) event.preventDefault(); setIsSigning(true); try { - const result = await tx([msgRequestRedemption], { + await tx([msgRequestRedemption], { fee, - onSuccess: () => { - setTransactionStatus('Success'); - }, + onSuccess: () => {}, }); } catch (error) { console.error('Transaction failed', error); - setTransactionStatus('Failed'); - setIsError(true); } finally { setIsSigning(false); } @@ -269,7 +259,7 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) let memo = memoBuffer.length > 0 && valoperAddress ? memoBuffer.toString('base64') : ''; const { send } = cosmos.bank.v1beta1.MessageComposer.withTypeUrl; - + // Create the message only executes if the liquid stake button is clickable const msgSend = send({ fromAddress: address ?? '', toAddress: zone?.depositAddress?.address ?? '', @@ -297,19 +287,15 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) const handleLiquidStake = async (event: React.MouseEvent) => { event.preventDefault(); setIsSigning(true); - setTransactionStatus('Pending'); + try { - const result = await sendTx([msgSend], { + await sendTx([msgSend], { memo, fee: stakeFee, - onSuccess: () => { - setTransactionStatus('Success'); - }, + onSuccess: () => {}, }); } catch (error) { console.error('Transaction failed', error); - setTransactionStatus('Failed'); - setIsError(true); } finally { setIsSigning(false); } @@ -548,7 +534,7 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) - + What you'll get q{selectedOption.value.toUpperCase()}: @@ -556,7 +542,11 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) {/* This pushes the next Stat component to the right */} - {(Number(tokenAmount) / (Number(zone?.redemptionRate) || 1)).toFixed(2)} + {!isZoneLoading ? ( + (Number(tokenAmount) * Number(zone?.redemptionRate || 1)).toFixed(2) + ) : ( + + )} @@ -712,7 +702,11 @@ export const StakingBox = ({ selectedOption, valoperAddress }: StakingBoxProps) {/* This pushes the next Stat component to the right */} - {(Number(tokenAmount) * Number(zone?.redemptionRate || 1)).toFixed(2)} + {!isZoneLoading ? ( + (Number(tokenAmount) * Number(zone?.redemptionRate || 1)).toFixed(2) + ) : ( + + )} diff --git a/web-ui/pages/staking/index.tsx b/web-ui/pages/staking/index.tsx index 5c89fa063..5570df767 100644 --- a/web-ui/pages/staking/index.tsx +++ b/web-ui/pages/staking/index.tsx @@ -54,18 +54,19 @@ export default function Staking() { const [isStakingModalOpen, setStakingModalOpen] = useState(false); const [isTransferModalOpen, setTransferModalOpen] = useState(false); + const [isRevertSharesModalOpen, setRevertSharesModalOpen] = useState(false); return ( <> Staking - + diff --git a/web-ui/public/img/index.js b/web-ui/public/img/index.js index db3c36119..d0bc4d00f 100644 --- a/web-ui/public/img/index.js +++ b/web-ui/public/img/index.js @@ -1,37 +1,37 @@ -import atom from "./networks/atom.svg"; -import aurastake from "./networks/aurastake.png"; -import cosmos from "./networks/cosmos.svg"; -import evmosPng from "./networks/evmos.png"; -import evmosSvg from "./networks/evmos.svg"; -import fishking from "./networks/fishking.png"; -import inj from "./networks/inj.svg"; -import junoPng from "./networks/juno.png"; -import junoSvg from "./networks/juno.svg"; -import k from "./networks/k.svg"; -import kaplrCircle from "./networks/kaplr-circle.svg"; -import keplr from "./networks/keplr.svg"; -import kraken from "./networks/kraken.png"; -import lavender2 from "./networks/lavender-2.png"; -import lavender from "./networks/lavender.png"; -import leap from "./networks/leap.svg"; -import osmosis from "./networks/osmosis.svg"; -import qAtom from "./networks/q-atom.svg"; -import qInj from "./networks/q-inj.svg"; -import qRegen from "./networks/q-regen.svg"; -import qinj from "./networks/qinj.svg"; -import qosmo from "./networks/qosmo.svg"; -import quicksilverSvg from "./networks/quicksilver.svg"; -import raydium from "./networks/raydium.png"; -import regen from "./networks/regen.svg"; -import sanka from "./networks/sanka.png"; -import smartnodes from "./networks/smartnodes.png"; -import stargaze2 from "./networks/stargaze-2.svg"; -import stargaze from "./networks/stargaze.png"; -import stargaze1 from "./networks/stargaze.svg"; -import stir from "./networks/stir.png"; -import terravegas from "./networks/terravegas.png"; -import logo from "./logo.png"; -import favicon from "./favicon.png"; +import faviconMain from './favicon-main.png'; +import logo from './logo.png'; +import atom from './networks/atom.svg'; +import aurastake from './networks/aurastake.png'; +import cosmos from './networks/cosmos.svg'; +import evmosPng from './networks/evmos.png'; +import evmosSvg from './networks/evmos.svg'; +import fishking from './networks/fishking.png'; +import inj from './networks/inj.svg'; +import junoPng from './networks/juno.png'; +import junoSvg from './networks/juno.svg'; +import k from './networks/k.svg'; +import kaplrCircle from './networks/kaplr-circle.svg'; +import keplr from './networks/keplr.svg'; +import kraken from './networks/kraken.png'; +import lavender2 from './networks/lavender-2.png'; +import lavender from './networks/lavender.png'; +import leap from './networks/leap.svg'; +import osmosis from './networks/osmosis.svg'; +import qAtom from './networks/q-atom.svg'; +import qInj from './networks/q-inj.svg'; +import qRegen from './networks/q-regen.svg'; +import qinj from './networks/qinj.svg'; +import qosmo from './networks/qosmo.svg'; +import quicksilverSvg from './networks/quicksilver.svg'; +import raydium from './networks/raydium.png'; +import regen from './networks/regen.svg'; +import sanka from './networks/sanka.png'; +import smartnodes from './networks/smartnodes.png'; +import stargaze2 from './networks/stargaze-2.svg'; +import stargaze from './networks/stargaze.png'; +import stargaze1 from './networks/stargaze.svg'; +import stir from './networks/stir.png'; +import terravegas from './networks/terravegas.png'; export { atom, @@ -67,5 +67,5 @@ export { stir, terravegas, logo, - favicon, + faviconMain, }; diff --git a/web-ui/public/img/networks/aurastake.png b/web-ui/public/img/networks/aurastake.png deleted file mode 100644 index a642885f6..000000000 Binary files a/web-ui/public/img/networks/aurastake.png and /dev/null differ diff --git a/web-ui/public/img/networks/dydx.png b/web-ui/public/img/networks/dydx.png new file mode 100644 index 000000000..44a45ea4c Binary files /dev/null and b/web-ui/public/img/networks/dydx.png differ diff --git a/web-ui/public/img/networks/dydx.svg b/web-ui/public/img/networks/dydx.svg new file mode 100644 index 000000000..f142b1ece --- /dev/null +++ b/web-ui/public/img/networks/dydx.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/web-ui/public/img/networks/evmos.png b/web-ui/public/img/networks/evmos.png deleted file mode 100644 index fc4e8bcb9..000000000 Binary files a/web-ui/public/img/networks/evmos.png and /dev/null differ diff --git a/web-ui/public/img/networks/evmos.svg b/web-ui/public/img/networks/evmos.svg deleted file mode 100644 index 409272546..000000000 --- a/web-ui/public/img/networks/evmos.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web-ui/public/img/networks/fishking.png b/web-ui/public/img/networks/fishking.png deleted file mode 100644 index 83f9c5475..000000000 Binary files a/web-ui/public/img/networks/fishking.png and /dev/null differ diff --git a/web-ui/public/img/networks/inj.svg b/web-ui/public/img/networks/inj.svg deleted file mode 100644 index 64d2961b8..000000000 --- a/web-ui/public/img/networks/inj.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/web-ui/public/img/networks/k.svg b/web-ui/public/img/networks/k.svg deleted file mode 100644 index 6870c9941..000000000 --- a/web-ui/public/img/networks/k.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/web-ui/public/img/networks/kaplr-circle.svg b/web-ui/public/img/networks/kaplr-circle.svg deleted file mode 100644 index dbb5025ee..000000000 --- a/web-ui/public/img/networks/kaplr-circle.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web-ui/public/img/networks/keplr.svg b/web-ui/public/img/networks/keplr.svg deleted file mode 100644 index 1d0e9b7e3..000000000 --- a/web-ui/public/img/networks/keplr.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web-ui/public/img/networks/kraken.png b/web-ui/public/img/networks/kraken.png deleted file mode 100644 index da512f6f6..000000000 Binary files a/web-ui/public/img/networks/kraken.png and /dev/null differ diff --git a/web-ui/public/img/networks/lavender-2.png b/web-ui/public/img/networks/lavender-2.png deleted file mode 100644 index fa6d4fa5a..000000000 Binary files a/web-ui/public/img/networks/lavender-2.png and /dev/null differ diff --git a/web-ui/public/img/networks/lavender.png b/web-ui/public/img/networks/lavender.png deleted file mode 100644 index 32c7d30bc..000000000 Binary files a/web-ui/public/img/networks/lavender.png and /dev/null differ diff --git a/web-ui/public/img/networks/q-inj.svg b/web-ui/public/img/networks/q-inj.svg deleted file mode 100644 index d4311e9e6..000000000 --- a/web-ui/public/img/networks/q-inj.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/web-ui/public/img/networks/qdydx.png b/web-ui/public/img/networks/qdydx.png new file mode 100644 index 000000000..61afdc900 Binary files /dev/null and b/web-ui/public/img/networks/qdydx.png differ diff --git a/web-ui/public/img/networks/raydium.png b/web-ui/public/img/networks/raydium.png deleted file mode 100644 index 1ff129706..000000000 Binary files a/web-ui/public/img/networks/raydium.png and /dev/null differ diff --git a/web-ui/public/img/networks/sanka.png b/web-ui/public/img/networks/sanka.png deleted file mode 100644 index a3e783ee7..000000000 Binary files a/web-ui/public/img/networks/sanka.png and /dev/null differ diff --git a/web-ui/public/img/networks/smartnodes.png b/web-ui/public/img/networks/smartnodes.png deleted file mode 100644 index 343bacd06..000000000 Binary files a/web-ui/public/img/networks/smartnodes.png and /dev/null differ diff --git a/web-ui/public/img/networks/stargaze-2.svg b/web-ui/public/img/networks/stargaze-2.svg deleted file mode 100644 index b462d964b..000000000 --- a/web-ui/public/img/networks/stargaze-2.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/web-ui/public/img/networks/stargaze.png b/web-ui/public/img/networks/stargaze.png deleted file mode 100644 index 18c2e7ef7..000000000 Binary files a/web-ui/public/img/networks/stargaze.png and /dev/null differ diff --git a/web-ui/public/img/networks/stir.png b/web-ui/public/img/networks/stir.png deleted file mode 100644 index 1e07d1625..000000000 Binary files a/web-ui/public/img/networks/stir.png and /dev/null differ diff --git a/web-ui/public/img/networks/terravegas.png b/web-ui/public/img/networks/terravegas.png deleted file mode 100644 index 125d09478..000000000 Binary files a/web-ui/public/img/networks/terravegas.png and /dev/null differ diff --git a/web-ui/public/img/svgexport-1(2).svg b/web-ui/public/img/svgexport-1(2).svg deleted file mode 100644 index dbb551644..000000000 --- a/web-ui/public/img/svgexport-1(2).svg +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web-ui/state/LiveZonesContext.tsx b/web-ui/state/LiveZonesContext.tsx new file mode 100644 index 000000000..ede60b343 --- /dev/null +++ b/web-ui/state/LiveZonesContext.tsx @@ -0,0 +1,35 @@ +import axios from 'axios'; +import { Zone } from 'quicksilverjs/dist/codegen/quicksilver/interchainstaking/v1/interchainstaking'; +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; + +interface LiveZonesContextType { + liveNetworks: string[]; +} + +interface LiveZonesProviderProps { + children: ReactNode; +} + +const LiveZonesContext = createContext({ liveNetworks: [] }); + +export const useLiveZones = (): LiveZonesContextType => useContext(LiveZonesContext); + +export const LiveZonesProvider: React.FC = ({ children }) => { + const [liveNetworks, setLiveNetworks] = useState([]); + + useEffect(() => { + const fetchLiveZones = async () => { + try { + const response = await axios.get(`${process.env.NEXT_PUBLIC_QUICKSILVER_API}/quicksilver/interchainstaking/v1/zones`); + const liveZones = response.data.zones.map((zone: Zone) => zone.chainId); + setLiveNetworks(liveZones); + } catch (error) { + console.error('Failed to fetch live zones:', error); + } + }; + + fetchLiveZones(); + }, []); + + return {children}; +}; diff --git a/web-ui/state/chains/prod.ts b/web-ui/state/chains/prod.ts index 7deac4566..4cc30a5ff 100644 --- a/web-ui/state/chains/prod.ts +++ b/web-ui/state/chains/prod.ts @@ -6,6 +6,7 @@ export const ibcDenomWithdrawMapping = { qREGEN: 'qregen', qSOMM: 'qsomm', qJUNO: 'qjuno', + qDYDX: 'qdydx', } }; @@ -16,7 +17,8 @@ export const ibcDenomWithdrawMapping = { qSTARS: 'ibc/46C83BB054E12E189882B5284542DB605D94C99827E367C9192CF0579CD5BC83', qREGEN: 'ibc/79A676508A2ECA1021EDDC7BB9CF70CEEC9514C478DA526A5A8B3E78506C2206', qSOMM: 'ibc/EAF76AD1EEF7B16D167D87711FB26ABE881AC7D9F7E6D0CF313D5FA530417208', - qJUNO: 'ibc/B4E18E61E1505C2F371B621E49B09E983F6A138F251A7B5286A6BDF739FD0D54' + qJUNO: 'ibc/B4E18E61E1505C2F371B621E49B09E983F6A138F251A7B5286A6BDF739FD0D54', + qDYDX: '' }, umee: { qATOM: 'ibc/454725EA4029BAA99C293904336DE9A4B84E2BF7D83B9C56EE6B03E8A65FB5A1', @@ -24,7 +26,8 @@ export const ibcDenomWithdrawMapping = { qSTARS: 'ibc/31946162F3E898B9E3A21792DD2AC740F2E82E7B92769BDF239C3DDA1726BB9F', qREGEN: 'ibc/16F0C7E49C2FE3A99E92A20DBCF4006B38ABC4E29F7F37829AD40F2C585BE835', qSOMM: 'ibc/ACF9DA139FE5BC8F95AC4A12B0B6D7710274DEDAC57284B881BEE1896F40642D', - qJUNO: 'ibc/CA0BEF2524A37205009210EFCFB09585FBA9648C5F065FA078944A5C6704E8DC' + qJUNO: 'ibc/CA0BEF2524A37205009210EFCFB09585FBA9648C5F065FA078944A5C6704E8DC', + qDYDX: '' }, }; @@ -45,6 +48,14 @@ export const networks = [ chainName: 'osmosis', chainId: 'osmosis-1', }, + { + value: 'DYDX', + logo: '/img/networks/dydx.png', + qlogo: '/img/networks/qdydx.png', + name: 'Dydx', + chainName: 'dydx', + chainId: 'dydx-mainnet-1', + }, { value: 'STARS', logo: '/img/networks/stargaze.svg', diff --git a/web-ui/styles/globals.css b/web-ui/styles/globals.css index 24769c2cc..6d3d2edeb 100644 --- a/web-ui/styles/globals.css +++ b/web-ui/styles/globals.css @@ -197,7 +197,7 @@ a { .custom-scrollbar { overflow-y: auto; scrollbar-width: thin; - scrollbar-color: transparent transparent; /* Makes the track transparent */ + scrollbar-color: transparent transparent; } .custom-scrollbar::-webkit-scrollbar { @@ -206,7 +206,7 @@ a { } .custom-scrollbar::-webkit-scrollbar-track { - background: transparent; /* Makes the track transparent */ + background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { diff --git a/web-ui/tsconfig.json b/web-ui/tsconfig.json index 523efab02..c4b246dfe 100644 --- a/web-ui/tsconfig.json +++ b/web-ui/tsconfig.json @@ -12,7 +12,7 @@ "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, - "module": "esnext", + "module": "es2020", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, diff --git a/web-ui/tx/ibcTransferTx.tsx b/web-ui/tx/ibcTransferTx.tsx deleted file mode 100644 index 2673b3754..000000000 --- a/web-ui/tx/ibcTransferTx.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { Box, Link, useToast, Text } from '@chakra-ui/react'; -import { SigningStargateClient, StdFee } from '@cosmjs/stargate'; -import { ChainName } from '@cosmos-kit/core'; - -import { Dispatch, SetStateAction } from 'react'; -import { ibc } from 'interchain-query'; - -const showSuccessToast = (toast: ReturnType, txHash: string, chainName: ChainName) => { - const mintscanUrl = `https://www.mintscan.io/${chainName}/txs/${txHash}`; - toast({ - position: 'bottom-right', - duration: 5000, - isClosable: true, - render: () => ( - - - Transaction Successful - - - View on Mintscan: {mintscanUrl} - - - ), - }); -}; - -const showErrorToast = (toast: ReturnType, errorMsg: string) => { - toast({ - title: 'Transaction Failed', - description: `Error: ${errorMsg}`, - status: 'error', - duration: 5000, - isClosable: true, - position: 'bottom-right', - }); -}; - -export const ibcWithdrawlTx = async ( - dstAddress: string, - fromAddress: string, - getSigningStargateClient: () => Promise, - setResp: Dispatch>, - toast: ReturnType, - setIsError: Dispatch>, - setIsSigning: Dispatch>, - chainName: ChainName, -) => { - setIsError(false); - setIsSigning(true); - - try { - const stargateClient = await getSigningStargateClient(); - - if (!stargateClient || !fromAddress) { - console.error('Stargate client undefined or fromAddress undefined.'); - return; - } - - const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; - - const msgIbcTransfer = transfer({ - sourcePort: 'transfer', - sourceChannel: 'channel-0', - token: { - denom: 'uqck', - amount: '7500', - }, - sender: fromAddress, - receiver: dstAddress, - timeoutHeight: { - revisionNumber: BigInt(0), - revisionHeight: BigInt(0), - }, - timeoutTimestamp: BigInt(0), - memo: '', - }); - - const fee: StdFee = { - amount: [ - { - denom: 'uqck', - amount: '7500', - }, - ], - gas: '500000', - }; - - const response = await stargateClient.signAndBroadcast(fromAddress, [msgIbcTransfer], fee); - - // Handle response - setResp(JSON.stringify(response, null, 2)); - setIsSigning(false); - - if (response.code === 0) { - showSuccessToast(toast, response.transactionHash, chainName); - } else { - setIsError(true); - showErrorToast(toast, 'Transaction failed'); - } - } catch (error) { - console.error('Error in unbonding transaction:', error); - if (error instanceof Error) { - setIsSigning(false); - setIsError(true); - showErrorToast(toast, error.message); - } - } -}; diff --git a/web-ui/tx/intentTx.tsx b/web-ui/tx/intentTx.tsx deleted file mode 100644 index b16097ca6..000000000 --- a/web-ui/tx/intentTx.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { Box, Text, Link, useToast } from '@chakra-ui/react'; -import { StdFee } from '@cosmjs/amino'; -import { SigningStargateClient } from '@cosmjs/stargate'; -import { ChainName, Dispatch } from '@cosmos-kit/core'; -import { assets } from 'chain-registry'; -import { quicksilver } from 'quicksilverjs'; -import { ValidatorIntent } from 'quicksilverjs/dist/codegen/quicksilver/interchainstaking/v1/interchainstaking'; -import { SetStateAction } from 'react'; - -const showSuccessToast = (toast: ReturnType, txHash: string, chainName: ChainName) => { - const mintscanUrl = `https://www.mintscan.io/${chainName}/txs/${txHash}`; - toast({ - position: 'bottom-right', - duration: 5000, - isClosable: true, - render: () => ( - - - Transaction Successful - - - View on Mintscan: {mintscanUrl} - - - ), - }); -}; - -const showErrorToast = (toast: ReturnType, errorMsg: string) => { - toast({ - title: 'Transaction Failed', - description: `Error: ${errorMsg}`, - status: 'error', - duration: 5000, - isClosable: true, - position: 'bottom-right', - }); -}; - -export const intentTx = ( - getSigningStargateClient: () => Promise, - setResp: (resp: string) => any, - chainName: string, - chainId: string, - address: string | undefined, - intents: ValidatorIntent[], - toast: ReturnType, - setIsError: Dispatch>, - setIsSigning: Dispatch>, -) => { - setIsError(false); - setIsSigning(true); - - return async (event: React.MouseEvent) => { - event.preventDefault(); - const apiUrl = 'https://rpc.test.quicksilver.zone'; - const stargateClient = await getSigningStargateClient(); - - if (!stargateClient || !address) { - console.error('Stargate client undefined or address undefined.'); - return; - } - - const { signalIntent } = quicksilver.interchainstaking.v1.MessageComposer.withTypeUrl; - const msgSignalIntent = signalIntent({ - chainId: chainId, - intents: intents, - fromAddress: address, - }); - - const mainTokens = assets.find(({ chain_name }) => chain_name === chainName); - const mainDenom = mainTokens?.assets[0].base ?? 'uqck'; - - const fee: StdFee = { - amount: [ - { - denom: mainDenom, - amount: '5000', - }, - ], - gas: '500000', - }; - - try { - const response = await stargateClient.signAndBroadcast(address, [msgSignalIntent], fee); - setResp(JSON.stringify(response, null, 2)); - setIsSigning(false); - showSuccessToast(toast, response.transactionHash, chainName); - } catch (error) { - console.error('Error signing and sending transaction:', error); - if (error instanceof Error) { - setIsSigning(false); - setIsError(true); - showErrorToast(toast, error.message); - } - } - }; -}; diff --git a/web-ui/tx/liquidStakeTx.tsx b/web-ui/tx/liquidStakeTx.tsx deleted file mode 100644 index 9c68504aa..000000000 --- a/web-ui/tx/liquidStakeTx.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { Box, Text, Link, useToast } from '@chakra-ui/react'; -import { StdFee } from '@cosmjs/amino'; -import { coins, Coin, SigningStargateClient } from '@cosmjs/stargate'; -import { ChainName, Dispatch } from '@cosmos-kit/core'; -import { quicksilver } from '@hoangdv2429/quicksilverjs'; -import { bech32 } from 'bech32'; -import { assets } from 'chain-registry'; -import chains from 'chain-registry'; -import { cosmos } from 'interchain-query'; -import { Zone } from 'quicksilverjs/types/codegen/quicksilver/interchainstaking/v1/interchainstaking'; -import { SetStateAction } from 'react'; - -import { shiftDigits } from '@/utils'; - -const showSuccessToast = (toast: ReturnType, txHash: string, chainName: ChainName) => { - const mintscanUrl = `https://www.mintscan.io/${chainName}/txs/${txHash}`; - toast({ - position: 'bottom-right', - duration: 5000, - isClosable: true, - render: () => ( - - - Transaction Successful - - - View on Mintscan: {mintscanUrl} - - - ), - }); -}; - -const showErrorToast = (toast: ReturnType, errorMsg: string) => { - toast({ - title: 'Transaction Failed', - description: `Error: ${errorMsg}`, - status: 'error', - duration: 5000, - isClosable: true, - position: 'bottom-right', - }); -}; - -interface ValidatorsSelect { - address: string; - intent: number; -} - -export const liquidStakeTx = ( - getSigningStargateClient: () => Promise, - setResp: (resp: string) => any, - chainName: string, - chainId: string, - address: string | undefined, - toast: ReturnType, - setIsError: Dispatch>, - setIsSigning: Dispatch>, - validatorsSelect: ValidatorsSelect[], - amount: number, - zone: Zone | undefined, -) => { - setIsError(false); - setIsSigning(true); - - const valToByte = (val: number) => { - if (val > 1) { - val = 1; - } - if (val < 0) { - val = 0; - } - return Math.abs(val * 200); - }; - - const addValidator = (valAddr: string, weight: number) => { - let { words } = bech32.decode(valAddr); - let wordsUint8Array = new Uint8Array(bech32.fromWords(words)); - let weightByte = valToByte(weight); - return Buffer.concat([Buffer.from([weightByte]), wordsUint8Array]); - }; - - let memoBuffer = Buffer.alloc(0); - - if (validatorsSelect.length > 0) { - validatorsSelect.forEach((val) => { - memoBuffer = Buffer.concat([memoBuffer, addValidator(val.address, val.intent)]); - }); - memoBuffer = Buffer.concat([Buffer.from([0x02, memoBuffer.length]), memoBuffer]); - } - - let memo = memoBuffer.length > 0 ? memoBuffer.toString('base64') : ''; - - return async (event: React.MouseEvent) => { - event.preventDefault(); - const stargateClient = await getSigningStargateClient(); - - if (!stargateClient || !address) { - console.error('Stargate client undefined or address undefined.'); - return; - } - - const { send } = cosmos.bank.v1beta1.MessageComposer.withTypeUrl; - const msgSend = send({ - fromAddress: address, - toAddress: zone?.depositAddress?.address ?? '', - amount: coins(amount.toFixed(0), zone?.baseDenom ?? ''), - }); - - const mainTokens = assets.find(({ chain_name }) => chain_name === chainName); - const fees = chains.chains.find(({ chain_name }) => chain_name === chainName)?.fees?.fee_tokens; - const mainDenom = mainTokens?.assets[0].base ?? ''; - const fixedMinGasPrice = fees?.find(({ denom }) => denom === mainDenom)?.average_gas_price ?? ''; - const feeAmount = shiftDigits(fixedMinGasPrice, 6); - - const fee: StdFee = { - amount: [ - { - denom: mainDenom, - amount: feeAmount.toString(), - }, - ], - gas: '500000', - }; - - try { - const response = await stargateClient.signAndBroadcast(address, [msgSend], fee, memo); - - setResp(JSON.stringify(response, null, 2)); - setIsSigning(false); - showSuccessToast(toast, response.transactionHash, chainName); - } catch (error) { - console.error('Error signing and sending transaction:', error); - if (error instanceof Error) { - setIsSigning(false); - setIsError(true); - showErrorToast(toast, error.message); - } - } - }; -}; - -export const unbondLiquidStakeTx = async ( - dstAddress: string, - fromAddress: string, - unbondAmount: number, - local_denom: string, - getSigningStargateClient: () => Promise, - setResp: Dispatch>, - toast: ReturnType, - setIsError: Dispatch>, - setIsSigning: Dispatch>, - chainName: ChainName, -) => { - setIsError(false); - setIsSigning(true); - - try { - const stargateClient = await getSigningStargateClient(); - - if (!stargateClient || !fromAddress) { - console.error('Stargate client undefined or fromAddress undefined.'); - return; - } - - const { requestRedemption } = quicksilver.interchainstaking.v1.MessageComposer.withTypeUrl; - - const value: Coin = { amount: unbondAmount.toFixed(0), denom: local_denom }; - const msgRequestRedemption = requestRedemption({ - value: value, - fromAddress: fromAddress, - destinationAddress: dstAddress, - }); - - const fee: StdFee = { - amount: [ - { - denom: 'uqck', - amount: '7500', - }, - ], - gas: '500000', - }; - - const response = await stargateClient.signAndBroadcast(fromAddress, [msgRequestRedemption], fee); - - // Handle response - setResp(JSON.stringify(response, null, 2)); - setIsSigning(false); - - if (response.code === 0) { - showSuccessToast(toast, response.transactionHash, chainName); - } else { - setIsError(true); - showErrorToast(toast, 'Transaction failed'); - } - } catch (error) { - console.error('Error in unbonding transaction:', error); - if (error instanceof Error) { - setIsSigning(false); - setIsError(true); - showErrorToast(toast, error.message); - } - } -}; diff --git a/web-ui/utils/ibc.ts b/web-ui/utils/ibc.ts index c392d7610..050a1125f 100644 --- a/web-ui/utils/ibc.ts +++ b/web-ui/utils/ibc.ts @@ -70,8 +70,8 @@ export const getIbcInfo = (fromChainName: string, toChainName: string) => { } const key = flipped ? 'chain_2' : 'chain_1'; - const sourcePort = ibcInfo.channels[0][key].port_id; - const sourceChannel = ibcInfo.channels[0][key].channel_id; + const source_port = ibcInfo.channels[0][key].port_id; + const source_channel = ibcInfo.channels[0][key].channel_id; - return { sourcePort, sourceChannel }; + return { source_port, source_channel }; }; \ No newline at end of file diff --git a/web-ui/utils/maths.ts b/web-ui/utils/maths.ts index 148462a47..0d3459194 100644 --- a/web-ui/utils/maths.ts +++ b/web-ui/utils/maths.ts @@ -26,6 +26,11 @@ export const toNumber = ( .toNumber(); }; +export function truncateToTwoDecimals(num: number) { + const multiplier = Math.pow(10, 2); + return Math.floor(num * multiplier) / multiplier; +} + export const sum = (...args: string[]) => { return args .reduce( diff --git a/web-ui/utils/staking.ts b/web-ui/utils/staking.ts index 69a78b381..6fb355239 100644 --- a/web-ui/utils/staking.ts +++ b/web-ui/utils/staking.ts @@ -1,11 +1,12 @@ import { Coin, decodeCosmosSdkDecFromProto } from '@cosmjs/stargate'; +import * as bech32 from 'bech32'; import BigNumber from 'bignumber.js'; +import * as CryptoJS from 'crypto-js'; import { QueryDelegationTotalRewardsResponse } from 'interchain-query/cosmos/distribution/v1beta1/query'; import { QueryAnnualProvisionsResponse } from 'interchain-query/cosmos/mint/v1beta1/query'; import { QueryDelegatorDelegationsResponse, QueryParamsResponse } from 'interchain-query/cosmos/staking/v1beta1/query'; -import { Pool, Validator } from 'interchain-query/cosmos/staking/v1beta1/staking'; -import * as bech32 from 'bech32'; -import * as CryptoJS from 'crypto-js'; +import { Pool } from 'interchain-query/cosmos/staking/v1beta1/staking'; +import { Validator } from 'quicksilverjs/dist/codegen/cosmos/staking/v1beta1/staking'; import { decodeUint8Arr, isGreaterThanZero, shiftDigits, toNumber } from '.'; @@ -53,6 +54,7 @@ export const parseValidators = (validators: Validator[]) => { const commissionRate = validator.commission?.commission_rates?.rate || ZERO; const commissionPercentage = parseFloat(commissionRate) * 100; + //TODO: Add valconsAddress to query missed blocks const valconsPrefix = extractValconsPrefix(validator.operator_address); const valconsAddress = getValconsAddress(validator.consensus_pubkey, valconsPrefix); @@ -63,21 +65,21 @@ export const parseValidators = (validators: Validator[]) => { name: validator.description?.moniker || '', identity: validator.description?.identity || '', address: validator.operator_address || '', - commission: commissionPercentage.toFixed() + '%', + commission: commissionPercentage.toFixed(2) + '%', votingPower: toNumber(shiftDigits(validator.tokens, -6, 0), 0), }; }); }; -function getValconsAddress(consensus_pubkey: any, valconsPrefix: string) { - if (!consensus_pubkey || typeof consensus_pubkey.key !== 'string') { +function getValconsAddress(consensusPubkey: any, valconsPrefix: string) { + if (!consensusPubkey || typeof consensusPubkey.key !== 'string') { console.error('Invalid or missing consensus public key'); return ''; } try { // Decode the Base64 key directly to bytes - const decoded = Buffer.from(consensus_pubkey.key, 'base64'); + const decoded = Buffer.from(consensusPubkey.key, 'base64'); // Convert bytes to Bech32 words const valconsWords = bech32.bech32.toWords(new Uint8Array(decoded));